Как стать автором
Обновить

Техники обфускации кода при помощи LLVM

Время на прочтение8 мин
Количество просмотров31K
image
На хабре есть много замечательных статей о возможностях и способах применения LLVM. Мне бы хотелось рассказать подробнее о популярных техниках обфускации, которые можно реализовать при помощи LLVM, с целью усложнения анализа приложений.

Введение


Статья несет в себе больше теории, чем практической составляющей и предполагает наличия определенных знаний у читателя, а так же желание решать интересные задачи самому, не получая готовых решений. Большинство инструкций в LLVM IR базируется на трехадресном коде, это значит, что они принимают два аргумента и возвращают одно значение и что количество доступных нам инструкций ограничено.

Используемый софт:
GCC 4.8.2 (mingw64)
IDA DEMO
Clang 3.4
LLVM 3.4

Что возможно реализовать при помощи LLVM?
1) Cлучайный CFG
Данный метод модифицирует граф исполнения программы дополняя ее базовыми блоками, оригинальный стартовый блок может быть перемещен, разбавлен мусором.
Примеры
Оригинал
#include <stdio.h>
#include <stdlib.h>
int rand_func()
{
	return 5+rand();;
}
int main()
{
	int a = rand_func();
	goto test;
	exit(0);
	test:
	int b = a+a;
}

Оригинальный граф
image
Обфускация 1 запуск, функция main
image
Обфускация 2 запуск, функция main
image
Обфускация 3 запуск, функция main
image

2) Вставка огромного количества базовых блоков в CFG, с необязательным исполнением.(Смотрите скриншоты с п.1)
Берется случайный базовый блок, изменяется его терминатор(инструкция завершения базового блока), создается множество базовых блоков и все они смешиваются между собой, они могут быть исполняемыми так и не быть, на фантазию автора.
3) Замусоривание кода.
Предположим, что у нас есть определенный код, он разбавляется мусорными инструкциями которые пытаются имитировать свою полезность. Они могут обращаться/изменять наши данные, не влияя на исполнение программы в целом. Цель максимально усложнить анализ нашего приложения, с минимальной потерей в производительности.
Один из множества доступных вариантов обфускации
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void test()
{
    int a =  32;
	int b = time(0);
	int c = a+a+b;
	int d = a-b+c*2;
	printf("%d",d);
}
int main()
{
	test();
}

image

4) Скрытие констант, данных.
Предположим, что у нас есть константа 15h, мы делаем так, что в нативном коде константа будет формироваться во время исполнения и не встречается в открытом виде.Так же константные данные могут скрываться при помощи любого алгоритма шифрования.
Пример
#include <stdio.h>
#include <stdlib.h>

int main()
{
	const char *habr = "habrahabr";
	printf("%s",habr);
}

Находим константные данные, а именно habrahabr, и вставляем свой расшифровщик данных. На изображении пример с xor-ом, но можно добавить любой алгоритм шифрования (AES,RC4 и тд.) Данные после использования (printf) на стеке будут зашифрованы случайным ключом.
image
Предположим вы захотели добавить шифрование данных, как это сделать проще всего?
LLVM умеет генерировать из cpp файлов свой код, который вы можете вставить в свой проект.
Смотрите подсказку в разделе ответы на вопросы.

5) Клонирование функций и использование их в случайном порядке.
Одна и та же функция клонируются на множество(с возможными изменениями), на месте кода вызова вставляется свой обработчик, функции вызываются в случайном порядке.
6) Объединение функций.
Все функции и их код переносятся в одну. В некоторых случаях использование подобного метода чревато.
7) Организация из кода конечного автомата или свитча.
Создается новый базовый блок, который становится точкой входа для main функции, от него создается ветвления (возможно на основе переменной) к другим базовым блокам.
Пример
Оригинальный код.
#include <stdio.h>
#include <stdlib.h>

int rand_func()
{
	return 5+rand();;
}

int main()
{
	const char *habr = "habrahabr";
	printf("%s",habr);
	int a = rand_func();
	goto test;
	exit(0);
	test:
	int b = a+a;
}

Первый раз.
image
Второй раз.
image

8) Создание из кода псевдоциклов.
Применимо относительно функций, берется базовый блок конкретной функции, к нему добавляется еще несколько блоков для организации цикла, цикл исполняется только один раз.
9) Создается случайная виртуальная машина, весь существующий код трансформируется под нее, для меня этот пункт возможен пока что только в теории.

С чего начать изучение?


Посмотреть список доступных трехадресных команд
//===-- llvm/Instruction.def — File that describes Instructions -*- C++ -*-===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
//
// This file contains descriptions of the various LLVM instructions. This is
// used as a central place for enumerating the different instructions and
// should eventually be the place to put comments about the instructions.
//
//===----------------------------------------------------------------------===//

FIRST_TERM_INST ( 1)
HANDLE_TERM_INST ( 1, Ret, ReturnInst)
HANDLE_TERM_INST ( 2, Br, BranchInst)
HANDLE_TERM_INST ( 3, Switch, SwitchInst)
HANDLE_TERM_INST ( 4, IndirectBr, IndirectBrInst)
HANDLE_TERM_INST ( 5, Invoke, InvokeInst)
HANDLE_TERM_INST ( 6, Resume, ResumeInst)
HANDLE_TERM_INST ( 7, Unreachable, UnreachableInst)
LAST_TERM_INST ( 7)

// Standard binary operators…
FIRST_BINARY_INST( 8)
HANDLE_BINARY_INST( 8, Add, BinaryOperator)
HANDLE_BINARY_INST( 9, FAdd, BinaryOperator)
HANDLE_BINARY_INST(10, Sub, BinaryOperator)
HANDLE_BINARY_INST(11, FSub, BinaryOperator)
HANDLE_BINARY_INST(12, Mul, BinaryOperator)
HANDLE_BINARY_INST(13, FMul, BinaryOperator)
HANDLE_BINARY_INST(14, UDiv, BinaryOperator)
HANDLE_BINARY_INST(15, SDiv, BinaryOperator)
HANDLE_BINARY_INST(16, FDiv, BinaryOperator)
HANDLE_BINARY_INST(17, URem, BinaryOperator)
HANDLE_BINARY_INST(18, SRem, BinaryOperator)
HANDLE_BINARY_INST(19, FRem, BinaryOperator)

// Logical operators (integer operands)
HANDLE_BINARY_INST(20, Shl, BinaryOperator) // Shift left (logical)
HANDLE_BINARY_INST(21, LShr, BinaryOperator) // Shift right (logical)
HANDLE_BINARY_INST(22, AShr, BinaryOperator) // Shift right (arithmetic)
HANDLE_BINARY_INST(23, And, BinaryOperator)
HANDLE_BINARY_INST(24, Or, BinaryOperator)
HANDLE_BINARY_INST(25, Xor, BinaryOperator)
LAST_BINARY_INST(25)

// Memory operators…
FIRST_MEMORY_INST(26)
HANDLE_MEMORY_INST(26, Alloca, AllocaInst) // Stack management
HANDLE_MEMORY_INST(27, Load, LoadInst ) // Memory manipulation instrs
HANDLE_MEMORY_INST(28, Store, StoreInst )
HANDLE_MEMORY_INST(29, GetElementPtr, GetElementPtrInst)
HANDLE_MEMORY_INST(30, Fence, FenceInst )
HANDLE_MEMORY_INST(31, AtomicCmpXchg, AtomicCmpXchgInst )
HANDLE_MEMORY_INST(32, AtomicRMW, AtomicRMWInst )
LAST_MEMORY_INST(32)

// Cast operators…
// NOTE: The order matters here because CastInst::isEliminableCastPair
// NOTE: (see Instructions.cpp) encodes a table based on this ordering.
FIRST_CAST_INST(33)
HANDLE_CAST_INST(33, Trunc, TruncInst ) // Truncate integers
HANDLE_CAST_INST(34, ZExt, ZExtInst ) // Zero extend integers
HANDLE_CAST_INST(35, SExt, SExtInst ) // Sign extend integers
HANDLE_CAST_INST(36, FPToUI, FPToUIInst ) // floating point -> UInt
HANDLE_CAST_INST(37, FPToSI, FPToSIInst ) // floating point -> SInt
HANDLE_CAST_INST(38, UIToFP, UIToFPInst ) // UInt -> floating point
HANDLE_CAST_INST(39, SIToFP, SIToFPInst ) // SInt -> floating point
HANDLE_CAST_INST(40, FPTrunc, FPTruncInst ) // Truncate floating point
HANDLE_CAST_INST(41, FPExt, FPExtInst ) // Extend floating point
HANDLE_CAST_INST(42, PtrToInt, PtrToIntInst) // Pointer -> Integer
HANDLE_CAST_INST(43, IntToPtr, IntToPtrInst) // Integer -> Pointer
HANDLE_CAST_INST(44, BitCast, BitCastInst ) // Type cast
LAST_CAST_INST(44)

// Other operators…
FIRST_OTHER_INST(45)
HANDLE_OTHER_INST(45, ICmp, ICmpInst ) // Integer comparison instruction
HANDLE_OTHER_INST(46, FCmp, FCmpInst ) // Floating point comparison instr.
HANDLE_OTHER_INST(47, PHI, PHINode ) // PHI node instruction
HANDLE_OTHER_INST(48, Call, CallInst ) // Call a function
HANDLE_OTHER_INST(49, Select, SelectInst ) // select instruction
HANDLE_OTHER_INST(50, UserOp1, Instruction) // May be used internally in a pass
HANDLE_OTHER_INST(51, UserOp2, Instruction) // Internal to passes only
HANDLE_OTHER_INST(52, VAArg, VAArgInst ) // vaarg instruction
HANDLE_OTHER_INST(53, ExtractElement, ExtractElementInst)// extract from vector
HANDLE_OTHER_INST(54, InsertElement, InsertElementInst) // insert into vector
HANDLE_OTHER_INST(55, ShuffleVector, ShuffleVectorInst) // shuffle two vectors.
HANDLE_OTHER_INST(56, ExtractValue, ExtractValueInst)// extract from aggregate
HANDLE_OTHER_INST(57, InsertValue, InsertValueInst) // insert into aggregate
HANDLE_OTHER_INST(58, LandingPad, LandingPadInst) // Landing pad instruction.
LAST_OTHER_INST(58)

Следует ознакомиться со следующей документацией:
LLVM-CheatSheet
LLVM Programmers Manual
LLVM-CheatSheet 2
LLVMBackendCPU
Obfuscating c++ programs via CFF

Cтоит посмотреть публичные реализации обфускации кода для ознакомления.
1) Obfuscator-llvm
Реализована замена инструкций, уплотнение графа исполнения.
2) Kryptonite
Реализована замена инструкций аналогами / разложение инструкций.

Сниппеты


Для того чтобы вставить асм инструкции можно использовать llvm::InlineAsm или MachinePass, через машинные проходы можно изменять, добавлять инструкции. Неплохой пример есть тут.

немного полезного кода для начала
Как прочесть байткод файл?
std::string file = "1.bc";
std::string ErrorInfo;
llvm::LLVMContext context;
llvm::MemoryBuffer::getFile(file.c_str(), bytecode);
llvm::Module *module = llvm::ParseBitcodeFile(bytecode.get(), context, &error);

Как сделать итерацию функций в модуле?
for (auto i = module->getFunctionList().begin(); i != module->getFunctionList().end(); ++i)
   {
       printf("Function %s",i->getName().str());
   }

Как проверить на принадлежность к какой-то инструкции?
if (llvm::isa<llvm::BranchInst>(currentInstruction))
   printf("BranchInst!");

Как заменить терминатор другой инструкцией?
llvm::BasicBlock *block = (инициализация)
block->replaceAllUsesWith(инструкция которой заменяем);

Как сделать приведение одной инструкции к другой?
llvm::Instruction* test = basicBlock->getTerminator();
llvm::BranchInst* branchInst =  llvm::dyn_cast<llvm::BranchInst>(test)

Как получить первую не phi инструкцию в базовом блоке?
llvm::Instruction *inst =  currentInstruction->getParent()->getFirstNonPHI()

Как итерировать инструкции в функции?
   for(llvm::inst_iterator i = inst_begin(function); i != inst_end(function); i++)
   {
      llvm::Instruction* inst = &*i;
   } 

Как узнать используется ли инструкция где то еще?
bool IsUsedOutsideParentBlock(llvm::Instruction* inst)
{
   for(llvm::inst::use_iterator i = inst->use_begin(); i != inst->use_end(); i++)
   {
      llvm::User* user = *i;
      if(llvm::cast<llvm::Instruction>(user)->getParent() != inst->getParent())
         return true;
   }
   return false; 
}

Как получить/изменить базовые блоки на которые ссылается InvokeInst и другие?
invokeInst->getSuccessor(0); //получаем указатель на базовый блок.
invokeInst->setSuccessor(0,basicBlock); //устанавливаем.



Ответы на вопросы


Q: Что на счет деобфускации?
A: Все зависит от вас, есть проект базирующийся на LLVM для снятия обфускации.
Q: Как сгенерировать байт код файл из исходника?
A: clang -emit-llvm -o 1.bc -c 1.c
Q: Как скомпилировать байт код?
A: clang -o 1 1.bc
Q: Как сгенерировать asm файл из LLVM IR представления?
A: llc foo.ll
Q: Как сгенерировать IR файл из исходника?
A: clang -S -emit-llvm 1.c
Q: Как скомпилировать .s файл(ассемблер)?
A: gcc -o exe 1.s
Q: Как получить obj файл из байткода?
A: llc -filetype=obj 1.bc
Q: Как получить исходник с LLVM api для cpp файла?
A: clang++ -c -emit-llvm 1.cpp -o 1.ll затем llc -march=cpp -o 1.ll.cpp 1.ll
Q: Скомпилировал clang под windows, а он не может найти заголовочные файлы, как лечить?
A: Нужно найти InitHeaderSearch.cpp и добавить необходимые пути, смотрите в сторону AddMinGWCPlusPlusIncludePaths,AddMinGW64CXXPaths.
Q: Clang скомпилированный под визуал студией работает нормально?
A: На текущий момент нет, он может компилировать только самый простой C-код.
Q: Clang с режимом оптимизации вырезает нагенерированные инструкции/функции, что делать?
A: Ваши проходы нужно встроить в clang, в этот cpp файл. Так же можно заставить компилятор думать, что добавленный нами код нужный, для этого нужно чтобы этот код обязательно использовался, в случае с функциями они должны вызываться. Для тестов можете использовать режим -O0.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+41
Комментарии11

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн