
Zyrox: LLVM based, compile-time obfuscator plugin.
why not ¯\_(ツ)_/¯
One of my biggest projects, where I learned a lot about LLVM internals, binary formats, assembly and obfuscation techniques.
I believe that learning through building is the best way to learn, thus I built this project to learn more about these topics.
I have wrote 4 blogs explaining the concepts behind Zyrox:
These parts go deeper than this readme, and definitely worth a read if you are interested in the topic.
This is intended for who wants to quick test Zyrox, or learn how to integrate it in a cmake project.
Follow the steps in Zyrox Template repo.
install llvm:
sudo apt update
sudo apt install llvm-18 llvm-18-dev clang-18
clone and compile 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
make sure you have python3 and pip installed.
# 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
After obfuscation, run PyPlugin.py to encrypt jump tables:
# 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]
Check out the Zyrox Template repo for an example CMake integration.
I get this is a complex topic, and this project was mostly for educational purposes, as well as to serve BSD Brawl. If you have any question, or just want to chat, feel free to reach out to me:
@s.b[email protected] or [email protected]any help, through pull requests or issues is appreciated!
ZyroxPlugin.cpp registers the pass, then links siphash (more on this later) and call StringEncryption to encrypt
strings.
The reason we encrypt strings early is so that decryption logic gets obfuscated too later.
then it calls ModuleUtils::ExpandCustomAnnotations
and QuickConfig::RegisterPasses to parse all __attribute__((annotate("..."))) expressions and run
QuickJs config (located in ZyroxConfig.js)
Every function is obfuscated by calling Zyrox::RunOnFunction located in ZyroxCore.cpp,
more documentation about this will be provided in the future.
switches create jump tables and PHI nodes are annoying to deal with thus we use FunctionUtils and BasicBlockUtils
to flatten (into if statements) and demote these respectively.
oh man, where do I start
all js-plugin args are in index.d.ts so will not be talked about in this documentation.
for annotations documentation, click here
This pass splits and shuffles a basic block into smaller ones. suppose we have this:
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;
}
which gets compiled into:
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
}
when using Basic Block Splitter with this config:
z.RegisterPass(ObfuscationType.BasicBlockSplitter, {
PassIterations: 1,
"BasicBlockSplitter.SplitBlockChance": 100,
"BasicBlockSplitter.SplitBlockMinSize": 2,
"BasicBlockSplitter.SplitBlockMaxSize": 5,
});
it becomes:
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
}
now it won't be that much different for such small function but notice how it split a basic block? this is helpful combined with other passes like Control Flow Flattening
Oh, man this pass have the most features among all lol. I will start by explaining how it works then it's config suppose we have this code:
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
each basic block (A, B and C) gets assigned a unique dispatcher state, example: (simplified)
states = {
1: LABEL_A,
2: LABEL_B,
3: LABEL_C,
};
then we inject a dispatcher block that controls everything and the code becomes:
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