
Zyrox : plugin d'obfuscation au moment de la compilation basé sur LLVM.
pourquoi pas ¯\_(ツ)_/¯
Un de mes plus gros projets, où j'ai beaucoup appris sur les internes de LLVM, les formats binaires, l'assembleur et les techniques d'offuscation.
Je crois qu'apprendre en construisant est la meilleure façon d'apprendre, donc j'ai construit ce projet pour en apprendre davantage sur ces sujets.
J'ai écrit 4 articles expliquant les concepts derrière Zyrox :
Ces parties vont plus en détail que ce readme, et valent vraiment la peine d'être lues si le sujet vous intéresse.
Ceci est destiné à ceux qui veulent tester rapidement Zyrox, ou apprendre à l'intégrer dans un projet cmake.
Suivez les étapes dans le dépôt Zyrox Template.
installez llvm :
sudo apt update
sudo apt install llvm-18 llvm-18-dev clang-18
clonez et compilez 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
assurez-vous d'avoir python3 et pip installés.
# Crée un environnement virtuel
python3 -m venv .venv
# Active l'environnement
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
Après l'offuscation, exécutez PyPlugin.py pour chiffrer les tables de saut :
# si vous avez installé les dépendances dans un environnement virtuel, activez-le d'abord :
source .venv/bin/activate
# puis exécutez avec :
python PyPlugin.py --in=<input_file> [--out=<output_file>] [--tables=<zyrox_tables_file>] [--android]
Consultez le dépôt Zyrox Template pour un exemple d'intégration CMake.
Je comprends que c'est un sujet complexe, et ce projet était surtout à des fins éducatives, ainsi que pour servir BSD Brawl. Si vous avez des questions, ou simplement envie de discuter, n'hésitez pas à me contacter :
@s.b[email protected] ou [email protected]toute aide, via des pull requests ou des issues, est appréciée !
ZyroxPlugin.cpp enregistre le pass, puis lie siphash (plus de détails plus tard) et appelle StringEncryption pour chiffrer
les chaînes.
La raison pour laquelle nous chiffrons les chaînes tôt est que la logique de déchiffrement soit elle aussi offusquée plus tard.
Ensuite, il appelle ModuleUtils::ExpandCustomAnnotations
et QuickConfig::RegisterPasses pour analyser toutes les expressions __attribute__((annotate("..."))) et exécuter
la configuration QuickJs (située dans ZyroxConfig.js)
Chaque fonction est offusquée en appelant Zyrox::RunOnFunction situé dans ZyroxCore.cpp,
plus de documentation à ce sujet sera fournie à l'avenir.
Les commutateurs créent des tables de saut et les nœuds PHI sont gênants à traiter, donc nous utilisons FunctionUtils et BasicBlockUtils
pour les aplatir (en instructions if) et les rétrograder respectivement.
oh là là, par où commencer
tous les arguments des plugins js sont dans index.d.ts, donc ils ne seront pas abordés dans cette documentation.
pour la documentation des annotations, cliquez ici
Ce pass divise et mélange un bloc de base en blocs plus petits. Supposons que nous ayons ceci :
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;
}
qui est compilé en :
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
}
en utilisant Basic Block Splitter avec cette configuration :
z.RegisterPass(ObfuscationType.BasicBlockSplitter, {
PassIterations: 1,
"BasicBlockSplitter.SplitBlockChance": 100,
"BasicBlockSplitter.SplitBlockMinSize": 2,
"BasicBlockSplitter.SplitBlockMaxSize": 5,
});
il devient :
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
}
maintenant, cela ne sera pas très différent pour une si petite fonction, mais remarquez comment il a divisé un bloc de base ? c'est utile combiné avec d'autres passes comme Control Flow Flattening
Oh, ce pass a le plus de fonctionnalités parmi tous, lol. Je vais commencer par expliquer comment il fonctionne, puis sa configuration. Supposons que nous ayons ce 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
chaque bloc de base (A, B et C) se voit attribuer un état unique du dispatcher, exemple : (simplifié)
states = {
1: LABEL_A,
2: LABEL_B,
3: LABEL_C,
};
ensuite nous injectons un bloc dispatcher qui contrôle tout et le code devient :