Shellcode 与汇编的关系:位置无关代码与空字节规避

关系概览

汇编源码 (.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 0xor ecx, ecx; push ecx
mov eax, 0xor 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 的开发、测试和分析都在受控的授权测试环境中进行。理解底层原理有助于防御侧识别攻击特征和设计更有效的检测规则。