关系概览
汇编源码 (.asm)
↓ 汇编器 (nasm / masm)
机器码(二进制字节)
↓ 满足额外约束
Shellcode
汇编代码是人可读的指令文本;机器码是 CPU 直接执行的二进制;Shellcode 是专门设计的机器码,额外满足注入执行的要求。
为什么叫 Shellcode
最初漏洞利用(如栈溢出)的攻击载荷:
[Shellcode][Padding][Return Address]
覆盖返回地址后程序跳转到攻击者注入的代码,执行 /bin/sh 获得 Shell,因此得名。现代 Shellcode 不一定启动 Shell,常见用途包括:下载木马、注入 DLL、建立反向连接、调用系统 API。
位置无关代码(PIC)
普通可执行文件有固定加载地址;注入的 Shellcode 则可能落在任意地址,因此不能使用绝对地址引用字符串或函数。
x86 经典写法(call/pop 技巧):
call get_rip
get_rip:
pop ebx ; ebx = 当前 IP,可作为基址计算偏移
; 用 [ebx + offset] 访问数据而非绝对地址
x64 更简洁,RIP 相对寻址天然支持 PIC:
lea rax, [rip + data_offset]
空字节(Null Byte)问题
许多漏洞场景通过字符串复制触发(如 strcpy),遇到 \x00 会截断。因此 Shellcode 必须避免产生空字节(0x00)。
反例:
mov eax, 1 ; b8 01 00 00 00 → 含 \x00
ret ; c3
机器码 b8 01 00 00 00 含三个 \x00,不适合作为 Shellcode。
改写后:
xor eax, eax ; 31 c0 → eax = 0,无 \x00
inc eax ; 40 → eax = 1
ret ; c3
机器码 31 c0 40 c3,无空字节。
常用规避技巧:
| 原写法 | 替换写法 |
|---|---|
mov eax, N(N 小于 256) | xor eax, eax; mov al, N |
push 0 | xor ecx, ecx; push ecx |
mov eax, 0 | xor eax, eax |
字符串末尾 \x00 | 运行时 xor [rsp+N], al 写入 |
NX / DEP:现代系统的阻碍
现代操作系统将内存页分为可写(W)和可执行(X),默认两者不能同时开启:
代码段:R + X(可读可执行)
数据段:R + W(可读可写)
栈: R + W(默认无执行权限)
因此攻击者把 Shellcode 注入栈或堆后,直接跳转执行会触发 NX/DEP 保护,产生访问违例。
绕过方式(了解原理即可):
- ROP(Return-Oriented Programming):链式复用代码段中已有的 gadget,不需要注入新代码。
- JIT Spraying:利用 JIT 编译器生成可执行内存,注入伪装成合法代码的载荷。
- mmap/VirtualAlloc + mprotect:合法申请可执行内存后拷贝并跳转(多见于合法场景如虚拟机)。
汇编动态执行机器码的原理
以 C 伪码说明(仅描述原理):
// 1. 申请可执行内存
void *mem = mmap(NULL, len, PROT_READ|PROT_WRITE|PROT_EXEC, ...);
// 2. 写入机器码
memcpy(mem, shellcode_bytes, len);
// 3. 跳转执行
((void(*)())mem)();
这等价于汇编:
mov rax, <mem_addr>
jmp rax ; CPU 将 mem_addr 处字节视为指令序列执行
CPU 不区分”正常代码”和”Shellcode”,只要内存页有执行权限,任何字节序列都可以执行。
小结
| 概念 | 说明 |
|---|---|
| 汇编 | 人可读的助记符指令文本 |
| 机器码 | 汇编编译后的二进制 |
| Shellcode | 满足 PIC + 无空字节 + 紧凑的机器码 |
| NX/DEP | 阻止数据页执行的硬件/OS 保护 |
| PIC | 代码不依赖绝对地址,可在任意内存位置运行 |
实际安全研究中,Shellcode 的开发、测试和分析都在受控的授权测试环境中进行。理解底层原理有助于防御侧识别攻击特征和设计更有效的检测规则。
