
Zyrox: plugin de ofuscação em tempo de compilação baseado em LLVM.
por que não ¯\_(ツ)_/¯
Um dos meus maiores projetos, onde aprendi muito sobre os internos do LLVM, formatos binários, assembly e técnicas de ofuscação.
Acredito que aprender construindo é a melhor forma de aprender, portanto construí este projeto para aprender mais sobre esses tópicos.
Escrevi 4 blogs explicando os conceitos por trás do Zyrox:
Essas partes vão mais fundo que este README e definitivamente valem a leitura se você está interessado no tópico.
Isso é destinado a quem deseja testar rapidamente o Zyrox, ou aprender como integrá-lo em um projeto cmake.
Siga os passos no repositório Zyrox Template.
instale o llvm:
sudo apt update
sudo apt install llvm-18 llvm-18-dev clang-18
clone e compile o 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
certifique-se de ter python3 e 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
Após a ofuscação, execute PyPlugin.py para criptografar as tabelas 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]
Confira o repositório Zyrox Template para um exemplo de integração com CMake.
Eu entendo que este é um tópico complexo, e este projeto foi principalmente para fins educacionais, além de servir ao BSD Brawl. Se você tiver alguma dúvida, ou apenas quiser conversar, sinta-se à vontade para entrar em contato comigo:
@s.b[email protected] ou [email protected]qualquer ajuda, através de pull requests ou issues, é apreciada!
ZyroxPlugin.cpp registra o pass, então linka siphash (mais sobre isso depois) e chama StringEncryption para criptografar strings.
A razão de criptografarmos strings cedo é para que a lógica de descriptografia também seja ofuscada depois.
então ele chama ModuleUtils::ExpandCustomAnnotations
e QuickConfig::RegisterPasses para analisar todas as expressões __attribute__((annotate("..."))) e executar a configuração do QuickJs (localizada em ZyroxConfig.js)
Toda função é ofuscada chamando Zyrox::RunOnFunction localizada em ZyroxCore.cpp, mais documentação sobre isso será fornecida no futuro.
switches criam tabelas de salto e nós PHI são irritantes de lidar, portanto usamos FunctionUtils e BasicBlockUtils para achatar (em ifs) e rebaixar estes respectivamente.
oh cara, por onde eu começo
todos os argumentos do plugin js estão em index.d.ts então não serão abordados nesta documentação.
para documentação de anotações, clique aqui
Este pass divide e embaralha um bloco básico em blocos menores. Suponha que temos isto:
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 é compilado para:
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
}
ao usar o Divisor de Bloco Básico com esta configuração:
z.RegisterPass(ObfuscationType.BasicBlockSplitter, {
PassIterations: 1,
"BasicBlockSplitter.SplitBlockChance": 100,
"BasicBlockSplitter.SplitBlockMinSize": 2,
"BasicBlockSplitter.SplitBlockMaxSize": 5,
});
torna-se:
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
}
agora não será tão diferente para uma função tão pequena, mas note como ele dividiu um bloco básico? isso é útil combinado com outros passes como Achatamento de Fluxo de Controle
Oh, cara, este pass tem mais recursos entre todos, lol. Vou começar explicando como funciona e depois sua configuração. Suponha que temos 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 bloco básico (A, B e C) recebe um estado de despacho único, exemplo: (simplificado)
states = {
1: LABEL_A,
2: LABEL_B,
3: LABEL_C,
};
então injetamos um bloco de despacho que controla tudo e o código se torna: