stihl не предоставил(а) никакой дополнительной информации.
Прочитав эту статью, ты узнаешь, как работает обфусцирующий компилятор, углубишься в архитектуру LLVM и сможешь писать собственные проходы для обфускации кода. Мы сделаем обфускатор строчек, соберем LLVM из исходников и узнаем, как интегрировать obfuscator-llvm в современный Visual Studio, чтобы собирать твой код с обфускацией.
Архитектурно LLVM разделен на три части: фронтенд, оптимизация и бэкенд. Фронтенд преобразует исходный код в промежуточное представление (intermediate representation) — универсальный промежуточный код, который используется на следующих этапах для оптимизации кода и сборки файла. Оптимизация удаляет неиспользуемый код, сворачивает арифметические вычисления в готовые константы и заменяет неэффективные конструкции более быстрыми. Бэкенд превращает IR в машинный код.
Если ты создаешь новый язык, достаточно написать фронтенд, генерирующий IR, а остальную работу LLVM возьмет на себя. Точно так же можно работать с кодом (изначально написанным на любом поддерживаемом языке) на уровне IR, меняя его под свои нужды на этапе оптимизации. Или написать свой бэкенд, чтобы собирать старый код под неизвестную процессорную архитектуру.
Необязательно писать фронтенд с нуля. Достаточно модифицировать исходный код LLVM. Оптимизация — более гибкий инструмент, она поддерживает загружаемые плагины, добавляющие проходы — специальные функции, обрабатывающие IR на уровне модуля, функции, цикла или базового блока. Для обфускации кода мы будем использовать именно их.
или Зарегистрируйся и поместим утилиту Для просмотра ссылки Войди или Зарегистрируйся в C:\Program Files\CMake\bin, тем самым сделав ее видимой для CMake при сборке.
Создаем папку в корне диска и запускаем vcvars64.bat. Он установит необходимые для сборки переменные окружения, такие как путь до компилятора и подключаемых файлов MSVC. Далее клонируем исходный код LLVM и запускаем сборку. Ждем около часа окончания сборки и получаем новые файлы в C:\LLVM\custom. Среди них утилиты, компиляторы и подключаемые файлы .lib. Они пригодятся при создании собственных проходов для оптимизации.
Также добавляем список .lib в Additional Dependencies:
Таким образом собираются все наши проходы.
Здесь я сначала отключаю раздражающее предупреждение о неверной типизации внутри кода LLVM. Затем прошу экспортировать функцию llvmGetPassPluginInfo. Она должна быть в экспорте плагина, чтобы сообщить при загрузке его название и добавить новый проход debug-trace в список доступных проходов. Проход располагается в методе run класса DebugTracePass.
Внутри прохода получаю ссылку на функцию OutputDebugStringA, предварительно указав ее прототип. Далее перебираю внутри модуля все функции, у которых есть тело, а не только определение. Вызов getEntryBlock отдает первый базовый блок в теле функции, а getFirstInsertionPt возвращает итератор. Преобразуем его в ссылку на первую инструкцию. Теперь создаю объект IRBuilder, который будет генерировать инструкции в заданной точке, то есть перед первой инструкцией. Формирую глобальную строку с именем функции, которая будет располагаться в секции данных. И создаю вызов функции OutputDebugStringA с аргументом в виде только что определенной строки. В конце PreservedAnalyses явно сообщает LLVM, что код модуля изменился и старый анализ модуля неактуален.
Как и ожидалось, в начале функции main появился вызов OutputDebugStringA. Попробуем собрать IR в исполняемый файл.
Запускаю полученный EXE и в Для просмотра ссылки Войдиили Зарегистрируйся вижу строку [8092] Enter: main.
Рассмотрим метод run. Код перебирает все глобальные переменные, отбрасывая те, у которых нет значения, и те, что не являются константой. Если константа — это строка, получаем ее значение и шифруем. Далее подменяем оригинальную строку шифрованными байтами. Затем копируем список пользователей этой константы в массив Users. Проходим по списку и вызываем обработчики для разных типов «пользователей». В обоих случаях вставляем перед инструкцией локальный буфер и вызов процедуры расшифровки. Наконец, заменяем ссылку на исходную строку ссылкой на созданный нами буфер.
Я взял простой тестовый код, выводящий строчку в консоль. Проще всего поместить вызываемую обфускатором decryptStringInto в оригинальный код.
Запустив файл в консоли, убеждаемся, что все работает: строка выводится без обфускации, хотя и не сохраняется в открытом виде.
или Зарегистрируйся, с ним мы и будем работать.
C++ Clang Compiler for Windows (19.1.5)
MSBuild support for LLVM (clang-cl) toolset
Теперь можно начинать сборку.
Правим файл llvmorg-16.0.6/llvm/lib/CMakeLists.txt и дописываем в него add_subdirectory(Obfuscation).
Запустив CMake, мы получим файл LLVM.sln, из которого запустим сборку Release-версии. Сборка занимает чуть больше часа.
Это изменяет переменные окружения, остальную настройку IDE сделает за нас, надо только выбрать правильный тулсет:
Теперь сборка любого кода будет выполняться нашей версией clang-cl со встроенным обфускатором.
Рассмотрим, что делает каждый из ключей.
На основе этого блока кода рядом вставлен цикл с условием, которое всегда будет False:
В отличие от традиционной вставки мусора, то есть незначащего (или «мертвого») кода, выловить оригинальный код по уникальным WinAPI становится куда сложнее. Кроме того, bogus-блоки усложняют граф исполнения, разделяя базовый блок на несколько частей.
Простые вычисления вроде X+Y превращаются в длинную цепочку операций. Обработке подвергаются только подходящие участки кода. Иногда в коде этого метода обфускации сильно течет память, из‑за чего компилятор вешает систему. Все‑таки двадцать гигабайт оперативной памяти для сборки пяти килобайт кода слегка избыточно. Припоминаю, что эта проблема была и на первом серьезном релизе в начале десятых.
Логика потока управления перестает считываться на глаз. Чтобы выяснить, что код делает, потребуется трассировка.
Настройки:
До и после обфускации
Что за зверь LLVM
Проект LLVM стартовал в 2000 году и получил распространение в начале десятых. Изначальная расшифровка — low level virtual machine, хотя сейчас она не отражает суть проекта. LLVM — фреймворк с открытым исходным кодом для создания компиляторов. На базе LLVM можно собрать компилятор для собственного языка программирования. Или «улучшить» существующий, чем мы сегодня и займемся.Архитектурно LLVM разделен на три части: фронтенд, оптимизация и бэкенд. Фронтенд преобразует исходный код в промежуточное представление (intermediate representation) — универсальный промежуточный код, который используется на следующих этапах для оптимизации кода и сборки файла. Оптимизация удаляет неиспользуемый код, сворачивает арифметические вычисления в готовые константы и заменяет неэффективные конструкции более быстрыми. Бэкенд превращает IR в машинный код.
Если ты создаешь новый язык, достаточно написать фронтенд, генерирующий IR, а остальную работу LLVM возьмет на себя. Точно так же можно работать с кодом (изначально написанным на любом поддерживаемом языке) на уровне IR, меняя его под свои нужды на этапе оптимизации. Или написать свой бэкенд, чтобы собирать старый код под неизвестную процессорную архитектуру.
Необязательно писать фронтенд с нуля. Достаточно модифицировать исходный код LLVM. Оптимизация — более гибкий инструмент, она поддерживает загружаемые плагины, добавляющие проходы — специальные функции, обрабатывающие IR на уровне модуля, функции, цикла или базового блока. Для обфускации кода мы будем использовать именно их.
Сборка LLVM под Windows
Для начала установим Для просмотра ссылки Войди
Код:
mkdir C:\LLVM && cd C:\LLVM
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
mkdir build && cd build
cmake -G "Ninja" ^
-DLLVM_ENABLE_PROJECTS="clang" ^
-DLLVM_ENABLE_ASSERTIONS=ON ^
-DCMAKE_BUILD_TYPE=Release ^
-DLLVM_BUILD_LLVM_DYLIB=ON ^
-DLLVM_ENABLE_RTTI=ON ^
-DCMAKE_INSTALL_PREFIX=C:/llvm/custom ^
../llvm
ninja
ninja install
Создаем папку в корне диска и запускаем vcvars64.bat. Он установит необходимые для сборки переменные окружения, такие как путь до компилятора и подключаемых файлов MSVC. Далее клонируем исходный код LLVM и запускаем сборку. Ждем около часа окончания сборки и получаем новые файлы в C:\LLVM\custom. Среди них утилиты, компиляторы и подключаемые файлы .lib. Они пригодятся при создании собственных проходов для оптимизации.
Сборка проходов
Создаем проект в Visual Studio и меняем его настройки:
Код:
Configuration Type = Dynamic Library (.dll)
Additional Include Directories = C:\LLVM\include
Additional Library Directories = C:\LLVM\lib
C++ Langauge Standart = ISO C++17 Standard (/std:c++17)
Code Generation → RuntimeLibrary = /MT
Также добавляем список .lib в Additional Dependencies:
Код:
LLVMCore.lib
LLVMSupport.lib
LLVMBitReader.lib
LLVMIRReader.lib
LLVMAnalysis.lib
LLVMPasses.lib
LLVMFrontendOpenMP.lib
LLVMTargetParser.lib
LLVMRemarks.lib
LLVMProfileData.lib
LLVMBinaryFormat.lib
LLVMDemangle.lib
LLVMBitstreamReader.lib
Таким образом собираются все наши проходы.
Проход для анализа покрытия
Создадим простой плагин для оптимизатора. Первое, что приходит на ум, — добавить логирование на вход каждой функции. AFL использует похожий подход для определения покрытия — он добавляет свой код в исследуемые исходники, чтобы отслеживать поток управления.
Код:
#pragma warning(disable : 4146)
#pragma comment(linker, "/export:llvmGetPassPluginInfo")
#define _CRT_SECURE_NO_WARNINGS
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/IR/PassManager.h"
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Type.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/DerivedTypes.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
struct DebugTracePass : PassInfoMixin<DebugTracePass> {
PreservedAnalyses run(Module& M, ModuleAnalysisManager&) {
LLVMContext& Ctx = M.getContext();
// Получаем i8* тип (char*)
Type* i8Ty = Type::getInt8Ty(Ctx);
PointerType* i8PtrTy = PointerType::get(i8Ty, 0);
// Тип функции: void OutputDebugStringA(char*)
FunctionType* debugFnTy = FunctionType::get(Type::getVoidTy(Ctx), { i8PtrTy }, false);
FunctionCallee debugFn = M.getOrInsertFunction("OutputDebugStringA", debugFnTy);
for (Function& F : M) {
// Пропуск, если это объявление функции
if (F.isDeclaration()) continue;
// Строим IR перед первой инструкцией
Instruction* insertPt = &*F.getEntryBlock().getFirstInsertionPt();
IRBuilder<> builder(insertPt);
// Формируем строку
std::string msg = "Enter: " + F.getName().str();
Value* msgStr = builder.CreateGlobalString(msg);
// Вставляем вызов OutputDebugStringA
builder.CreateCall(debugFn, msgStr);
}
return PreservedAnalyses::none();
}
};
extern "C"
llvm:assPluginLibraryInfo llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, // Версия API плагинов
"InjectFunctionCallPass", // Название плагина
"v0.1", // Версия
[](llvm:assBuilder& PB) {
PB.registerPipelineParsingCallback(
[](llvm::StringRef Name,
llvm::ModulePassManager& MPM,
llvm::ArrayRef<llvm:assBuilder:ipelineElement>) {
if (Name == "debug-trace") {
MPM.addPass(DebugTracePass());
return true;
}
return false;
});
} };
}
Здесь я сначала отключаю раздражающее предупреждение о неверной типизации внутри кода LLVM. Затем прошу экспортировать функцию llvmGetPassPluginInfo. Она должна быть в экспорте плагина, чтобы сообщить при загрузке его название и добавить новый проход debug-trace в список доступных проходов. Проход располагается в методе run класса DebugTracePass.
Внутри прохода получаю ссылку на функцию OutputDebugStringA, предварительно указав ее прототип. Далее перебираю внутри модуля все функции, у которых есть тело, а не только определение. Вызов getEntryBlock отдает первый базовый блок в теле функции, а getFirstInsertionPt возвращает итератор. Преобразуем его в ссылку на первую инструкцию. Теперь создаю объект IRBuilder, который будет генерировать инструкции в заданной точке, то есть перед первой инструкцией. Формирую глобальную строку с именем функции, которая будет располагаться в секции данных. И создаю вызов функции OutputDebugStringA с аргументом в виде только что определенной строки. В конце PreservedAnalyses явно сообщает LLVM, что код модуля изменился и старый анализ модуля неактуален.
Запуск прохода
Создадим для теста самый простой исходник:
Код:
#include <Windows.h>
int main()
{
return 0;
}
И соберем из него промежуточный код:
clang.exe -S -emit-llvm test.cpp -o test.ll
Настало время применить новый проход:
opt.exe -load-pass-plugin llvm_pass.dll -passes=debug-trace -S test.ll -o output.ll
Посмотрим на созданный после прохода промежуточный код:
@0 = private unnamed_addr constant [12 x i8] c"Enter: main\00", align 1
; Function Attrs: mustprogress noinline norecurse nounwind optnone uwtable
define dso_local noundef i32 @main() #0 {
entry:
call void @OutputDebugStringA(ptr @0)
%retval = alloca i32, align 4
store i32 0, ptr %retval, align 4
ret i32 0
}
declare void @OutputDebugStringA(ptr)
Как и ожидалось, в начале функции main появился вызов OutputDebugStringA. Попробуем собрать IR в исполняемый файл.
clang.exe output.ll -o output.exe
Запускаю полученный EXE и в Для просмотра ссылки Войди
Проход для скрытия строк
Рассмотрим пример поинтереснее. Допустим, мы ходим избежать детектирования нашего файла по строковым константам. Для этого следует зашифровать все строчки на этапе компиляции и расшифровывать их в локальный буфер в момент использования. Для шифрования подойдет любой симметричный алгоритм, но для простоты используем обычный XOR с константой.
Код:
class EncryptStringsPass : public PassInfoMixin<EncryptStringsPass> {
LLVMContext* Ctx;
Type* i8Ty;
PointerType* i8PtrTy;
Type* i32Ty;
FunctionCallee DecryptFunc;
std::string encryptString(const std::string& Original) {
std::string Encrypted = Original;
for (char& c : Encrypted) c ^= 0xAA;
return Encrypted;
}
void createDecryptionBuffer(IRBuilder<>& B, Value* TargetPtr, unsigned Len, Value*& OutBufPtr) {
auto* Buf = B.CreateAlloca(ArrayType::get(i8Ty, Len + 1));
auto* BufCast = B.CreatePointerCast(Buf, i8PtrTy);
auto* PtrCast = B.CreatePointerCast(TargetPtr, i8PtrTy);
B.CreateCall(DecryptFunc, { BufCast, PtrCast, B.getInt32(Len) });
OutBufPtr = BufCast;
}
void handleConstantExprUser(ConstantExpr* CE, const std::string& Original) {
std::vector<User*> CEUsers(CE->users().begin(), CE->users().end());
for (User* Use : CEUsers) {
if (auto* I = dyn_cast<Instruction>(Use)) {
IRBuilder<> B(I);
Value* BufPtr = nullptr;
createDecryptionBuffer(B, CE, Original.size(), BufPtr);
I->replaceUsesOfWith(CE, BufPtr);
}
}
}
void handleRegularUser(Value* Ptr, Instruction* InsertPt, const std::string& Original) {
IRBuilder<> B(InsertPt);
Value* BufPtr = nullptr;
createDecryptionBuffer(B, Ptr, Original.size(), BufPtr);
Ptr->replaceUsesWithIf(BufPtr, [&](Use& U) {
return dyn_cast<Instruction>(U.getUser()) == InsertPt;
});
}
public:
PreservedAnalyses run(Module& M, ModuleAnalysisManager&) {
Ctx = &M.getContext();
i8Ty = Type::getInt8Ty(*Ctx);
i8PtrTy = PointerType::get(i8Ty, 0);
i32Ty = Type::getInt32Ty(*Ctx);
DecryptFunc = M.getOrInsertFunction(
"decryptStringInto",
FunctionType::get(Type::getVoidTy(*Ctx), { i8PtrTy, i8PtrTy, i32Ty }, false)
);
for (auto& GV : M.globals()) {
if (!GV.hasInitializer() || !GV.isConstant()) continue;
auto* CA = dyn_cast<ConstantDataArray>(GV.getInitializer());
if (!CA || !CA->isString()) continue;
std::string Original = CA->getAsString().str();
std::string Encrypted = encryptString(Original);
auto* ArrayTy = cast<ArrayType>(GV.getValueType());
if (Encrypted.size() < ArrayTy->getNumElements())
Encrypted.resize(ArrayTy->getNumElements(), '\0');
std::vector<uint8_t> Bytes(Encrypted.begin(), Encrypted.end());
GV.setInitializer(ConstantDataArray::get(*Ctx, Bytes));
GV.setConstant(false);
std::vector<User*> Users(GV.users().begin(), GV.users().end());
for (User* U : Users) {
if (auto* CE = dyn_cast<ConstantExpr>(U)) {
if (CE->getOpcode() == Instruction::GetElementPtr) {
handleConstantExprUser(CE, Original);
}
continue;
}
Instruction* InsertPt = nullptr;
Value* Ptr = nullptr;
if (auto* GEP = dyn_cast<GetElementPtrInst>(U)) {
Ptr = GEP;
if (!GEP->user_empty())
InsertPt = dyn_cast<Instruction>(*GEP->user_begin());
}
else if (auto* I = dyn_cast<Instruction>(U)) {
Ptr = &GV;
InsertPt = I;
}
if (Ptr && InsertPt)
handleRegularUser(Ptr, InsertPt, Original);
}
}
return PreservedAnalyses::none();
}
};
Рассмотрим метод run. Код перебирает все глобальные переменные, отбрасывая те, у которых нет значения, и те, что не являются константой. Если константа — это строка, получаем ее значение и шифруем. Далее подменяем оригинальную строку шифрованными байтами. Затем копируем список пользователей этой константы в массив Users. Проходим по списку и вызываем обработчики для разных типов «пользователей». В обоих случаях вставляем перед инструкцией локальный буфер и вызов процедуры расшифровки. Наконец, заменяем ссылку на исходную строку ссылкой на созданный нами буфер.
Код:
#include <windows.h>
#include <stdint.h>
#include <stdlib.h>
extern "C" void decryptStringInto(char* out, const char* enc, int len) {
char key = 0xAA;
for (int i = 0; i < len; ++i) {
out = enc ^ key;
}
out[len] = '\0';
}
int main()
{
DWORD written;
const char* message = "Xakep.ru\n";
WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE), message, (DWORD)lstrlenA(message), &written, NULL);
return 0;
}
Я взял простой тестовый код, выводящий строчку в консоль. Проще всего поместить вызываемую обфускатором decryptStringInto в оригинальный код.
Код:
clang.exe -S -emit-llvm test_str.cpp -o test_str.ll
opt.exe -load-pass-plugin llvm_pass.dll -passes=encrypt-strings -S test_str.ll -o output.ll
clang.exe output.ll -o output.exe
Соберем исходник и посмотрим после декомпиляции, что внутри:
int __fastcall main(int argc, const char **argv, const char **envp)
{
HANDLE StdHandle;
LPCSTR lpBuffer;
int nNumberOfCharsToWrite;
CHAR v7[9];
LPCSTR lpString;
DWORD NumberOfCharsWritten[2];
NumberOfCharsWritten[1] = 0;
sub_140001000(v7, &unk_14001A000, 8);
lpString = v7;
nNumberOfCharsToWrite = lstrlenA(v7);
lpBuffer = lpString;
StdHandle = GetStdHandle(0xFFFFFFF5);
WriteConsoleA(StdHandle, lpBuffer, nNumberOfCharsToWrite, NumberOfCharsWritten, 0);
return 0;
}
Запустив файл в консоли, убеждаемся, что все работает: строка выводится без обфускации, хотя и не сохраняется в открытом виде.
OLLVM-16
Время поговорить о серьезных обфускаторах. Проект obfuscator-llvm (или, как его сокращенно называют, OLLVM) появился в 2010 году как университетский проект по защите кода. Фактически это форк LLVM, встраивающий проходы обфускации в сам компилятор. Он работает с промежуточным кодом, так что теоретически может работать с любыми поддерживаемыми LLVM выходными языком и архитектурой. Последняя версия обфускатора вышла для LLVM-4 (актуальная версия — LLVM-20). Но к счастью, есть современный порт Для просмотра ссылки ВойдиСборка
Для начала установим в Visual Studio 2022 пару пакетов для работы с LLVM:Compilers, build tools, and runtimes
Теперь можно начинать сборку.
Код:
git clone -b llvmorg-16.0.6 --depth=1 https://github.com/llvm/llvm-project.git llvmorg-16.0.6
git clone https://github.com/wwh1004/ollvm-16
robocopy ollvm-16/Obfuscation llvmorg-16.0.6/llvm/lib/Obfuscation
Правим файл llvmorg-16.0.6/llvm/lib/CMakeLists.txt и дописываем в него add_subdirectory(Obfuscation).
Код:
cmake -S C:\llvm-16.0.6\llvm -B C:\llvm-16.0.6-build ^
-G "Visual Studio 17 2022" -A x64 -Thost=x64 ^
-DLLVM_ENABLE_PROJECTS="clang;lld" -DCMAKE_BUILD_TYPE=Release ^
-DLLVM_TARGETS_TO_BUILD=X86 -DLLVM_OBFUSCATION_LINK_INTO_TOOLS=ON ^
-DLLVM_INCLUDE_TESTS=OFF -DLLVM_INCLUDE_EXAMPLES=OFF
Запустив CMake, мы получим файл LLVM.sln, из которого запустим сборку Release-версии. Сборка занимает чуть больше часа.
Интеграция в Visual Studio
По умолчанию Visual Studio использует LLVM-19. Нам нужно сообщить компилятору, что мы хотим использовать другую версию. Для этого в корне проекта создаем файл Directory.build.props:
Код:
<Project>
<PropertyGroup>
<LLVMInstallDir>C:\llvm-16.0.6-build</LLVMInstallDir>
<LLVMToolsVersion>16.0.6</LLVMToolsVersion>
</PropertyGroup>
</Project>
Это изменяет переменные окружения, остальную настройку IDE сделает за нас, надо только выбрать правильный тулсет:
Platform Toolset = LLVM (clang-cl)
Теперь сборка любого кода будет выполняться нашей версией clang-cl со встроенным обфускатором.
Обзор методов обфускации
Управление обфускацией в нашем компиляторе происходит через специальные ключи. Допиши в Command Line эти строки:-mllvm -sub -mllvm -sub_loop=3 -mllvm -split -mllvm -split_num=3 -mllvm -fla -mllvm -bcf -mllvm -bcf_loop=3 -mllvm -bcf_prob=40
Рассмотрим, что делает каждый из ключей.
Bogus control flow
Этот метод вставляет в базовый блок фальшивые участки кода, которые никогда не будут исполнены. Настройки:- -bcf — включает этот вид обфускации;
- -bcf_loop=N — задает, сколько раз применять BCF к одной функции;
- -bcf_prob=P — задает вероятность (в процентах, от 0 до 100) добавления bogus-блока.
AcceleratorsW = LoadAcceleratorsW(hInstance, (LPCWSTR)0x6D);
На основе этого блока кода рядом вставлен цикл с условием, которое всегда будет False:
Код:
AcceleratorsW = LoadAcceleratorsW(a1, (LPCWSTR)0x6D);
v9 = x;
for ( i = y; y >= 10 && (((_BYTE)x * ((_BYTE)x + 1)) & 1) != 0; i = y )
{
LoadAcceleratorsW(a1, (LPCWSTR)0x6D);
AcceleratorsW = LoadAcceleratorsW(a1, (LPCWSTR)0x6D);
v9 = x;
}
Instruction substitution
Это метод обфускации, при котором простые инструкции типа сложения, вычитания или проверки флага заменяются более сложными, но семантически эквивалентными конструкциями. Он работает по принципу «делаем простое сложным», чтобы затруднить анализ и распознавание стандартных паттернов в коде. Настройки:- -sub — включает этот вид обфускации;
- -sub_loop=N — сколько раз пройти по функции.
Код:
EndDialog(a1, a3);
И вот во что превратился код после обфускации:
v34 = (~v32 & 0xCE296CF528B2527EuLL | v32 & 0x31D6930AD74DAD81LL)
^ (~v33 & 0xCE296CF528B2527EuLL | v33 & 0x31D6930AD74DAD81LL);
v35 = ((v34 | ~(~v32 | ~v33)) ^ 0x45B110EDBA343CE6LL)
& (v34 | ~(~v32 | ~v33) | 0x45B110EDBA343CE6LL);
EndDialog(hDlg, ~(~(~(v26 ^ ~v35) & (v35 | v26) ^ 0x7B085DB5B30B2D66LL)
^ 0x7B085DB5B30B2D66LL | (~v35 & 0x861C6ABB45D8AB39uLL
| v35 & 0x79E39544BA2754C6LL) ^ 0x79E39544BA2754C6LL));
Простые вычисления вроде X+Y превращаются в длинную цепочку операций. Обработке подвергаются только подходящие участки кода. Иногда в коде этого метода обфускации сильно течет память, из‑за чего компилятор вешает систему. Все‑таки двадцать гигабайт оперативной памяти для сборки пяти килобайт кода слегка избыточно. Припоминаю, что эта проблема была и на первом серьезном релизе в начале десятых.
Control flow flattening
Этот метод «уплощает» поток управления, превращая легко читаемый граф в подобие виртуальной машины. Например, таким был код до:
Код:
if (x > 0)
do_positive();
else
do_negative();
do_final();
Таким стал после:
int label = 0;
while (1) {
switch (label) {
case 0:
if (x > 0)
label = 1;
else
label = 2;
break;
case 1:
do_positive();
label = 3;
break;
case 2:
do_negative();
label = 3;
break;
case 3:
do_final();
return;
}
}
Логика потока управления перестает считываться на глаз. Чтобы выяснить, что код делает, потребуется трассировка.
Настройки:
- -fla — включает этот вид обфускации;
- -split — активирует деление базовых блоков на части;
- -split_num=N — сколько раз разделить базовый блок.
