Эта статья вторая в цикле по созданию crackme под linux amd64. В этой части мы создадим исполняемый файл, в котором каждая функция будет зашифрована собственным ключом, и будет расшифровываться только на время исполнения. Процесс создания будет полностью автоматизирован, то есть при добавлении нового кода или изменении старого никаких дополнительных действий делать будет не нужно. Код всего проекта находится в репозитории на github. Первая часть. Вторая часть. Алгоритм создания такого бинарника достаточно прост, распишем его по шагам: Скомпилировать код в исполняемый файл так, чтобы каждая функция имела размер кратный 16. Это условие исходит из того, что в режиме CBC можно работать только с буферами, размер которых кратен размеру блока, то есть 16. Сгенерировать ключ и вектор инициализации для каждой функции, затем зашифровать их. После этого передать информацию о функциях (адрес и размер), а также ключи и векторы инициализации программе для генерации функций AddRoundKey и get_iv, которую я описал в предыдущей части. После данного шага исполняемый файл изменится следующим образом: Скомпилировать код движка, отвечающего за шифрование и расшифровывание функций. Он как раз использует код, сгенерированный на предыдущем шаге. Чтобы скомпилированный код движка можно было встроить в наш исполняемый файл, нужно передать компилятору флаг -fPIC, чтобы код был позиционно-независимым. Встроить код движка в исполняемый файл, вставить в начало каждой функции инструкцию call для вызова кода движка. Здесь появляется проблема, связанная с тем, что мы не можем менять размер функции, потому что тогда у других функций поменяются адреса, что может привести к некорректной работе инструкций, которые обращаются к памяти. Поэтому мы будем заменять первые 5 байт каждой функции на инструкцию call, оригинальные же 5 байт положим в хэш-таблицу и при расшифровывании будем копировать их из неё обратно в начало функции. После данного шага наш исполняемый файл будет выглядеть так: Как я писал выше, нам нужно скомпилировать код так, чтобы размеры всех функций были кратны 16. Для начала напишем простой код на си, который и будем компилировать. Мы не будем использовать стандартную библиотеку, так как иначе у нас будет слишком много функций, а как мы выяснили в прошлой главе, чем больше функций, тем больше размер AddRoundKey, тем медленнее исполнение и компиляция. Сам код представлен ниже: start.s: crackme.c: Есть один простой способ, как сделать размеры всех функций кратными 16 — поместить каждую функцию в отдельную секцию и задать в скрипте компоновщика выравнивание для каждой такой секции. Первая задача решается передачей флага -ffunction-sections компилятору gcc, вторая — генерацией скрипта компоновщика. Для генерации скрипта я решил использовать стандартные консольные утилиты — readelf и awk. Сначала с помощью readelf получаем из объектных файлов имена секций, затем с помощью awk на их основе генерируем скрипт компоновщика. Текст программы на awk приведён ниже. create_linker_script.awk: Мы проверяем, начинается ли второй или третий столбец с .text, если нет, то пропускаем строку, иначе печатаем название секции вместе с ALIGN(16). Проверка двух столбцов связана с тем, что readelf выравнивает номера секций, для секций с однозначными номерами имя будет в третьем столбце, а для секций с двухзначными номерами - во втором. Пример вывода readelf для лучшего понимания: Почему мы проверяем, что название секции начинается с .text? Потому что при передаче флага -ffunction-sections gcc кладёт каждую функцию в свою секцию с именем, соответствующим шаблону .text.имя_функции, именно поэтому ранее мы явно указали .section .text._start для функции _start. Теперь напишем Makefile для сборки исполняемого файла: Как можно видеть по флагам компиляции - мы собираем статический исполняемый файл без линковки со стандартной библиотекой. Стоит также обратить внимание на то, что readelf вызывается с флагом --wide, потому что иначе имена секций могут выводится не полностью. Для наглядности далее приведён текст сгенерированного скрипта компоновщика: Создадим директорию ./utils/patcher, в которой будет хранится код программы, которая будет модифицировать наш бинарник, и положим туда файлы aes.h, aes.c, являющиеся модифицированными файлами из проекта tiny-AES-c. Для лучшего понимания кода, далее приведена сигнатура функции, которою мы будем использовать для шифрования: Теперь приступим к созданию шифровальщика. Начнём с написания небольшой структуры для хранения информации об ELF файле и функции, которая считывает ELF файл в данную структуру: Теперь напишем процедуру для шифрования одной функции: Далее создадим процедуру, которая будет шифровать все функции: Приступим к процедуре generate_add_round_key. Программа gen-add-round-key принимает следующие аргументы командной строки: -a, в котором передаётся адрес функции, и -s, в котором передаётся размер функции. Вектор инициализации и ключ программа считывает из стандартного ввода. Код процедуры generate_add_round_key имеет вид: После того, как мы зашифровали все функции, нам надо сохранить новый ELF файл. За это отвечает процедура write_encrypted_exec: Кроме генерации AddRoundKey нам нужно записать в файл table.inc размер хэш-таблицы, в которой будут храниться первые пять байт функций, которые мы будем заменять на инструкцию call. В директории utils/patcher лежат файлы funcs_table.h и funcs_table.c, в которых имплементирована простая хэш-таблица с открытой адресацией, интерфейс которой выглядит так: Процедура для записи размера хэш-таблицы выглядит следующим образом: Наконец рассмотрим функцию main: Makefile для сборки patcher довольно простой, поэтому здесь не приведён. Сейчас нам нужно интегрировать patcher в процесс сборки, для начала положим в utils код программы gen-add-round-key и создадим директорию engine, в которой будет лежать код движка расшифровки функций. Именно в неё мы будем сохранять код функции AddRoundKey и файл table.inc с размером хэш-таблицы. Структура проекта на данном этапе выглядит так: Теперь добавим в наш Makefile вызов patcher: Теперь приступим к созданию движка. Алгоритм его работы достаточно прост: Если функция, которую мы расшифровываем (далее caller), была вызвана из другой функции (далее prev_caller), шифруем prev_caller, иначе сразу переходим к шагу 2. Это условие нужно, потому что мы также шифруем функцию _start, которая ни откуда не вызывается. Сохраняем адрес возврата caller, т.е. адрес, с которого продолжится исполнение после выхода из caller. Расшифровываем caller. Вызываем caller и запоминаем возвращаемое значение. Шифруем caller. Если на шаге 1 мы зашифровали prev_caller, расшифровываем его обратно. Кладём адрес возврата в стек, так чтобы после инструкции ret исполнение продолжилось с него. Кладём в регистр rax значение, которое мы запомнили на 4 шаге и выходим из движка. Переходим к реализации. Создаём в директории engine файл engine.c со следующим содержимым: Далее идут все функции из tiny-AES-c нужные для работы AES_CBC_decrypt_buffer и AES_CBC_encrypt_buffer у большей части из них поменялось лишь определение: я заменил static на INLINE static inline, это нужно, чтобы в результате движок был одной большой функцией, в которой чёрт ногу сломит. Далее разберём некоторые существенные изменения в функциях шифрования. У функций InvCipher и Cipher исчез за ненадобностью параметр const uint8_t* RoundKey: У функций AES_CBC_encrypt_buffer и AES_CBC_decrypt_buffer ушли за ненадобностью параметры struct AES_ctx *ctx и size_t length: Теперь перейдём к реализации самого движка. Для начала рассмотрим вспомогательные функции для работы с хэш-таблицей: Наконец, рассмотрим код самого движка: Рассмотрим строчку, в которой мы получаем адрес возврата: На иллюстрации показано состояние стека для каждой из вызываемых функций, как можно видеть нужный нам адрес возврата находится именно по адресу rbp + 16. Однако, чтобы эта инструкция работала так, как нам нужно, необходимо компилировать движок с флагом -fno-omit-frame-pointer, чтобы gcc оставлял ниже приведённые инструкции: Код движка на этом завершён, теперь добавим в Makefile цели для его компиляции. Но для начала напишем простой скрипт компоновщика, чтобы код и данные лежали в одной секции, и мы могли легко их вырезать и вставить в наш исполняемый файл. Создадим файл engine/linker.ld со следующим содержимым: Теперь допишем пару инструкций в наш Makefile: На этом рассмотрение работы движка окончено, переходим к четвёртому шагу алгоритма. Здесь будет описана данная техника встраивания собственного кода в уже готовый бинарник, если вы с ней знакомы, смело пропускайте это описание. Эта техника так называется, потому что встраиваемый код будет располагаться до основного кода исполняемого файла, таким образом мы увеличиваем сегмент с кодом (text segment) в обратную сторону: Как видно из иллюстрации, при таком встраивании адреса всех функций и данных остаются неизменными. Также можно заменить, что мы увеличиваем размер сегмента на размер нашего встраиваемого кода, округлённый до размера страницы. Дело в том, что адрес начала сегмента должен быть кратен размеру страницы (4096 байт), если наш код не кратен ему, мы вынуждены заполнить оставшееся пространство нулями (padding на иллюстрации). Теперь можно описать алгоритм: Вычисляем округлённую до размера страницы длину встраиваемого кода, далее rounded_size. Проходимся по всем заголовкам секций, если секция расположена после сегмента с кодом (shdr->sh_offset >= text_phdr.p_offset + text_phdr.p_filesz), увеличиваем её смещение на rounded_size: shdr->sh_offset += round_size. Находим заголовок .text секции, уменьшаем адрес на rounded_size и увеличиваем размер на rounded_size. Находим заголовок исполняемого сегмента, уменьшаем p_vaddr и p_paddr на rounded_size, увеличиваем p_filesz и p_memsz на rounded_size. Проходимся по всем заголовкам программы, если сегмент расположен после исполняемого, увеличиваем его смещение относительно начала файла (p_offset) на rounded_size. Сохраняем на диск новый ELF файл:  сначала пишем все данные до заголовков программы включительно, потом пишем встраиваемый код, далее нули, чтобы сделать размер кратным размеру страницы, наконец пишем все данные после заголовков программы из оригинального файла. Мы подошли к финальному шагу, для начала рассмотрим алгоритм встраивания движка: Вычисляем адрес функции движка в нашем исполняемом файле. Создаём пустую хэш-таблицу, в которой будут лежать первые пять байт функций. Проходимся по всем функциям, добавляем первые пять байт в хэш-таблицу, заменяем их на инструкцию call с вызовом функции движка, для этого используем адрес из первого шага. Находим символ funcs_table в движке, копируем в него нашу хэш-таблицу. Встраиваем код движка в наш файл с помощью reverse text infection. Встраиванием будет заниматься patcher, поэтому пишем код в utils/patcher/patcher.c. Для начала рассмотрим две вспомогательные функции: А теперь приступим к основному коду: Теперь добавим в main новый аргумент командной строки -p: Осталось добавить новую цель в Makefile и готово: Запускаем наш crackme: ./crackme "Hello Habr!?" и ничего не происходит, как же так? Вспоминаем эту иллюстрацию и понимаем, что в функции _start нужно поменять код, отвечающий за передачу argc и argv: Компилируем всё снова, и оно начинает работать. В этой статье я показал простой способ шифрования всех функций в исполняемом файле с последующей расшифровкой только на время исполнения. Очевидно, это самый простой и не самый лучший вариант, потому что в итоге из всех функций путь ведёт в одно и то же место - в движок. Возможно, когда-нибудь позже я придумаю, что посложнее, но пока живём с тем, что имеем. В заключительной третьей статье речь пойдёт про добавление наномитов в наш crackme для усложнения отладки. Надеюсь, вам было весело.