本文是为了学习apple做的铺垫,两种利用手法的利用原理是基于ctfshow的pwn213和pwn214动调和一些师傅的博客总结得出,如有侵权联系删除
House of kiwi 利用条件
能够触发__malloc_assert
能够申请到_IO_file_sync 和 _IO_helper_jumps这两个位置并且修改。
利用原理 在函数sysmalloc中,有一个检查top chunk是否对其的代码片段:
1 2 3 4 assert ((old_top == initial_top (av) && old_size == 0 ) || ((unsigned long ) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long ) old_end & (pagesize - 1 )) == 0 ));
如果检查没有通过,会触发__malloc_assert:
1 2 3 4 5 6 7 8 9 10 11 static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function) { (void ) __fxprintf (NULL , "%s%s%s:%u: %s%sAssertion `%s' failed.\n" , __progname, __progname[0 ] ? ": " : "" , file, line, function ? function : "" , function ? ": " : "" , assertion); fflush (stderr ); abort (); }
我们要劫持的流就是这里的fflush(stderr)中的_IO_SYNC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int _IO_fflush (FILE *fp) { if (fp == NULL ) return _IO_flush_all (); else { int result; CHECK_FILE (fp, EOF); _IO_acquire_lock (fp); result = _IO_SYNC (fp) ? EOF : 0 ; _IO_release_lock (fp); return result; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 0x7ffff7e00208 <__GI__IO_fflush+88 >: lea rdx,[rip+0x1967f1 ] 0x7ffff7e0020f <__GI__IO_fflush+95 >: lea rax,[rip+0x197552 ] 0x7ffff7e00216 <__GI__IO_fflush+102 >: sub rax,rdx0x7ffff7e00219 <__GI__IO_fflush+105 >: mov rcx,rbp0x7ffff7e0021c <__GI__IO_fflush+108 >: sub rcx,rdx0x7ffff7e0021f <__GI__IO_fflush+111 >: cmp rax,rcx0x7ffff7e00222 <__GI__IO_fflush+114 >: jbe 0x7ffff7e00268 <__GI__IO_fflush+184 > 0x7ffff7e00224 <__GI__IO_fflush+116 >: mov rdi,rbx0x7ffff7e00227 <__GI__IO_fflush+119 >: call QWORD PTR [rbp+0x60 ] 0x7ffff7e0022a <__GI__IO_fflush+122 >: neg eax0x7ffff7e0022c <__GI__IO_fflush+124 >: sbb r12d,r12d0x7ffff7e0022f <__GI__IO_fflush+127 >: test DWORD PTR [rbx],0x8000 0x7ffff7e00235 <__GI__IO_fflush+133 >: jne 0x7ffff7e00258 <__GI__IO_fflush+168 >0x7ffff7e00237 <__GI__IO_fflush+135 >: mov rdi,QWORD PTR [rbx+0x88 ]0x7ffff7e0023e <__GI__IO_fflush+142 >: mov eax,DWORD PTR [rdi+0x4 ]0x7ffff7e00241 <__GI__IO_fflush+145 >: sub eax,0x1 0x7ffff7e00244 <__GI__IO_fflush+148 >: mov DWORD PTR [rdi+0x4 ],eax0x7ffff7e00247 <__GI__IO_fflush+151 >: jne 0x7ffff7e00258 <__GI__IO_fflush+168 >0x7ffff7e00249 <__GI__IO_fflush+153 >: mov QWORD PTR [rdi+0x8 ],0x0
在call QWORD PTR [rbp+0x60]
处下断点,
1 *RBP 0x7ffff7f97600 (_IO_file_jumps) ◂— 0x0
bp为_IO_file_jumps
也就是vtable
,我们要利用的就是这里面的_IO_default_sync
指针
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_wstrn_jumps attribute_hidden = { JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_wstr_finish), JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstrn_overflow), JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow), JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow), JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail), JUMP_INIT(xsputn, _IO_wdefault_xsputn), JUMP_INIT(xsgetn, _IO_wdefault_xsgetn), JUMP_INIT(seekoff, _IO_wstr_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_default_setbuf), JUMP_INIT(sync, _IO_default_sync), # 利用这个函数指针 JUMP_INIT(doallocate, _IO_wdefault_doallocate), JUMP_INIT(read, _IO_default_read), JUMP_INIT(write, _IO_default_write), JUMP_INIT(seek, _IO_default_seek), JUMP_INIT(close, _IO_default_close), JUMP_INIT(stat, _IO_default_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) };
如果我们将其修改为setcontext+61
,就能触发这个magic gadget的汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .text:0000000000053A6D mov rsp, [rdx+0A0h] .text:0000000000053A74 mov rbx, [rdx+80h] .text:0000000000053A7B mov rbp, [rdx+78h] .text:0000000000053A7F mov r12, [rdx+48h] .text:0000000000053A83 mov r13, [rdx+50h] .text:0000000000053A87 mov r14, [rdx+58h] .text:0000000000053A8B mov r15, [rdx+60h] .text:0000000000053A8F test dword ptr fs:48h, 2 .text:0000000000053A9B jz loc_53B56 ...... .text:0000000000053B56 mov rcx, [rdx+0A8h] .text:0000000000053B5D push rcx .text:0000000000053B5E mov rsi, [rdx+70h] .text:0000000000053B62 mov rdi, [rdx+68h] .text:0000000000053B66 mov rcx, [rdx+98h] .text:0000000000053B6D mov r8, [rdx+28h] .text:0000000000053B71 mov r9, [rdx+30h] .text:0000000000053B75 mov rdx, [rdx+88h] .text:0000000000053B75 ; } // starts at 53A30 .text:0000000000053B7C ; __unwind { .text:0000000000053B7C xor eax, eax .text:0000000000053B7E retn
在call _IO_file_sync
时,rdx的值实际上是_IO_helper_jumps
的地址,这个值是稳定不变的,实际上这个结构体的地址就在_IO_file_jumps
前面一点:
1 2 3 4 5 6 7 __libc_IO_vtables:0000000000215A00 qword_215A00 dq 0 ; DATA XREF: sub_45390+1A3↑o __libc_IO_vtables:0000000000215A00 ; sub_5A980+1643↑o ... __libc_IO_vtables:0000000000215A08 dq 0 __libc_IO_vtables:0000000000215A10 dq offset _IO_default_finish __libc_IO_vtables:0000000000215A18 dq offset sub_722E0 __libc_IO_vtables:0000000000215A20 dq offset sub_8DDD0 __libc_IO_vtables:0000000000215A28 dq offset _IO_default_uflow
然后我们构造一个orw的rop链放在一个已知地址的位置,并让rsp = [_IO_helper_jumps+0xA0
]等于ROP链的地址,[_IO_helper_jumps+0xA8
]等于一条ret指令的地址,这样,在执行到push rcx后,rsp中是这样的
1 2 3 4 5 6 7 8 9 pwndbg> tele 0x555555605158 00:0000│ rsp 0x555555605158 —▸ 0x7ffff7c2a3e6 (iconv+198) ◂— ret 01:0008│ 0x555555605160 (ROP) —▸ 0x7ffff7c45eb0 (mblen+112) ◂— pop rax 02:0010│ 0x555555605168 (ROP+8) ◂— 0x2 03:0018│ 0x555555605170 (ROP+16) —▸ 0x7ffff7c2a3e5 (iconv+197) ◂— pop rdi 04:0020│ 0x555555605178 (ROP+24) —▸ 0x555555605020 (FLAG) ◂— '/ctfshow_flag' 05:0028│ 0x555555605180 (ROP+32) —▸ 0x7ffff7c2be51 ◂— pop rsi 06:0030│ 0x555555605188 (ROP+40) ◂— 0x0 07:0038│ 0x555555605190 (ROP+48) —▸ 0x7ffff7c29db4 ◂— syscall
在执行完setcontext后,就会ret到我们劫持的这个rcx的ret,从而执行我们的ROP链,也就是完成了栈迁移
House of emma 利用条件 1.可以进行两次任意地址写堆地址(通常是largebin attack) 2.可以触发fsop
攻击方法 劫持stderr指针为我们构造的fake_IO_FILE__pointer_chk_guard
处写入已知内容,来绕过函数指针的保护
攻击限制 若stderr 的指针存放于 bss 段上,无法被我们修改,那么只能通过exit来触发FSOP,但由于我们的构造可能会导致异或内容被篡改后,exit无法正常执行,使得程序无法执行到我们构造的 IO流,需要攻击位于TLS结构体的_pointer_chk_guard
,并且远程可能需要爆破TLS偏移
源码分析 vtable虚表中有_IO_cookie_jumps
结构体,在_IO_cookie_jumps
中包含着_IO_cookie_read
,_IO_cookie_write
等一系列函数,这些函数存在着任意函数指针的调用,但是这些函数指针的调用被pointer_guard 进行了加密
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 static ssize_t _IO_cookie_read (FILE *fp, void *buf, ssize_t size) { struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_read_function_t *read_cb = cfile->__io_functions.read; #ifdef PTR_DEMANGLE PTR_DEMANGLE (read_cb); #endif if (read_cb == NULL ) return -1 ; return read_cb (cfile->__cookie, buf, size); } static ssize_t _IO_cookie_write (FILE *fp, const void *buf, ssize_t size) { struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_write_function_t *write_cb = cfile->__io_functions.write; #ifdef PTR_DEMANGLE PTR_DEMANGLE (write_cb); #endif if (write_cb == NULL ) { fp->_flags |= _IO_ERR_SEEN; return 0 ; } ssize_t n = write_cb (cfile->__cookie, buf, size); if (n < size) fp->_flags |= _IO_ERR_SEEN; return n; } static off64_t _IO_cookie_seek (FILE *fp, off64_t offset, int dir) { struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_seek_function_t *seek_cb = cfile->__io_functions.seek; #ifdef PTR_DEMANGLE PTR_DEMANGLE (seek_cb); #endif return ((seek_cb == NULL || (seek_cb (cfile->__cookie, &offset, dir)== -1 ) || offset == (off64_t ) -1 ) ? _IO_pos_BAD : offset); } static int _IO_cookie_close (FILE *fp) { struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_close_function_t *close_cb = cfile->__io_functions.close; #ifdef PTR_DEMANGLE PTR_DEMANGLE (close_cb); #endif if (close_cb == NULL ) return 0 ; return close_cb (cfile->__cookie); }
利用原理 1.伪造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 struct _IO_FILE { int _flags; char *_IO_read_ptr; char *_IO_read_end; char *_IO_read_base; char *_IO_write_base; char *_IO_write_ptr; char *_IO_write_end; char *_IO_buf_base; char *_IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers ; struct _IO_FILE *_chain ; int _fileno; int _flags2; __off_t _old_offset; unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1 ]; _IO_lock_t *_lock; #ifdef _IO_USE_OLD_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 0x0 _flags0x8 _IO_read_ptr0x10 _IO_read_end0x18 _IO_read_base0x20 _IO_write_base0x28 _IO_write_ptr0x30 _IO_write_end0x38 _IO_buf_base0x40 _IO_buf_end0x48 _IO_save_base0x50 _IO_backup_base0x58 _IO_save_end0x60 _markers0x68 _chain0x70 _fileno0x74 _flags20x78 _old_offset0x80 _cur_column0x82 _vtable_offset0x83 _shortbuf0x88 _lock0x90 _offset0x98 _codecvt0xa0 _wide_data0xa8 _freeres_list0xb0 _freeres_buf0xb8 __pad50xc0 _mode0xc4 _unused20xd8 vtable
需要关注的地方有:
1.*vtable指针:
2.*_lock
指针:这个指针的值应该为_IO_stdfile_1_lock
,所以我们得知道libc_base
,然后根据偏移计算出_IO_stdfile_1_lock
的具体地址
2.修改pointer_guard的值为已知值 house of emma利用方式有一条完整的函数调用链,我们需要这个pointer_guard的值来引导rip到我们想要的函数
需要注意的是pointer guard的值并不在libc中,而是在libc的低地址处,如果使用pwndbg,你可以看到在libc前面有一个匿名的内存区域,大小为0x3000但是我实际动调时发现是在libc下方,大小为5000,这个应该影响不大
tls结构体就位于这个匿名的内存空间中,它包含有pointer_guard,更具体地说,pointer_guard的值应该位于(libc_base - 0x3000 + 0x770),实际上,这个结构体的名字是tcbhead_t. 下面是它的构造:
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 (line 36 , /sysdeps/x86_64/nptl/tls.h) typedef struct { void *tcb; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; unsigned long int unused_vgetcpu_cache[2 ]; unsigned int feature_1; int __glibc_unused1; void *__private_tm[4 ]; void *__private_ss; unsigned long long int ssp_base; __128bits __glibc_unused2[8 ][4 ] __attribute__ ((aligned (32 ))); void *__padding[8 ]; } tcbhead_t ;
这里正好介绍一下怎么找到这个pointer_guard,我们都知道stack_guard是存放canary的地方,所以我们用canary和search命令就能很轻松的找到这个stack_guard,再往下8位就是我们要找的pointer_guard,而且,因为stack_guard就在pointer_guard上方,所以这里的值肯定是不能填错的
在另一篇博客 中学习到的方法:
比赛中我们可能不能获取到pointer_guard的值,但是可以利用一些手段将其改写为一个已知值,比如largebin attack或者hoa等等
3.__fxprintf
->__vfprintf_internal
->_IO_cookie_read
->mg2
->setcontext+61
还是跟kiwi一样,我们要利用top chunk检查时的宏定义,不过这次我们利用的是其中的__fxprintf
函数
1 2 3 4 5 6 7 8 9 10 11 static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function) { (void ) __fxprintf (NULL , "%s%s%s:%u: %s%sAssertion `%s' failed.\n" , __progname, __progname[0 ] ? ": " : "" , file, line, function ? function : "" , function ? ": " : "" , assertion); fflush (stderr ); abort (); }
fxprintf函数会间接调用到vxprintf_internal函数,后者会调用_IO_cookie_read函数:
1 <__vfprintf_internal+280> call qword ptr [r12 + 0x38]
而这里的r12寄存器的值就是(_IO_cookie_jumps + 0x38), 这就是我们前面写的*vtable值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static const struct _IO_jump_t _IO_cookie_jumps libio_vtable = { JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_file_finish), JUMP_INIT(overflow, _IO_file_overflow), JUMP_INIT(underflow, _IO_file_underflow), JUMP_INIT(uflow, _IO_default_uflow), JUMP_INIT(pbackfail, _IO_default_pbackfail), JUMP_INIT(xsputn, _IO_file_xsputn), JUMP_INIT(xsgetn, _IO_default_xsgetn), JUMP_INIT(seekoff, _IO_cookie_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_file_setbuf), JUMP_INIT(sync, _IO_file_sync), JUMP_INIT(doallocate, _IO_file_doallocate), JUMP_INIT(read, _IO_cookie_read), JUMP_INIT(write, _IO_cookie_write), JUMP_INIT(seek, _IO_cookie_seek), JUMP_INIT(close, _IO_cookie_close), JUMP_INIT(stat, _IO_default_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue), };
_IO_cookie_read
函数
1 2 3 4 5 6 7 8 9 10 11 12 <_IO_cookie_read>: endbr64 <_IO_cookie_read+4>: mov rax,QWORD PTR [rdi+0xe8] <_IO_cookie_read+11>: ror rax,0x11 <_IO_cookie_read+15>: xor rax,QWORD PTR fs:0x30 <_IO_cookie_read+24>: test rax,rax <_IO_cookie_read+27>: je <_IO_cookie_read+38> <_IO_cookie_read+29>: mov rdi,QWORD PTR [rdi+0xe0] <_IO_cookie_read+36>: jmp rax <_IO_cookie_read+38>: mov rax,0xffffffffffffffff <_IO_cookie_read+45>: ret
先看_IO_cookie_read
的中间几行汇编,这是高版本glibc的一种保护方式–将地址进行简单加密,这两条指令实际上是在解密rax,首先循环右移0x11位,然后异或fs:0x30h,也就是异或pointer_guard,加密方式很好反推出来:首先异或pointer_guard,然后循环左移0x11位
再看上下,它直接jmp rax
,也就是jmp QWORD PTR [rdi+0xe8]
,这里的rdi实际上就是假的_IO_FILE_plus
结构体的地址,因此我们可以将任意可执行的地址写入到[rdi+0xe8],如果这里没有沙箱,我们可以让[rdi+0xe8]等于system函数地址,[rdi+0xe0]等于字符串/bin/sh的地址,而开启了沙箱的程序需要我们利用magic gadget来利用rdx来完成栈迁移,但是这里跟kiwi有一些不同,kiwi中利用的_IO_file_sync
,在执行到_IO_file_sync
时rdx为_IO_helper_jumps
是可控的,而这里的rdx是不可控的,所以需要用到另一个gadget,称之为mg2
1 2 3 4 5 pwndbg> p/x 0x1675b0 + 0x7ffff7c00000 $5 = 0x7ffff7d675b0 pwndbg> tele 0x7ffff7d675b0 00:0000│ 0x7ffff7d675b0 ◂— mov rdx, qword ptr [rdi + 8] 01:0008│ 0x7ffff7d675b8 ◂— call qword ptr [rdx + 0x20]
这个gadget怎么找?
这个gadget能让rdx=[rdi+0xe8],然后call [rdx+0x20],也就是call [rdi+0x28]
而这里的rdi就是fake_io_file
,我们在fake_io_file+0xe8
处写mg2的地址,fake_io_file+0xe0
处写一个地址-0x28(假设是bss-0x28),这样程序会将rdi更改为[fake_io_file+0xe0],然后jmp到mg2,mg2中将rdx设置为[rdi+8]也就是bss-0x20,接着call [rdx+0x20]也就是call [bss+0x20],我们只需要在这里存setcontext+61
的地址即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .text:0000000000053A6D mov rsp, [rdx+0A0h] .text:0000000000053A74 mov rbx, [rdx+80h] .text:0000000000053A7B mov rbp, [rdx+78h] .text:0000000000053A7F mov r12, [rdx+48h] .text:0000000000053A83 mov r13, [rdx+50h] .text:0000000000053A87 mov r14, [rdx+58h] .text:0000000000053A8B mov r15, [rdx+60h] .text:0000000000053A8F test dword ptr fs:48h, 2 .text:0000000000053A9B jz loc_53B56 ...... .text:0000000000053B56 mov rcx, [rdx+0A8h] .text:0000000000053B5D push rcx .text:0000000000053B5E mov rsi, [rdx+70h] .text:0000000000053B62 mov rdi, [rdx+68h] .text:0000000000053B66 mov rcx, [rdx+98h] .text:0000000000053B6D mov r8, [rdx+28h] .text:0000000000053B71 mov r9, [rdx+30h] .text:0000000000053B75 mov rdx, [rdx+88h] .text:0000000000053B75 ; } // starts at 53A30 .text:0000000000053B7C ; __unwind { .text:0000000000053B7C xor eax, eax .text:0000000000053B7E retn
跟kiwi中一样,我们需要将rsp设置为rop链的地址,rcx设置为ret,也就是将[rdx+0A0h]设置为rop链的地址,[rdx+0A8h]设置为ret
此时,rdx=[fake_io_file+0xe0]=bss-0x28,所以我们在bss-0x28+0xa0=bss+0x78处写rop链的地址,bss-0x28+0xa8=bss+0x80处写ret就能完成栈迁移从而进行orw(无沙箱就是分别填system和ret,但是要多一个bss-0x28处填/bin/sh地址)