Про­читав эту статью, ты узна­ешь, как работа­ет обфусци­рующий ком­пилятор, углу­бишь­ся в архи­тек­туру LLVM и смо­жешь писать собс­твен­ные про­ходы для обфуска­ции кода. Мы сде­лаем обфуска­тор стро­чек, соберем LLVM из исходни­ков и узна­ем, как интегри­ровать obfuscator-llvm в сов­ремен­ный Visual Studio, что­бы собирать твой код с обфуска­цией.
 

Что за зверь LLVM

Про­ект LLVM стар­товал в 2000 году и получил рас­простра­нение в начале десятых. Изна­чаль­ная рас­шифров­ка — low level virtual machine, хотя сей­час она не отра­жает суть про­екта. LLVM — фрей­мворк с откры­тым исходным кодом для соз­дания ком­пилято­ров. На базе LLVM мож­но соб­рать ком­пилятор для собс­твен­ного язы­ка прог­рамми­рова­ния. Или «улуч­шить» сущес­тву­ющий, чем мы сегод­ня и зай­мем­ся.

Ар­хитек­турно LLVM раз­делен на три час­ти: фрон­тенд, опти­миза­ция и бэкенд. Фрон­тенд пре­обра­зует исходный код в про­межу­точ­ное пред­став­ление (intermediate representation) — уни­вер­саль­ный про­межу­точ­ный код, который исполь­зует­ся на сле­дующих эта­пах для опти­миза­ции кода и сбор­ки фай­ла. Опти­миза­ция уда­ляет неис­поль­зуемый код, сво­рачи­вает ариф­метичес­кие вычис­ления в готовые кон­стан­ты и заменя­ет неэф­фектив­ные конс­трук­ции более быс­тры­ми. Бэкенд прев­раща­ет IR в машин­ный код.

Ес­ли ты соз­даешь новый язык, дос­таточ­но написать фрон­тенд, генери­рующий IR, а осталь­ную работу LLVM возь­мет на себя. Точ­но так же мож­но работать с кодом (изна­чаль­но написан­ным на любом под­держи­ваемом язы­ке) на уров­не IR, меняя его под свои нуж­ды на эта­пе опти­миза­ции. Или написать свой бэкенд, что­бы собирать ста­рый код под неиз­вес­тную про­цес­сорную архи­тек­туру.

Не­обя­затель­но писать фрон­тенд с нуля. Дос­таточ­но модифи­циро­вать исходный код LLVM. Опти­миза­ция — более гиб­кий инс­тру­мент, она под­держи­вает заг­ружа­емые пла­гины, добав­ляющие про­ходы — спе­циаль­ные фун­кции, обра­баты­вающие IR на уров­не модуля, фун­кции, цик­ла или базово­го бло­ка. Для обфуска­ции кода мы будем исполь­зовать имен­но их.

 

Сборка LLVM под Windows

Для начала уста­новим CMake и помес­тим ути­литу ninja.exe в C:\Program Files\CMake\bin, тем самым сде­лав ее видимой для CMake при сбор­ке.

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::PassPluginLibraryInfo llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, // Версия API плагинов
"InjectFunctionCallPass", // Название плагина
"v0.1", // Версия
[](llvm::PassBuilder& PB) {
PB.registerPipelineParsingCallback(
[](llvm::StringRef Name,
llvm::ModulePassManager& MPM,
llvm::ArrayRef<llvm::PassBuilder::PipelineElement>) {
if (Name == "debug-trace") {
MPM.addPass(DebugTracePass());
return true;
}
return false;
});
} };
}

Здесь я сна­чала отклю­чаю раз­дра­жающее пре­дуп­режде­ние о невер­ной типиза­ции внут­ри кода LLVM. Затем про­шу экспор­тировать фун­кцию llvmGetPassPluginInfo. Она дол­жна быть в экспор­те пла­гина, что­бы сооб­щить при заг­рузке его наз­вание и добавить новый про­ход debug-trace в спи­сок дос­тупных про­ходов. Про­ход рас­полага­ется в методе run клас­са DebugTracePass.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии