中关村线下赛pwn题解
TOC
- 1. 预置后门
- 2. 格式化字符串
- 3. off by one
- 4. double free
- 5. 栈溢出
- 6. 如何恢复源程序
源程序和 patch 好的程序下载链接:zhonguancun.zip。
比赛的时候只有一道pwn题,但是此次的pwn题是新题型,主办方并没有给源程序,我们只有执行的权限,没有读的权限。但是我们可以利用 动态库劫持 或者 ptrace 来直接读取内存。
读取代码如下:
#include <stdio.h> #include <dlfcn.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/prctl.h>
#define PATH "/home/ctf/pwn"
void my_init(void) __attribute__((constructor));
void my_init() { char *image_base; int result, fd; unsigned long long offset;
setbuf(stdout, NULL);
image_base = *(char **)dlopen(NULL, RTLD_LAZY); fd = open(PATH, O_RDWR|O_CREAT|O_NOCTTY, 0644); if(fd < 0) { perror("open"); exit(EXIT_FAILURE); }
offset = 0; while(offset < 0x1000000 ) { result = write(fd, image_base + offset, 0x1000); if(result > 0) { printf("%p\n", image_base + offset); } offset += 0x1000; } exit(EXIT_SUCCESS); }
|
读出来的内存并不能直接运行,但是能用IDA进行分析。
下面我说一下他的一些漏洞,以及patch方法。
预置后门
void __cdecl flag() { int v0; char buf[256]; unsigned __int64 v2;
v2 = __readfsqword(0x28u); memset(buf, 0, sizeof(buf)); v0 = open("/flag", 0, buf); read(v0, buf, 0x100uLL); printf("%s", "If you want to get flag, please login ...\n", "Don't patch this function!!!"); if ( (unsigned int)check_passwd() ) puts(buf); memset(buf, 0, 0x100uLL); }
__int64 check_passwd() { int fd; char v2[32]; char s[8]; __int64 v4; __int64 buf; __int64 v6; unsigned __int64 v7;
v7 = __readfsqword(0x28u); fd = open("/tmp/passwd", 0); *(_QWORD *)v2 = 'nimda'; *(_QWORD *)&v2[8] = 0LL; *(_QWORD *)&v2[16] = 0LL; *(_QWORD *)&v2[24] = 0LL; *(_QWORD *)s = 0LL; v4 = 0LL; buf = 0LL; v6 = 0LL; if ( !fd ) puts("Open File Fail"); read(fd, &buf, 6uLL); printf("Please input your name: ", &buf); read_n(&v2[16], 16); if ( !strcmp(&v2[16], v2) ) { printf("%s, Please input your password: ", &v2[16]); gets(s); } printf("We just hava a account -> \x1B[%dm%s\x1B[0m\n", '"', v2); return 0LL; }
|
对于上面的代码,如果有实力 patch 的话,可以直接 nop 掉open和puts函数。
否则的话就只能设置密码,而且如果程序不能读/tmp/passwd
文件的话,程序默认为空,那么就是空密码,则可以直接泄露flag,但是即使设置了密码,但是程序会返回匹配成功的字节数,这里必须要查看汇编才可以得知这一点。
LOAD:0000000000402DF2 loc_402DF2: LOAD:0000000000402DF2 mov eax, [rbp+success_amount] LOAD:0000000000402DF5 cdqe LOAD:0000000000402DF7 movzx edx, byte ptr [rbp+rax+buf] LOAD:0000000000402DFC mov eax, [rbp+success_amount] LOAD:0000000000402DFF cdqe LOAD:0000000000402E01 movzx eax, [rbp+rax+s] LOAD:0000000000402E06 cmp dl, al LOAD:0000000000402E08 jz short loc_402E25 LOAD:0000000000402E0A mov eax, [rbp+success_amount] LOAD:0000000000402E0D mov esi, eax LOAD:0000000000402E0F mov edi, offset aLoginConfirmFa LOAD:0000000000402E14 mov eax, 0 LOAD:0000000000402E19 call _printf LOAD:0000000000402E1E mov eax, 0 LOAD:0000000000402E23 jmp short loc_402E3E
|
所以只要对6个字节进行逐个爆破,就能爆破出密码,如果没有实力 patch 的话,防御方法就是经常换密码。
格式化字符串
void __cdecl fmt(__int64 a1, unsigned int a2) { char *v2; int fd; char format[32]; unsigned __int64 v5;
v5 = __readfsqword(0x28u); sub_4023BF(a1, a2); fd = open("/tmp/user_lock", 0); if ( !fd ) { puts("Open Fail.."); sub_400E40(0LL); } if ( fd == -1 ) { v2 = "Function not yet implemented.\nThis is the beta version."; puts("Function not yet implemented.\nThis is the beta version."); *(_QWORD *)format = 0LL; *(_QWORD *)&format[8] = 0LL; *(_QWORD *)&format[16] = 0LL; *(_QWORD *)&format[24] = 0LL; while ( 1 ) { sub_4023BF((__int64)v2, 0); --dword_606194; read(0, format, 28uLL); if ( format[0] == 'q' || format[0] == 'Q' || !strncmp(format, "exit", 4uLL) ) break; printf(format, "exit"); v2 = format; memset(format, 0, 32uLL); } } else { puts("user_lock"); --dword_606194; } }
|
这里,如果 open("/tmp/user_lock", 0);
失败的话,就可以进行格式化字符串漏洞,由于payload 仅有28个字节,我们可以逐个字节修改 atoi.got 为 system 地址,下次在进行 get_int 时 输入 sh 即可起shell。
防御方法还是分为两种,有能力patch 就直接让open返回 1,并把printf 修改掉,否则直接创建/tmp/user_lock
文件也行。
off by one
程序还有了 heap 题,如下面代码所示,由于使用的size是chunk_size,那么如果chunk有prev_inuse标志位的我们则可以溢出一个字节,但是还有8个字节的填充区,理论上这里 是利用不了的。这里的修复方式:对size进行对齐操作即可。
void __fastcall edit(__int64 a1, unsigned int a2) { int v2; int v3;
sub_4023BF(a1, a2); printf("1. modify name\n2. modify note\nChoose >"); v2 = get_int(); if ( (unsigned int)(v2 - 1) <= 1 ) { print_pipe_info(0); v3 = get_pipe_id((__int64)"Please choose the modify pipe id: "); if ( v3 < 0 ) return; printf("Plese input info >"); if ( v2 == 1 ) read_n(gloabl_pipes[v3].malloc_ptr, *((_DWORD *)gloabl_pipes[v3].malloc_ptr - 2) - 16); else read_n(gloabl_pipes[v3].malloc_note, 256); puts("OK, the pipe info is changed, spend money 1"); --dword_606194; } puts("Invalid choose"); }
|
我们amount开始为51,每次做不同的操作都会让amount减少,当amount为负数时,则check_amount会直接退出。
并且print_pipe_info
中还有一个格式化字符串漏洞,用这个泄露基地址,劫持got表。这个patch 的方式,只要加上第一个参数%s
就可以了。
void __cdecl print_pipe_info(int a1) { ... if ( gloabl_pipes[i].malloc_note ) printf(gloabl_pipes[i].malloc_note); puts(&byte_4042B7); ... }
|
double free
这里没有清理指针,由于环境是glibc-2.28,此时并没有tcache的double free的检查,所以这个漏洞还是很好利用的。修复 时完成指针清理操作就可以了。
void __fastcall delete(__int64 a1, unsigned int a2) { int v2;
check_amount(); v2 = get_pipe_id((__int64)"Please input want to destroy id: "); if ( v2 >= 0 ) { free(gloabl_pipes[v2].malloc_ptr); free(gloabl_pipes[v2].malloc_note); puts("OK, the pipe is successfully destroy, Spend money 5"); amount -= 5; } }
|
栈溢出
gets函数直接导致溢出。配合上面的格式化串泄露 stack_guard 就能利用该漏洞。这里用 read 函数完成 gets 的功能,并进行替换即可。
__int64 check_passwd() { int fd; char v2[32]; char s[8]; __int64 v4; __int64 buf; __int64 v6; unsigned __int64 v7;
v7 = __readfsqword(0x28u); fd = open("/tmp/passwd", 0); *(_QWORD *)v2 = 'nimda'; *(_QWORD *)&v2[8] = 0LL; *(_QWORD *)&v2[16] = 0LL; *(_QWORD *)&v2[24] = 0LL; *(_QWORD *)s = 0LL; v4 = 0LL; buf = 0LL; v6 = 0LL; if ( !fd ) puts("Open File Fail"); read(fd, &buf, 6uLL); printf("Please input your name: ", &buf); read_n(&v2[16], 16); if ( !strcmp(&v2[16], v2) ) { printf("%s, Please input your password: ", &v2[16]); gets(s); } printf("We just hava a account -> \x1B[%dm%s\x1B[0m\n", '"', v2); return 0LL; }
|
patch 的汇编如下:
mov rsi, rdi push rdi xor edi, edi mov edx, 0x10 call read pop rdi dec eax add rdi, rax cmp byte ptr [rdi], 0xa jne over mov byte ptr [rdi], 0
over: ret
|
如何恢复源程序
由于从内存dump下来的程序中没有section表,所以我们需要手动还原该表,主办方也给了我们相关资料,但是无论如何还原程序都是一项复杂的工作,这里我用一个小技巧来运行程序。
直接把dump下来的内存通过一个程序转载到内存里,然后再跳到main函数执行就行。
其中我们只要对got表和std指针进行复原就行,之后程序便能正常运行。装载程序代码如下所示。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <unistd.h> #include <fcntl.h> #include <dlfcn.h> #include <sys/mman.h> #include <gnu/lib-names.h> #include <termios.h>
#define PWN_PATH "./pwn" #define PWN_IMAGE_BASE 0x400000 #define PWN_GOT_OFFSET (0x606018-0x400000) #define PWN_STD_OFFSET (0x6061A0-0x400000) #define PWN_MAIN_OFFSET (0x403C6E -0x400000)
char *gets(char *);
int main(int argc, char **args, char **envp) { char *image_base, **dst, **src; int fd; int (*main_entry)(int, char **, char**);
fd = open(PWN_PATH, O_RDONLY);
image_base = mmap(PWN_IMAGE_BASE, 0x6000, PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); read(fd, image_base, 0x6000); mmap(image_base + 0x205000, 0x3000, PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); read(fd, image_base + 0x205000, 0x3000); dst = image_base + PWN_GOT_OFFSET; dst[0] = free; dst[1] = pthread_create; dst[2] = strncmp; dst[3] = puts; dst[4] = strlen; dst[5] = exit; dst[6] = system; dst[7] = printf; dst[8] = memset; dst[9] = alarm; dst[10] = read; dst[12] = srand; dst[13] = strcmp; dst[14] = getchar; dst[15] = memcpy; dst[16] = time; dst[17] = gets; dst[18] = pthread_exit; dst[19] = malloc; dst[20] = setvbuf; dst[21] = tcgetattr; dst[22] = tcsetattr; dst[23] = open; dst[24] = pthread_join; dst[25] = atoi; dst[26] = scanf; dst[27] = sprintf; dst[28] = exit; dst[29] = sleep; dst[30] = rand; dst[31] = usleep; dst = image_base + PWN_STD_OFFSET; dst[0] = stdout; dst[2] = stdin; dst[4] = stderr;
main_entry = image_base + PWN_MAIN_OFFSET;
mprotect(image_base, 0x6000, PROT_READ|PROT_EXEC); mprotect(image_base + 0x205000, 0x3000, PROT_READ|PROT_WRITE); return main_entry(argc, args, envp); }
|