恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Shellcode与栈的底层交互:从返回地址覆盖到可执行栈实操

  • 首页
  • 资讯中心
  • /
  • Shellcode与栈的底层交互:从返回地址覆盖到可执行栈实操

相关资讯

realloc UAF漏洞原理与CTF利用实战 2026/10/12 14:04:47
英语电话沟通不再慌:从听力难点到实战话术全拆解 2026/10/12 14:04:47
uC/OS中锁调度器与关中断的本质区别及选型指南 2026/10/12 14:04:47

最新资讯

FastReport批量打印实战:从循环直打到队列调度的稳定方案
C++实战:从CSV解析到疫情数据统计管线
TDSC 2025区块链论文速读指南:从三看三问到笔记沉淀
【Spring AI Alibaba】百炼工作流 + Multi-Agent 搭建电商智能导购:把 Base URL 改到 TaoToken 的完整配置
H3C主网络工程实施方案模板:从调研、IP规划到割接回退的全流程实战指南
Apex英雄更新后闪退?驱动、反作弊与系统设置的完整排查指南

今日推荐

Debian新手入门:从部署到日常操作的完整指南
MongoDB复制集扩缩容实战:从rs.add到选主事故复盘
条形码目标检测数据集实战:从YOLOv8训练到部署

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Shellcode与栈的底层交互:从返回地址覆盖到可执行栈实操

发布时间:2026/10/12 14:04:47
Shellcode与栈的底层交互:从返回地址覆盖到可执行栈实操 1. 从一个反直觉的栈行为说起第一次接触 Shellcode 和栈的配合是在分析一段极简的本地代码执行片段时。当时我盯着调试器里栈指针的变化发现一个很有意思的现象函数返回地址被覆盖之后程序并没有立刻崩溃而是跳到了一个完全由我控制的地址上继续执行。那一刻我才真正意识到栈不只是一块存放局部变量的内存区域它同时还是控制流的关键枢纽。这篇文章想聊的就是这个主题Shellcode 和栈之间到底是怎么互相作用的。我会从栈的内存布局讲起拆解函数调用约定、返回地址覆盖、栈对齐、可执行栈与不可执行栈的区别再一步步给出可复现的实操过程。适合有一定汇编和 C 语言基础、想真正搞懂底层执行机制的人。如果你只是听说过“栈溢出”这个词但从没亲手调试过那这篇内容应该能帮你把很多模糊的概念落到实处。需要先说明一点下面所有实验都在本地隔离环境里完成用的是自己写的测试程序目的是理解程序运行原理和防御思路。所有代码仅用于学习底层机制不要拿去做任何未经授权的操作。2. 栈的内存布局与函数调用约定2.1 栈到底长什么样栈在内存里是一段连续区域但它有个很反直觉的特点它从高地址向低地址增长。也就是说每压入一个元素栈指针在 x86-64 上是 RSP会减小。很多人第一次看到这个会懵因为平时数组是从低到高排的栈却反着来。一个典型的栈帧包含这些内容从高地址到低地址依次是调用者的栈帧底部、函数参数如果通过栈传递、返回地址、保存的旧栈基址RBP、局部变量、临时数据。在 x86-64 的 System V 调用约定下前六个整型参数走寄存器RDI、RSI、RDX、RCX、R8、R9所以栈上更多看到的是返回地址和局部变量。我用一段最简单的 C 代码来演示void func_b() { char buf[16]; buf[0] A; } void func_a() { func_b(); } int main() { func_a(); return 0; }编译时关掉优化和栈保护gcc -O0 -fno-stack-protector -z execstack -o demo demo.c这里三个编译选项各有用途。-O0保证编译器不优化掉栈帧-fno-stack-protector关掉栈金丝雀-z execstack让栈可执行。这三个选项在真实生产环境里通常都是反过来的这里只是为了观察机制。2.2 返回地址为什么是关键函数调用时call指令会把下一条指令的地址压入栈这个地址就是返回地址。函数执行完ret指令会从栈顶弹出这个地址并跳过去。问题就出在这里如果局部变量数组越界写入恰好覆盖到了返回地址那么ret就会跳到被覆盖后的地址。这就是 Shellcode 和栈产生关联的根本原因。Shellcode 本质上是一段位置无关的机器码它不依赖固定的加载地址可以被放到任何可执行的内存区域。当返回地址被覆盖成指向 Shellcode 的地址时程序就会执行这段代码。我实测下来覆盖返回地址的偏移量计算是最容易出错的一步。在 64 位系统上缓冲区到返回地址之间通常还隔着保存的 RBP8 字节所以偏移量往往是缓冲区大小加上 8。但这个值会随编译器版本、优化级别变化必须用调试器实际确认。2.3 栈对齐这个容易被忽略的细节x86-64 的 ABI 要求调用函数时栈指针 16 字节对齐。这个规则在正常写代码时不用管编译器会处理。但当你手动构造 Shellcode 并跳转过去时如果栈没对齐某些指令尤其是涉及 SSE 的 movaps会直接触发段错误。我踩过这个坑Shellcode 逻辑完全正确但一执行就崩查了半天才发现是跳转时 RSP 的末位不是 0。解决办法是在 Shellcode 开头加一条and rsp, -16或者手动push一个占位值调整对齐。这个细节在大多数入门教程里不会提但实际调试时非常关键。3. Shellcode 的构造原理与位置无关性3.1 为什么 Shellcode 必须位置无关Shellcode 的核心约束是位置无关。因为你不知道它会被放到内存的哪个地址所以代码里不能出现绝对地址引用。所有对字符串、数据的访问都得用相对寻址。在 x86-64 上实现位置无关有个经典技巧用call指令把当前指令地址压栈然后从栈上弹出到某个寄存器再基于这个寄存器做相对偏移访问数据。比如jmp short get_addr back: pop rsi ; rsi 现在指向字符串 ; 后续使用 rsi 作为字符串基址 ... get_addr: call back db /bin/sh, 0这段逻辑的意思是先跳到get_addr执行call back把back的地址压栈然后pop rsi把这个地址取出来。这样rsi就指向了紧跟其后的字符串数据。整个过程不依赖任何绝对地址。3.2 从汇编到机器码的转换写好的汇编需要转成机器码才能作为 Shellcode 使用。流程一般是汇编器生成目标文件链接器处理再用 objdump 提取机器码。但更直接的办法是用 nasm 生成纯二进制nasm -f bin shellcode.asm -o shellcode.bin xxd -p shellcode.bin | tr -d \n这样得到的十六进制字符串就是可以直接嵌入 C 程序的 Shellcode。我习惯用xxd而不是objdump因为xxd输出更干净不用手动去掉地址和助记符列。需要注意的是Shellcode 里不能有 null 字节0x00因为很多注入场景下 null 会被截断。如果汇编里出现了 null通常是因为用了mov rax, 0这类指令可以改成xor rax, rax。这个技巧叫 null-free 编码是 Shellcode 编写的基本功。3.3 一个最小可用的测试 Shellcode为了演示栈和 Shellcode 的配合我构造一段最简单的 Shellcode功能就是往一个寄存器写个值然后正常退出避免执行后失控; exit_shellcode.asm BITS 64 xor rdi, rdi ; 退出码 0 mov rax, 60 ; sys_exit syscall汇编后得到机器码长度很短。把它嵌入测试程序通过覆盖返回地址跳转过去程序会干净退出而不是崩溃。这个测试的价值在于你能明确看到控制流被劫持了而且劫持后的行为完全由你控制。提示测试 Shellcode 时一定要选无害功能比如退出、写日志、返回固定值。不要执行任何有副作用的操作这是本地实验的基本纪律。4. 完整实操从覆盖返回到执行 Shellcode4.1 实验环境搭建我用的环境是 64 位 Linux工具链包括 gcc、gdb、nasm、python3。先准备一个有栈溢出漏洞的测试程序// vuln.c #include stdio.h #include string.h void vulnerable(char *input) { char buf[64]; strcpy(buf, input); } int main(int argc, char **argv) { if (argc 1) { vulnerable(argv[1]); } return 0; }编译gcc -O0 -fno-stack-protector -z execstack -no-pie -o vuln vuln.c-no-pie是为了让程序加载地址固定方便计算 Shellcode 的绝对地址。真实环境下 PIE 会开启地址随机化会让这种攻击难得多但作为学习固定地址更容易观察。4.2 确定偏移量用 gdb 加载程序在vulnerable函数返回前下断点查看 RSP 和缓冲区地址gdb ./vuln (gdb) break vulnerable (gdb) run AAAA (gdb) disas vulnerable找到strcpy调用之后的栈布局计算buf起始地址到返回地址的距离。我实测这个例子里是 64 字节缓冲区加上 8 字节保存的 RBP共 72 字节。也就是说输入的第 73 到 80 字节会覆盖返回地址。可以用 python 生成测试输入验证python3 -c print(A*72 B*8) | xargs ./vuln如果 gdb 里看到返回地址变成 0x4242424242424242说明偏移量算对了。4.3 定位 Shellcode 地址并构造 payload接下来要确定 Shellcode 在栈上的地址。因为关了 ASLR 和 PIE栈地址在每次运行时相对固定但仍有细微变化。稳妥的做法是在 gdb 里看buf的实际地址(gdb) break vulnerable (gdb) run AAAA (gdb) print buf假设得到地址 0x7fffffffe000那么 Shellcode 放在缓冲区开头返回地址就填这个值。payload 结构是Shellcode 填充到 72 字节 8 字节返回地址。用 python 构造import struct shellcode b\x48\x31\xff\x48\xc7\xc0\x3c\x00\x00\x00\x0f\x05 padding b\x90 * (72 - len(shellcode)) ret_addr struct.pack(Q, 0x7fffffffe000) payload shellcode padding ret_addr print(payload)这里的\x90是 NOP 指令作为填充。实际调试时地址可能需要微调因为环境变量会影响栈布局。我通常会在 Shellcode 前面加一长串 NOP叫 NOP sled这样返回地址只要落在 NOP 区域里就能滑到 Shellcode容错率高很多。4.4 执行与观察把 payload 传给程序./vuln $(python3 payload.py)如果一切正确程序会执行 Shellcode 然后退出而不是报段错误。在 gdb 里单步跟踪能看到ret之后 RIP 跳到了栈上的 Shellcode 地址然后逐条执行。这个过程中最容易出问题的地方有三个偏移量算错、返回地址不对、栈对齐问题。我建议每改一次 payload 都在 gdb 里确认 RIP 的落点不要盲目试。5. 常见问题与排查速查5.1 段错误排查思路段错误是这类实验最常见的现象但原因可能完全不同。我整理了一个排查顺序现象可能原因排查方法ret 后立即崩溃返回地址错误gdb 看 RIP 落点跳到正确地址但崩溃栈不可执行检查编译选项 -z execstackShellcode 执行一半崩溃栈未对齐检查 RSP 末位是否为 0地址每次不同ASLR 开启关闭 ASLR 或改用相对跳转输入被截断存在 null 字节检查 Shellcode 是否 null-free5.2 栈不可执行带来的变化现代系统默认栈不可执行NX 位。这种情况下即使返回地址被覆盖跳到栈上也会触发保护异常。这也是为什么真实场景下单纯覆盖返回地址跳栈已经很难奏效需要配合其他技术。但从学习角度理解 NX 之前必须先理解可执行栈下的行为否则你不知道自己在防什么。我实测下来-z execstack和默认编译的差异非常直观前者能跑通后者直接 SIGSEGV。这个对比实验值得亲手做一遍。5.3 调试时的实用技巧在 gdb 里查看栈内容用x/40gx $rsp查看寄存器用info registers。如果 Shellcode 是十六进制字符串可以用restore命令或者直接在程序里用数组加载。还有一个技巧在 Shellcode 开头插入int30xCC这样执行到那里会触发断点能确认控制流确实到达了 Shellcode。确认之后再换成真正的指令。这个办法比反复猜地址高效得多。注意所有实验务必在本地虚拟机或隔离环境进行不要在任何共享或生产系统上测试。理解机制是为了写出更安全的代码不是为了攻击。6. 从攻击视角回到防御视角搞懂 Shellcode 和栈的配合之后回头看防御手段会清晰很多。栈金丝雀stack canary在返回地址前放一个随机值函数返回前检查它有没有被改一旦溢出就会在ret之前被发现。ASLR 让栈地址随机化返回地址就算被覆盖也不知道该填什么。NX 让栈不可执行跳过去也没用。PIE 让代码段地址也随机化进一步增加难度。这些机制不是孤立的而是层层叠加。我个人的体会是只理解攻击不理解防御等于只学了一半。真正有价值的认知是知道每一层防护拦住了什么、绕过的成本有多高这样才能在写代码时做出正确的安全决策。比如你在写 C 代码时用strncpy替代strcpy、开启编译器默认的栈保护、不关闭 ASLR这些看似普通的习惯实际上就是在应用上面这些机制。底层原理懂了这些习惯就不再是死记硬背的规则而是有明确理由的工程选择。最后分享一个我常用的验证方法写完任何涉及缓冲区操作的代码先用-fstack-protector-all编译跑一遍测试用例再用checksec看二进制的防护状态。这个流程花不了几分钟但能提前发现大部分低级问题。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号