写在前面在之前所有的实战中我们都有一个前提——手里有题目对应的二进制文件ELF可以在 IDA 里看伪代码用 ROPgadget 找 Gadget。但如果题目只给了一个远程 IP 和端口二进制文件完全未知甚至运行在远程服务器上我们该怎么办这就是 PWN 的终极盲打技术BROPBlind Return Oriented Programming。今天我们将戴上夜视仪在完全没有源码的黑暗中仅靠程序的“崩溃”与“存活”反馈一步步摸出 ROP 链并拿 Shell。 目录极致绝境没有二进制文件的盲打核心思想基于侧信道的“崩溃探测法”寻路指南针寻找 Stop Gadget 与 BROP Gadget破局关键定位puts与泄露 libc 地址终极构造Dump 内存与完整 ROP 链Week6 总结与进阶展望1. 极致绝境没有二进制文件的盲打BROP 攻击场景通常出现在远程服务如 nginx、Apache 或某个默默运行的守护进程存在栈溢出但我们拿不到固件。我们唯一能做的就是向远程发送数据并观察 TCP 连接是否断开程序崩溃连接突然断开EOF。程序存活连接保持甚至有正常回显。因为程序通常是使用fork创建子进程处理连接的所以即使子进程崩溃父进程依然存活Canary 等保护机制在这里反而不是阻碍反而因为进程不死让我们可以无限次试错。2. 核心思想基于侧信道的“崩溃探测法”BROP 的核心逻辑是“试错”。假设我们向buf填入大量字符导致程序返回到一个随机的非法地址程序崩溃。如果我们把返回地址换成一个合法的代码地址程序可能不会立刻崩溃而是继续执行该地址处的代码。通过遍历地址空间发送[Padding] [探测地址]观察连接是否断开我们就能在黑暗中找到有用的指令地址。3. 寻路指南针寻找 Stop Gadget 与 BROP Gadget在盲打中我们不能随便跳转因为跳转过去的代码如果包含ret可能会破坏我们后续的栈结构导致崩溃。我们需要找特定的 Gadget。3.1 寻找 Stop Gadget停止小工具我们需要找一个“坑位”让程序跳过去后既不崩溃也不返回而是卡住或循环。这通常是sleep函数或while(1)循环。探测逻辑发送[Padding] [探测地址] [大量垃圾数据]。如果程序没有崩溃断开连接保持说明探测地址就是一个 Stop Gadget。它把后续的垃圾数据吞掉了。这个 Stop Gadget 是我们后续探测的“保命符”跳到它就不会崩。3.2 寻找 BROP Gadget万能跳板在 64 位中最常用的 Gadget 是__libc_csu_init尾部的 6 个pop加 1 个retpop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret探测逻辑发送[Padding] [探测地址] [6个有效地址] [Stop Gadget]。如果探测地址是 BROP Gadget它会消耗掉栈上的 6 个地址然后执行ret跳到我们的 Stop Gadget程序存活如果不是栈结构错位程序大概率崩溃。通过这种方式我们在茫茫内存中盲猜出这个 7 连发 Gadget 的地址。4. 破局关键定位puts与泄露 libc 地址有了 BROP Gadget我们其实就拥有了pop r15; ret取 BROP Gadget 地址 7 偏移即可得到pop r15; ret但通常我们直接用它推导出pop rdi; ret即 BROP Gadget 地址 9。现在我们有了pop rdi; ret可以控制第一个参数了。接下来要找输出函数。4.1 盲找 PLT 表PLT 表的每一项通常是 16 字节结构固定。我们可以通过遍历某个可能的地址范围尝试调用它。4.2 定位putsplt我们怎么知道遍历到的是puts还是printf我们可以把rdi设置为某个 GOT 表项的地址比如 BROP Gadget 所在页的某个固定偏移那里大概率有.dynamic段的魔数\x7fELF。如果我们调用某个 PLT 项后远程返回了\x7fELF这样的字符串说明我们找到了puts或write假设性盲打脚本推演from pwn import * # 假设我们已经找到 offset72, brop_gadget0x4007ba, stop_gadget0x40055c # pop_rdi brop_gadget 9 # 尝试探测 PLT 表项 # 假设从 0x400500 开始探测 for addr in range(0x400500, 0x400600, 0x10): try: p remote(127.0.0.1, 8888) payload bA * 72 payload p64(pop_rdi) # 弹出参数给 rdi payload p64(0x400000) # ELF 文件头地址必定有 \x7fELF payload p64(addr) # 探测的 PLT 地址 payload p64(stop_gadget) # 保命 p.sendline(payload) response p.recv(timeout1) # 如果收到 \x7fELF说明找到了 puts if b\x7fELF in response: log.success(fFound putsplt at: {hex(addr)}) puts_plt addr p.close() break p.close() except: p.close() continue模拟终端输出[] Opening connection to 127.0.0.1 on port 8888: Done [] Found putsplt at: 0x4005355. 终极构造Dump 内存与完整 ROP 链找到putsplt和pop rdi; ret后我们就从“盲打”回到了“明打”5.1 泄露 libc 地址我们可以让puts打印putsgot里的内容从而泄露 libc 基址。但盲打中我们不知道putsgot在哪。破局思路putsgot通常在putsplt附近。我们可以利用putsplt里的第一条jmp [got_addr]指令反推 GOT 地址或者直接盲扫 BSS 段后面的 GOT 表把内容打印出来直到发现像 libc 地址以0x7f开头的数据。5.2 Dump 二进制文件既然有puts我们可以把整个程序的.text段从头到尾打印出来保存到本地文件反编译成 ELF。这样我们就“偷”回了二进制文件# 假设已经拿到了 puts_plt 和 pop_rdi dump_addr 0x400000 dump_data b while dump_addr 0x401000: payload bA * 72 payload p64(pop_rdi) payload p64(dump_addr) payload p64(puts_plt) payload p64(stop_gadget) # 保持存活 p.sendline(payload) # puts 遇到 \x00 会截断需要特殊处理补齐 leak p.recv(timeout1) dump_data leak dump_addr len(leak) # 粗略推进拿到完整的 ELF 文件后接下来的操作就是常规的 ret2libc找到system和/bin/sh构建最终 Payload 拿 Shell。6. Week6 总结与进阶展望至此Week6 的进阶栈溢出之旅圆满结束本周我们从栈迁移突破空间限制开始掌握了SROP突破寄存器限制学习了ret2dl_resolve突破无 libc 泄露限制最后在BROP中体验了在完全没有二进制文件的黑暗中重建光明的极致盲打。这些技术不再是简单的套公式而是深刻理解了操作系统、编译器与底层汇编机制后的“魔法”。栈溢出到这里基本上已经没有更多的新花样了。下周预告 (Week7)栈上的厮杀彻底告一段落。从下周起我们将正式踏入现代 PWN 的主战场、也是最容易让人劝退的领域——堆。我们将从glibc的内存管理机制讲起揭开malloc和free的底层面纱学习Use-After-Free (UAF)、Double Free以及最经典的Fastbin Attack。堆的世界更加复杂但也更加精彩如果 Week6 的系列文章对你的学习有帮助请点赞收藏支持你的鼓励是我持续更新的最大动力。我们 Week7 见