
Zyrox: plugin ofuscador en tiempo de compilación basado en LLVM.
por qué no ¯\_(ツ)_/¯
Uno de mis proyectos más grandes, donde aprendí mucho sobre los internos de LLVM, formatos binarios, ensamblador y técnicas de ofuscación.
Creo que aprender construyendo es la mejor manera de aprender, por eso construí este proyecto para aprender más sobre estos temas.
He escrito 4 blogs explicando los conceptos detrás de Zyrox:
Estas partes profundizan más que este readme, y definitivamente vale la pena leerlas si estás interesado en el tema.
Esto está pensado para quienes quieran probar rápidamente Zyrox, o aprender cómo integrarlo en un proyecto cmake.
Sigue los pasos en el repositorio Zyrox Template.
instala llvm:
sudo apt update
sudo apt install llvm-18 llvm-18-dev clang-18
clona y 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
asegúrate de tener python3 y pip instalados.
# 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
Después de la ofuscación, ejecuta PyPlugin.py para encriptar las tablas de salto:
# 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]
Revisa el repositorio Zyrox Template para un ejemplo de integración con CMake.
Entiendo que es un tema complejo, y este proyecto fue principalmente con fines educativos, además de servir a BSD Brawl. Si tienes alguna pregunta, o simplemente quieres charlar, no dudes en contactarme:
@s.b[email protected] o [email protected]cualquier ayuda, a través de pull requests o issues, es apreciada!
ZyroxPlugin.cpp registra el pase, luego enlaza siphash (más sobre esto después) y llama a StringEncryption para encriptar las cadenas.
La razón por la que encriptamos las cadenas temprano es para que la lógica de descifrado también se ofusque después.
luego llama a ModuleUtils::ExpandCustomAnnotations y QuickConfig::RegisterPasses para parsear todas las expresiones __attribute__((annotate("..."))) y ejecutar la configuración de QuickJs (ubicada en ZyroxConfig.js)
Cada función se ofusca llamando a Zyrox::RunOnFunction ubicada en ZyroxCore.cpp, se proporcionará más documentación sobre esto en el futuro.
los switches crean tablas de salto y los nodos PHI son molestos de manejar, por lo que usamos FunctionUtils y BasicBlockUtils para aplanar (en sentencias if) y degradar estos respectivamente.
oh vaya, por dónde empezar
todos los argumentos del plugin js están en index.d.ts, por lo que no se hablará de ellos en esta documentación.
para documentación de anotaciones, haz clic aquí
Este pase divide y mezcla un bloque básico en otros más pequeños. Supongamos que tenemos esto:
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;
}
que se compila 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
}
al usar el Divisor de Bloques Básicos con esta configuración:
z.RegisterPass(ObfuscationType.BasicBlockSplitter, {
PassIterations: 1,
"BasicBlockSplitter.SplitBlockChance": 100,
"BasicBlockSplitter.SplitBlockMinSize": 2,
"BasicBlockSplitter.SplitBlockMaxSize": 5,
});
se convierte 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 %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
}
ahora no será muy diferente para una función tan pequeña, pero ¿notaste cómo dividió un bloque básico? Esto es útil combinado con otros pases como Aplanamiento de Flujo de Control
Oh, vaya, este pase tiene la mayor cantidad de características entre todos lol. Empezaré explicando cómo funciona y luego su configuración. supongamos que tenemos este código:
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
cada bloque básico (A, B y C) recibe un estado de despachador único, ejemplo: (simplificado)
states = {
1: LABEL_A,
2: LABEL_B,
3: LABEL_C,
};
luego inyectamos un bloque despachador que controla todo y el código se convierte en: