ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

CTF pwn静态链接栈溢出:用mprotect和ROP绕过NX执行shellcode

CTF pwn静态链接栈溢出:用mprotect和ROP绕过NX执行shellcode CTFshow 的 pwn 练习做到 049 这一题时我第一眼看到file输出的statically linked心里先冒出来的是“栈溢出加静态编译那不就是 ret2syscall 梭哈一把嘛”。等真正开始调试才发现这道题虽然没开 PIE 也没开 canary偏偏 NX 是开着的原本想在栈上直接塞 shellcode 的路直接被堵死。于是解法收敛到一种非常经典的套路用 ROP 调用mprotect把一块地址固定的内存改成可读可写可执行然后把 shellcode 放进去跳转执行。老读者应该能看出来这就是很多 CTF pwn 入门题的核心分水岭——不会 mprotect 的人只能在栈上反复撞墙会的人一眼就能把整条利用链拉通。这篇就用 CTFshow 的 pwn049 当例子把静态编译程序里的栈溢出、地址定位、mprotect 页对齐、二次输入 shellcode 这些点全部过一遍。我把实际调试时遇到的坑也放在后面就算你没做过这道题看完这套思路以后再遇到“静态链接 NX 开启 栈溢出”的题至少知道第一步该去找什么、第二步该在 gdb 里检查什么。1. 环境摸查先别急着写 exp把题目底牌看干净1.1 三个常用命令看清一个 ELF拿到题目文件之后我非常不建议直接拖进 IDA 看反编译。先花十秒钟在终端里跑三个命令file pwn049 checksec --file./pwn049 readelf -h pwn049file会直接告诉你这个 ELF 是静态链接还是动态链接。我本地这道题输出里明确写着statically linked看到这一行我心里就清楚后面不需要考虑 libc 版本、不需要做地址泄露也几乎不需要担心远程环境的依赖问题。checksec的输出则会列出一堆保护机制我整理成表更直观检查项结果对解题的影响Archamd64参数传递顺序固定为 rdi、rsi、rdx、rcxStack CanaryNo canary found覆盖返回地址时不会触发栈保护NXNX enabled栈上不能直接执行 shellcode必须先改内存权限PIENo PIE代码段、数据段地址固定可以直接用符号地址静态链接statically linked所有 glibc 目标文件都被链进 ELF可用 gadget 和函数极多行文说到这我已经能看到弹幕里有人问既然静态链接了 glibc为什么不能直接找system(/bin/sh)这个想法没错但现实很骨感。静态链接程序里是否包含system函数完全取决于编译时有没有代码引用它如果主程序从头到尾没调用过链接器根本不会把这个函数塞进来。就算某些题目里碰巧有system往往也找不到一个现成的/bin/sh字符串让 ROP 链直接指向。所以 pwn049 这类题的标准答案几乎都是 mprotect 路线而不是无脑 ret2libc。1.2 静态编译对利用者来说意味着什么静态编译在 CTF 里看起来很“笨重”文件动不动好几 MB函数列表密密麻麻但对做 pwn 的人来说它反而是一个相当友好的环境。首先是地址不随机非 PIE 情况下所有函数地址、变量地址、bss 段位置全部固定你可以直接把read、mprotect的地址写进 exp不需要先打一次泄露再计算基址。其次是 gadget 数量多到离谱静态编译会把大量辅助函数链进来系统中几乎每个角落都藏着pop rdi; ret、pop rsi; ret之类的短指令用 ROPgadget 搜起来非常爽。动态链接程序的常规思路是“栈溢出以后 ret2plt 泄露 libc再二次利用劫持控制流”但静态链接没有 plt 这一说也不需要加载外部 so。它的核心矛盾反而集中在“NX 开启之后我该把 shellcode 放哪执行”。这时候就会出现一个很有意思的选择动态链接下你第一反应可能是在内存中找一块带有执行权限的固定地址或者干脆用 one_gadget静态链接下最顺手的方法就是自己造一块可执行内存。1.3 保护机制结果直接影响攻击路径把 checksec 的结果连起来读一遍解题路径其实已经自己浮出来了。Canary 没开意味着栈溢出后可以放心覆盖返回地址PIE 没开意味着覆盖成什么地址都是稳定的NX 开启意味着直接把 shellcode 扔栈上会触发非法指令静态链接意味着我不需要做 libc 泄露。把这些约束组装起来问题就变成一个非常明确的任务找到一段固定地址、可写的内存把 shellcode 写进去再通过 mprotect 把这段内存变成可执行最后让 RIP 跳过去。很多新手卡在 pwn049不是不会栈溢出而是没有建立“地址固定 不可执行 mprotect”的反射。看到 NX 就只知道“不能执行 shellcode”却忘了 mprotect 本身就是 syscall它的作用就是修改一个进程地址空间里某些页的权限我们可以主动把一个原本不可执行但可写的 bss 段改成可执行。这个思路一旦打通后面的 exp 只是体力活。2. 逆向主程序确认漏洞点与偏移量2.1 从 main 函数反编译结果定位溢出IDA 打开这种静态编译程序函数列表会非常长但你完全不用管里面那些 glibc 内部函数。先看_start的交叉引用或者直接用字符串窗口找可疑提示回到 main 函数。pwn049 的 main 函数逻辑非常简单典型代码如下int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { char buf[32]; read(0, buf, 0x100uLL); return 0; }这里就是经典的栈溢出点局部变量buf只有 32 字节也就是 0x20但read可以往这块内存里读入 0x100 字节。多出来的 0xe0 字节会一路往下覆盖到 saved rbp、返回地址甚至把 main 函数栈帧之后的更大区域全部变成我们可控的数据。第一眼看过去这个洞甚至不需要二次分析直接就能确定可以控制 RIP。我也要提醒一下实际题目编译出来 buf 大小和 read 长度可能跟我这里不完全一样但套路一致只要局部缓冲区小于 read 长度溢出就成立。你要做的只是在反编译窗口里确认一下缓冲区大小、读入长度再看一眼有没有 canary 检测。如果 checksec 已经显示 No canary那这里就是纯纯的送分点。2.2 用 cyclic 精确计算返回地址偏移偏移计算是整个 exp 里最不能拍脑袋的部分。我早期做题时喜欢用 IDA 里的“buf 大小 8”来猜比如 buf 是 0x20就猜覆盖返回地址需要 0x20 8 40 字节。这个方法在本题是对的但遇到编译器自动加 padding、或者栈变量排列比较奇怪时就会翻车。更稳的方法是用 pwntools 生成一段循环字符串让程序崩溃后根据 RIP 里的字符反推偏移。实际操作是cyclic 200把输出交给./pwn049程序崩溃后用 gdb 查看 RIP或者配合 core dumpgdb ./pwn049 core info registers rip cyclic -l 0x616161...在我本地环境中最终测出的偏移是 40也就是 0x28。这跟反编译结果完全对上0x20 字节的 buf 加上 8 字节的 saved rbp返回地址从第 40 字节开始被覆盖。知道了这个数字后面构造 payload 时只需要在前面填充 40 个垃圾字节然后一段接一段地摆 ROP 指令即可。2.3 为什么 read 是特别适合构造 ROP 的入口这里多聊一句read函数的特殊性。它不是 stdio 层的fgets或scanf不会因为遇到\x00就停止也不会因为换行符产生额外处理。read是真正的系统调用它的语义就是“从文件描述符里读 N 个字节到内存”所以 payload 中间夹带大量 0x00 是完全没有问题的。这一点对于构造 64 位 ROP 链极其重要因为p64()出来的地址本来就有一堆高位 0x00如果用gets之类的函数这些 0x00 会被当成字符串结尾截断掉根本打不出去。另一个好处是 read 本身就是一个符号函数在后续利用里可以直接调用它来做二次输入。我们控制返回地址后可以让程序先执行一段read(0, bss_addr, 0x100)把真正的 shellcode 送到 bss 段再执行 mprotect 改权限最后跳过去。这个二次注入流程能不能顺利跑通就看你对 read 的参数传递和栈布局是否理解透彻所以我把下一章的重点全放在这。3. mprotect 权限修改思路详解3.1 为什么静态链接场景下首选 mprotect先简单比较一下几种攻击路径免得有人觉得 mprotect 是绕远路攻击路径可行性卡点ret2text不行程序里没有现成的后门函数ret2libc不稳定静态程序是否包含 system 取决于链接过程不能保证在栈上直接执行 shellcode不行NX 开启栈页不可执行ROP 直接拼 execve syscall可行但繁琐需要精确构造 rax/rdi/rsi/rdx且要确认是否有 seccompread mprotect shellcode推荐思路清晰只需 3 个参数的 ROPmprotect 的妙处在于它把“没有可写可执行内存”这个问题一键解决。栈不可执行没关系我们改栈的权限即可但栈地址在 ASLR 下是随机的不方便写死所以更合理的选择是改 bss 段。bss 段地址在非 PIE 程序里是固定的而且天然可写只是默认没有执行权限。我们只要调用 mprotect 把 bss 所在页加上 PROT_EXEC
返回列表