
Zyrox: plugin offuscatore basato su LLVM, a tempo di compilazione.
perché no ¯\_(ツ)_/¯
Uno dei miei progetti più grandi, in cui ho imparato molto sugli internals di LLVM, formati binari, assembly e tecniche di offuscamento.
Credo che imparare costruendo sia il modo migliore per apprendere, quindi ho creato questo progetto per imparare di più su questi argomenti.
Ho scritto 4 blog che spiegano i concetti alla base di Zyrox:
Queste parti approfondiscono più di questo readme e valgono sicuramente una lettura se sei interessato all'argomento.
Questo è pensato per chi vuole testare rapidamente Zyrox, o imparare come integrarlo in un progetto cmake.
Segui i passaggi nel repo Zyrox Template.
installa llvm:
sudo apt update
sudo apt install llvm-18 llvm-18-dev clang-18
clona e compila 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
assicurati di avere python3 e pip installati.
# 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
Dopo l'offuscamento, esegui PyPlugin.py per cifrare le jump table:
# 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]
Dai un'occhiata al repo Zyrox Template per un esempio di integrazione CMake.
Capisco che sia un argomento complesso, e questo progetto è stato principalmente per scopi educativi, oltre che per servire BSD Brawl. Se hai domande, o vuoi semplicemente fare due chiacchiere, sentiti libero di contattarmi:
@s.b[email protected] o [email protected]qualsiasi aiuto, tramite pull request o issue, è apprezzato!
ZyroxPlugin.cpp registra il pass, poi collega siphash (ne parleremo più avanti) e chiama StringEncryption per cifrare le stringhe.
Il motivo per cui cifriamo le stringhe subito è così che anche la logica di decifratura venga offuscata in seguito.
poi chiama ModuleUtils::ExpandCustomAnnotations e QuickConfig::RegisterPasses per fare il parsing di tutte le espressioni __attribute__((annotate("..."))) ed eseguire la config QuickJs (situata in ZyroxConfig.js)
Ogni funzione viene offuscata chiamando Zyrox::RunOnFunction situata in ZyroxCore.cpp; ulteriore documentazione a riguardo verrà fornita in futuro.
gli switch creano jump table e i nodi PHI sono fastidiosi da gestire, quindi usiamo FunctionUtils e BasicBlockUtils per appiattirli (in istruzioni if) e declassarli rispettivamente.
oh cavolo, da dove comincio
tutti gli argomenti del plugin js sono in index.d.ts, quindi non verranno trattati in questa documentazione.
per la documentazione sulle annotazioni, clicca qui
Questo pass divide e mescola un basic block in blocchi più piccoli. Supponiamo di avere questo:
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;
}
che viene compilato in:
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
}
quando si usa Basic Block Splitter con questa config:
z.RegisterPass(ObfuscationType.BasicBlockSplitter, {
PassIterations: 1,
"BasicBlockSplitter.SplitBlockChance": 100,
"BasicBlockSplitter.SplitBlockMinSize": 2,
"BasicBlockSplitter.SplitBlockMaxSize": 5,
});
diventa:
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
}
ora non sarà molto diverso per una funzione così piccola, ma nota come ha diviso un basic block?
questo è utile se combinato con altri pass come Control Flow Flattening
Oh, cavolo, questo pass ha il maggior numero di funzionalità tra tutti lol. Inizierò spiegando come funziona, poi la sua config. Supponiamo di avere questo codice:
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 ogni basic block (A, B e C) viene assegnato uno stato univoco del dispatcher, esempio: (semplificato)
states = {
1: LABEL_A,
2: LABEL_B,
3: LABEL_C,
};
poi iniettiamo un blocco dispatcher che controlla tutto e il codice diventa: