为什么不呢 ¯\_(ツ)_/¯
这是我最大的项目之一,我在其中学到了很多关于 LLVM 内部机制、二进制格式、汇编和混淆技术的知识。
我相信通过构建来学习是最好的学习方式,因此我构建了这个项目来更深入地了解这些主题。
我写了 4 篇博客来解释 Zyrox 背后的概念:
这些部分比本 readme 更深入,如果你对这个主题感兴趣,绝对值得一读。
这适合想要 快速测试 Zyrox,或学习如何将其集成到 cmake 项目中的人。
按照 Zyrox Template 仓库中的步骤操作。
安装 llvm:
sudo apt update
sudo apt install llvm-18 llvm-18-dev clang-18
克隆并编译 zyrox:
git clone --recurse-submodules https://github.com/PeterHackz/zyrox.git
cd zyrox
cmake -S . -B build -DCMAKE_C_COMPILER=/usr/bin/clang -DCMAKE_CXX_COMPILER=/usr/bin/clang++
cmake --build build --parallel 4
确保你已安装 python3 和 pip。
# Create a virtual environment
python3 -m venv .venv
# Activate the env
source .venv/bin/activate
pip install -r requirements.txt
pip install -r requirements.txt
clang -O0 -flto=full -c main.c -o out/main.o
clang -flto=full -fuse-ld=lld -Wl,--load-pass-plugin=./build/libzyrox.so out/main.o -o out/main
混淆完成后,运行 PyPlugin.py 来加密跳转表:
# if you installed dependencies in a virtual environment, activate it first:
source .venv/bin/activate
# then run with:
python PyPlugin.py --in=<input_file> [--out=<output_file>] [--tables=<zyrox_tables_file>] [--android]
请查看 Zyrox Template 仓库,获取 CMake 集成示例。
我明白这是一个复杂的话题,这个项目主要是为了教育目的,同时也服务于 BSD Brawl。 如果你有任何问题,或者只是想聊天,欢迎联系我:
@s.b[email protected] 或 [email protected]任何形式的帮助,无论是通过 pull request 还是 issue,都非常感谢!
ZyroxPlugin.cpp 注册 pass,然后链接 siphash(稍后详细介绍)并调用 StringEncryption 来加密字符串。
我们提前加密字符串的原因是,这样解密逻辑之后也会被混淆。
然后它调用 ModuleUtils::ExpandCustomAnnotations
和 QuickConfig::RegisterPasses 来解析所有 __attribute__((annotate("..."))) 表达式,并运行
QuickJs 配置(位于 ZyroxConfig.js)。
每个函数都通过调用位于 ZyroxCore.cpp 中的 Zyrox::RunOnFunction 进行混淆,
未来会提供更多相关文档。
switch 会创建跳转表,而 PHI 节点处理起来很烦人,因此我们分别使用 FunctionUtils 和 BasicBlockUtils
将其扁平化(转换为 if 语句)和降级。
哦天哪,我该从哪里开始呢
所有 js-plugin 参数都在 index.d.ts 中,因此本文档中不再讨论。
关于注解的文档,点击这里
此 pass 将一个基本块拆分并打乱为多个更小的基本块。假设我们有以下代码:
int __test_fn(int x)
{
if (x == 2) {
printf("x is 2\n");
} else {
printf("x is not 2!, x is: %d\n", x);
}
return x + 4 * x - 2 / 4;
}
它会被编译成:
define internal i32 @__test_fn(i32 noundef %0) #0 !zyrox !8 !obfuscated !11 {
%2 = alloca i32, align 4
store i32 %0, ptr %2, align 4
%3 = load i32, ptr %2, align 4
%4 = icmp eq i32 %3, 2
br i1 %4, label %5, label %7
5: ; preds = %1
%6 = call i32 (ptr, ...) @printf(ptr noundef @.str.1)
br label %10
7: ; preds = %1
%8 = load i32, ptr %2, align 4
%9 = call i32 (ptr, ...) @printf(ptr noundef @.str.2, i32 noundef %8)
br label %10
10: ; preds = %7, %5
%11 = load i32, ptr %2, align 4
%12 = load i32, ptr %2, align 4
%13 = mul nsw i32 4, %12
%14 = add nsw i32 %11, %13
%15 = sub nsw i32 %14, 0
ret i32 %15
}
当使用 Basic Block Splitter 并使用以下配置时:
z.RegisterPass(ObfuscationType.BasicBlockSplitter, {
PassIterations: 1,
"BasicBlockSplitter.SplitBlockChance": 100,
"BasicBlockSplitter.SplitBlockMinSize": 2,
"BasicBlockSplitter.SplitBlockMaxSize": 5,
});
它会变成:
define internal i32 @__test_fn(i32 noundef %0) #0 !zyrox !8 !obfuscated !11 {
%2 = alloca i32, align 4
store i32 %0, ptr %2, align 4
%3 = load i32, ptr %2, align 4
%4 = icmp eq i32 %3, 2
br i1 %4, label %5, label %14
5: ; preds = %1
%6 = call i32 (ptr, ...) @printf(ptr noundef @.str.1)
br label %7
7: ; preds = %14, %5
%8 = load i32, ptr %2, align 4
%9 = load i32, ptr %2, align 4
%10 = mul nsw i32 4, %9
%11 = add nsw i32 %8, %10
br label %12
12: ; preds = %7
%13 = sub nsw i32 %11, 0
ret i32 %13
14: ; preds = %1
%15 = load i32, ptr %2, align 4
%16 = call i32 (ptr, ...) @printf(ptr noundef @.str.2, i32 noundef %15)
br label %7
}
现在对于这么小的函数来说,差别不会太大,但注意它是如何拆分一个基本块的?这与其他 pass 结合使用时很有帮助,例如 Control Flow Flattening。
哦天哪,这个 pass 的功能是所有 pass 中最多的,哈哈。 我先解释它的工作原理,然后再介绍它的配置。 假设我们有以下代码:
LABEL_A: bool b = x == 2;
IF EQ: goto LABEL_B
goto LABEL_C
LABEL_B do_stuff()
LABEL_C do_other_stuff()
goto LABEL_A
每个基本块(A、B 和 C)都会被分配一个唯一的分发器状态,例如:(简化版)
states = {
1: LABEL_A,
2: LABEL_B,
3: LABEL_C,
};
然后我们注入一个控制所有内容的分发器块,代码变成:
int state = 0;
LABEL_D goto LABEL_CA // dispatcher label jumps to first condition block, label condition A
LABEL_CA if state == 1: goto LABEL_A
// if not 1, go to check if it is label B (fallback)
LABEL_CB if state == 2: goto LABEL_B
LABEL_CC if state == 3: goto LABEL_CC
// unreachable
goto LABEL_D
LABEL_A: bool b = x == 2;
// IF EQ: goto LABEL_B
// goto LABEL_C
state = 2 if b else 3 // update state for the block we want and back to dispatcher
goto LABEL_D
LABEL_B do_stuff()
LABEL_C do_other_stuff()
state = 1
goto LABEL_D
这个方案有一些缺陷,混淆器会修复这些缺陷。如你所见,由于我们只有一个分发器变量,很容易进行反混淆,因为我们知道一个块在设置状态后会跳转到哪里。很容易修复!
z.RegisterPass(ObfuscationType.ControlFlowFlattening, {
PassIterations: 1,
"ControlFlowFlattening.UseFunctionResolverChance": 60,
"ControlFlowFlattening.UseGlobalStateVariablesChance": 60,
"ControlFlowFlattening.UseOpaqueTransformationChance": 40,
"ControlFlowFlattening.UseGlobalVariableOpaquesChance": 80,
"ControlFlowFlattening.UseSipHashedStateChance": 40,
"ControlFlowFlattening.CloneSipHashChance": 80,
});
让我们逐一介绍这些选项:
UseFunctionResolverChance:注入一个函数来检查状态,因此它不再执行 if (state == expected_state),而是执行 if (injected_resolver(state))。示例:
bool __fastcall cff_resolve_state_check_3585(__int64 a1)
{
return a1 == 0x288A6154F8A5E3E2LL;
}
UseGlobalStateVariablesChance:将要比较的状态值保存到全局变量中:
bool __fastcall cff_resolve_state_check_506(__int64 a1)
{
return a1 == qword_1B20D8;
}
UseOpaqueTransformationChance:将检查混淆为某种变换,该变换只会在特定状态下返回真:
bool __fastcall cff_resolve_state_check_7901(__int64 a1)
{
return ((((a1 ^ 0xEA9E45BB6099BC6ELL) + qword_1C64D8) << qword_1A63F0)
| (((a1 ^ 0xEA9E45BB6099BC6ELL)
+ qword_1C64D8) >> qword_1ACE98)) == qword_1B0B80;
}
UseGlobalVariableOpaquesChance:在执行 UseOpaqueTransformationChance 时使用全局变量而不是数字,正如你在上面的示例中注意到的那样。(、、)假设我们有以下代码:
if (x == 2) goto LABEL_A
goto LABEL_B
LABEL_A: do_stuff()
LABEL_B: // ...
它会被转换为:
@global jump_table = {0, &LABEL_A, &LABEL_B};
if (x == 2) goto jump_table[0] + @inline(decrypt(jump_table[1]));
goto jump_table[0] + @inline(decrypt(jump_table[2]));
// ...
使用此 pass 时,插件会输出一个 zyrox_tables.txt 文件,供 PyPlugin.py 使用。
PyPlugin.py 会加密跳转表并修补重定位条目,然后让重定位器指向每个表的 jump_table[0]。重定位器基本上执行以下操作:
target.writePointer(base.add(value)),因此通过将 value 设为 0,我们让重定位器返回基地址并在运行时将其放入跳转表,然后我们将其与 goto 一起用于生成运行时地址。在 arm32 thumb 模式下,此 pass 会在解密后自动添加 | 1。
要使用 PyPlugin.py,只需执行以下操作:
(如果使用 venv,请先激活它)
python3 PyPlugin.py --in <out_obfuscated_file> --android
如果你针对的是 arm64 版本,传入 --android 很重要,因为 x86_64 版本的重定位器签名不同。
你还可以传入 --out(默认使用传给 --in 的同一文件),也可以传入 --tables(默认为 zyrox_tables.txt)。
虽然 indirect branching 看起来很棒,但它也有性能开销,因为它在运行时解密指针。这是一个不影响性能的简单版本,像这样:
if (x == 2) goto LABEL_A
goto LABEL_B
LABEL_A: do_stuff()
LABEL_B: // ...
变成:
@stack jump_table = {&LABEL_B, &LABEL_A}
goto jump_table[!(x == 2)]
LABEL_A: do_stuff()
LABEL_B: // ...
虽然这看起来很简单,而且容易被破解(我同意),但它足以让 IDA 和 Ghidra 失效,而且不影响性能。
也称为 MBA Sub(Mixed Boolean Arithmetic Substitution,混合布尔算术替换),它将简单运算转换为输出相同的复杂运算。它使用一个预定义的集合。 示例:
a ^ b = (~a & b) | (a & ~b)
b * c = (((b | c) * (b & c)) + ((b & ~c) * (c & ~b)))
r = rand(); c = b + r; a = a + c; a = a - r
如果你感兴趣,可以在 Passes/MBASub.cpp 中查看完整列表。
只需查看 index.d.ts。注解解析器使用相同的顺序。
要标记一个函数,只需执行以下操作:
__attribute__((annotate("ibr:1,100"))) void hello_world () {
some_hello ();
}
注解代码:
示例:
在 index.d.ts 中我们看到:
{
"BasicBlockSplitter.SplitBlockMinSize"?: number;
"BasicBlockSplitter.SplitBlockMaxSize"?: number;
"BasicBlockSplitter.SplitBlockChance"?: number;
};
现在重点是,PassIterations 是第一个参数,也是所有 pass 共有的参数,因此它将是注解中的第一个参数。
要用 bbs 注解某个函数,我们这样做:
__attribute__((annotate("bbs:1,15,30,100"))) void hello_world () {
some_hello ();
}
这意味着:对 hello_world 运行 Basic Block Splitter 1 次,最小大小 = 15,最大大小 = 30,概率 = 100。
你还可以组合多个 pass:
__attribute__((annotate("bbs:1,15,30,100 ibr:1,100 sibr:1,100"))) void hello_world () {
some_hello ();
}
这意味着对 hello_world 先运行 Basic Block Splitter,_然后_运行 Indirect Branching,_再然后_运行 Simple Indirect Branching。它们将按从左到右的定义顺序运行。
qword_1C64D8qword_1A63F0qword_1ACE98UseSipHashedStateChance:使用一个微型的自定义 siphash 函数来检查状态。因此 if (state == 23872) 会变成类似 if (siphash(state) == 3874872081) 的样子,使查找一个块跳转到哪里变得更加困难。当块的去向条件被哈希后,块会执行 state = 23872。每次 siphash 调用都会使用随机值,使其更难被模拟。CloneSipHashChance:克隆 siphash 函数,并在可能时尝试内联它,从而生成多个兄弟副本,这使得只 hook 一个函数是不够的。非常推荐使用此选项,因为它只会增加二进制大小,而不会影响性能。