
Zyrox: плагин-обфускатор на основе LLVM, действующий на этапе компиляции.
почему бы и нет ¯\_(ツ)_/¯
Один из моих самых больших проектов, где я много узнал о внутренностях LLVM, бинарных форматах, ассемблере и техниках обфускации.
Я считаю, что лучший способ учиться — это создавать, поэтому я создал этот проект, чтобы узнать больше об этих темах.
Я написал 4 блога, объясняющих концепции, лежащие в основе Zyrox:
Эти части раскрывают тему глубже, чем этот README, и их определённо стоит прочитать, если вам интересна тема.
Это предназначено для тех, кто хочет быстро протестировать Zyrox или научиться интегрировать его в CMake-проект.
Выполните шаги из репозитория Zyrox Template.
установите llvm:
sudo apt update
sudo apt install llvm-18 llvm-18-dev clang-18
склонируйте и соберите 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
убедитесь, что установлены python3 и pip.
# 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
После обфускации запустите PyPlugin.py, чтобы зашифровать таблицы переходов:
# 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]
Смотрите репозиторий Zyrox Template, где приведён пример интеграции с CMake.
Я понимаю, что это сложная тема, и этот проект в основном создавался в образовательных целях, а также для BSD Brawl. Если у вас есть вопросы или просто хотите пообщаться, смело обращайтесь ко мне:
@s.b[email protected] или [email protected]Любая помощь в виде pull request'ов или issues приветствуется!
ZyroxPlugin.cpp регистрирует проход, затем подключает siphash (подробнее об этом позже) и вызывает StringEncryption для шифрования строк.
Причина, по которой мы шифруем строки на раннем этапе, в том, чтобы логика дешифрования тоже была обфусцирована позже.
Затем вызываются ModuleUtils::ExpandCustomAnnotations
и QuickConfig::RegisterPasses для разбора всех выражений __attribute__((annotate("..."))) и запуска
QuickJs-конфигурации (расположенной в ZyroxConfig.js)
Каждая функция обфусцируется вызовом Zyrox::RunOnFunction из ZyroxCore.cpp;
в будущем будет добавлено больше документации по этому вопросу.
switch создают таблицы переходов, а PHI-узлы раздражающе сложны в обработке, поэтому мы используем FunctionUtils и BasicBlockUtils,
чтобы соответственно флаттенить (преобразовывать в if-выражения) и понижать их.
ох, с чего бы начать
все аргументы js-плагина описаны в index.d.ts, поэтому в этой документации они не рассматриваются.
документацию по аннотациям см. здесь
Этот проход разбивает базовый блок на несколько меньших и перемешивает их. Предположим, у нас есть такой код:
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;
}
который компилируется в:
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
}
при использовании Basic Block Splitter с такой конфигурацией:
z.RegisterPass(ObfuscationType.BasicBlockSplitter, {
PassIterations: 1,
"BasicBlockSplitter.SplitBlockChance": 100,
"BasicBlockSplitter.SplitBlockMinSize": 2,
"BasicBlockSplitter.SplitBlockMaxSize": 5,
});
он становится таким:
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
}
Для такой маленькой функции разница не будет слишком заметной, но обратите внимание, как разбивается базовый блок. Это полезно в сочетании с другими проходами, такими как Control Flow Flattening
Ох, у этого прохода больше всего возможностей среди всех, лол. Сначала я объясню, как он работает, а затем его конфигурацию. Предположим, у нас есть такой код:
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, B и C) назначается уникальное состояние диспетчера, например: (упрощённо)
states = {
1: LABEL_A,
2: LABEL_B,
3: LABEL_C,
};
затем мы внедряем блок-диспетчер, который управляет всем, и код становится таким: