Реверс инжиниринг для самых маленьких на практике

в 10:58, , рубрики: ctf, disassembler, ida, реверс-инжиниринг

Скомпилированное приложение является «чёрным ящиком». Чтобы туда заглянуть, восстановить алгоритм работы применяется реверс‑инжиниринг. Это непростой навык с высоким порогом входа. В статье мы попробуем взять дизассемблер, несложную задачку и пойдём в бой. Материал будет полезен тем, кому хочется с чего-то начать и погрузиться в тему реверса.

В ходе нашего погружения разберем, какие инструменты использовать, с какой стороны подходить к решению подобных задач, разберём различные теоретические моменты. Для углублённого изучения будут ссылки на дополнительную литературу.

Задача

В качестве примера для разбора мы взяли задачу с платформы Codeby Games из раздела Реверс-инжиниринг – файлы. Называется: "Шифрование или что-то в этом роде, не знаю". Чтобы получить доступ к платформе – нужно зарегистрироваться. К самой платформе мы не имеем никакого отношения.

Решением задачи является строка (флаг), которую будем добывать.
Скачиваем бинарный файл “task3.exe” из раздела “Файлы” и приступаем.

В статье будут применяться бесплатные инструменты:

Первичный запуск

Все исполняемые файлы, скачанные из интернета, рекомендуем всегда сначала запускать внутри виртуальной машины. Это позволит первично обезопасить себя от вредоносной активности незнакомого приложения.

Также перед запуском рекомендуем анализировать исполняемые файлы специальными утилитами на предмет вредоносности. В случае установки анализаторов локально делать это также необходимо внутри виртуальной машины, так как некоторые из них автоматически запускают образцы, которые были приняты на анализ.

Рекомендуем анализаторы
  • VirusTotal (Есть бесплатная версия)

  • Hybrid Analysis (Есть бесплатная версия)

  • AnyRun (Есть бесплатная версия)

  • PEStudio (Есть бесплатная версия)

  • Capa (Бесплатно)

Современное вредоносное ПО часто взаимодействует с сетью как для доставки очередной "стадии вредоносности", так и для распространения в локальной сети. Рекомендуем либо целиком отключать виртуальную машину от сети, либо, если для исследуемого приложения требуется подключение в интернет, использовать VPN изнутри виртуальной машины. Это не даст образцу распространиться в сети вашей хостовой машины.

Теперь, когда обо всем предупредили, можно приступать к работе.

Запускаем приложение!

Перед нами классическое консольное приложение, которое запрашивает строку:

Первичный запуск

Первичный запуск

Пробуем ввести флаг и убеждаемся, что удача не на нашей стороне:

Попытка ввода флага

Попытка ввода флага

Но, теперь мы знаем, что проверка флага каким-то образом связана со строками INPUT YOUR FLAG: и INCORRECT!!!.

Запускаем дизассемблер IDA и открываем исполняемый файл. IDA предлагает использовать оставленную автором отладочную информацию:

Реверс инжиниринг для самых маленьких на практике - 3

Наличие отладочной информации всегда сильно упрощает дальнейший анализ.

Немного об отладочной информации

На сайте Касперского отладочные символы определяются, как набор специальных символов, который генерируется при компиляции программы и содержит сведения о расположении переменных и функций в итоговом бинарном файле, а также другую служебную информацию. Такой набор данных позволяет производить пошаговую отладку программы с исходным кодом или исследовать сторонний код.

Существует множество форматов отладочных символов. Чаще всего вам придется сталкиваться с этими:

  • PDB – Program Data Base, используется в продуктах, собранных с использованием компиляторов, разработанных компанией Microsoft.

  • DWARF (Debugging With Arbitrary Record Format), STABS (Symbol Table and String Table), COFF (Common Object File Format), ELF (Executable and Linkable Format) используются в основном в продуктах, собранных с использованием open-source компиляторов.

  • Mach-O – Mach Objecton macOS and iOS, используется только в продуктах, собранных различными компиляторами, для целевых ОС MacOs или iOS.

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

Не стоит пугаться такому списку, потому что суть одна и та же. Разберем чуть подробнее, что хранят в себе DWARF и PDB – самые часто используемые форматы.

Информация DWARF обычно хранится в отдельном файле, который затем связывается (линкуется) с исполняемым файлом.

Основная информация, хранящаяся в файле DWARF:

  • имена файлов исходного кода и номера строк

  • информацию о глобальных и локальных переменных и их типах

  • информацию о функциях и подфункциях

  • граф вызовов программы

Информация PDB обычно также хранится в отдельном файле, который затем связывается с исполняемым файлом.

Основная информация, хранящаяся в файле PDB:

  • информацию об именах и типах переменных

  • информацию о функциях

  • информацию о местоположении строк кода

  • информацию о стеке вызовов

О том, как управлять своей жизнью, и самому включать отладочную информацию на примере отладчика gdb и исполняемого ELF-файла, советуем прочитать эти две статьи:

В Visual Studio отладочная информация включается при наличии флага линковщика /DEBUG. Отключить можно в настройках проекта, поставив полю Linker -> Debugging -> Generate debug info значение NO.

Также в настройках линковщика можно отключить создание pdb файла, очистив поле Linker -> Debugging -> Generate Program Database File.

Если не отключить создание PDB-файла, в собранном исполняемом файле в секции данных будет лежать путь до этого файла.

Копаем глубже:

Больше о том, как исследователи используют отладочную информацию можно почитать в статье Utilizing Debugging Information of Applications in Memory Forensics.

Крис Касперски в статье Защита игр от взлома рекомендовал никогда не оставлять отладочную информацию в скомпилированной программе.

Начинаем анализировать

Теперь, когда есть поверхностное знание, что такое отладочная информация начнем анализ. Для этого откроем IDA, с использованием отладочной информации. Перед нами функция main.

Функция main

Функция main

С отладочной информацией дизассемблер сразу понял, что перед нами не набор бессмысленных переменных. По модификаторам <Тип данных> ptrпонимаем, что flag, check и i это указатели на данные определенных типов.

Спускаемся ниже по коду к адресу 401598. По синтаксису команды mov [rbp+check+4], 7ABh видим, что в память по адресам check, check+4, check+8 и т.д. помещаются какие-то числа.

Копаем глубже

Больше о квадратных скобках в операндах команд и вообще о типах адресации можно почитать в статье Операнды в языке ассемблера.

Так как данные помещаются в check+N по регулярному смещению 4, делаем вывод, что check является указателем на массив. Этих команд 25 штук, поэтому можем предположить, что массив хранит в себе 25 элементов длиной 4 байта. Если это верно, то далее, когда массив будет использоваться в циклах, скорее всего операции будут проводиться с двадцатью пятью элементами.

Отвлечемся немного и ответим на пару вопросов.

А почему каждая строка начинается с “.text”?

Исполняемый файл имеет множество участков, называемых секциями. Они имеют различное назначение. Код в исполняемых файлах хранится в секции .text. У этой секции есть специальные права на исполнение. Чтобы в этом убедиться, откроем исполняемый файл в редакторе CFF Explorer.

Загрузив файл в CFF Explorer, видим вкладки в левой части рабочего пространства:

Реверс инжиниринг для самых маленьких на практике - 5

Сначала идут заголовки, потом таблицы: импорта, экспорта и TLS. Ниже различные модули для работы с исполняемыми файлами, включенные в CFF Explorer.

Информация о секциях хранится в заголовке Section Headers. Откроем его:

Реверс инжиниринг для самых маленьких на практике - 6
Копаем глубже

О структуре исполняемых файлов Windows подробне прочитать в статье Часть 1. Обзор PE формата и других статьях цикла.
Статьи покрывают: из чего состоит исполняемый файл Windows, как искать секции, что такое импортируемые и экспортируемые функции и так далее многое другое.
А выжимка есть на хабре PE (Portable Executable): На странных берегах. Для начала ее вполне хватит.

Нас интересует поле Characteristics. Здесь содержится информация о данных, хранящихся в секции. У секции .text Characteristics имеет значение 60500060. Старшая 6 говорит о том, что у секции есть характеристики:

  • IMAGE_SCN_MEM_READ и IMAGE_SCN_MEM_EXECUTE – секция может быть исполнена как код

  • IMAGE_SCN_MEM_READ – секция может быть прочитана

Копаем глубже

Все возможные характеристики можно посмотреть в документации Microsoft.

А почему после .text следует “040…”?

У исполняемого файла есть параметр ImageBase – смещение относительно адреса в памяти, куда была загружена программа. Обычно это 0x40000. Этот параметр хранится в заголовке Optional Header:

Реверс инжиниринг для самых маленьких на практике - 7

Так почему же main() не начинается с 400000? Потому что это адрес, где лежит весь исполняемый файл, в том числе заголовки. Заголовки находятся как раз над сегментом кода. Поэтому нас интересуют еще два поля Optional Header:

  • BaseOfCode – смещение относительно начала загруженного исполняемого файла (ImageBase). Здесь начинается сегмент кода text. В этой части размещается код начальной работы процесса: проверка сигнатуры исполняемого файла, получение аргументов main, StartupInfo для процесса и так далее.

  • AddressOfEntryPoint – смещение относительно ImageBase, откуда начинает исполняться программа. Здесь начинается функция mainCRTStartup, которая выполняется до человеческого main и является точкой входа библиотеки времени выполнения Си (C Runtime). Помимо прочего, здесь инициализируются статические элементы кода, а так же переменные argc, argv и envp. А еще, в этой функции помимо всего остального, инициализируются статические элементы (с пометкой static), если они используются в программе.

В нашем случае AddressOfEntryPoint равен 1510. Посмотрим на него в IDA. В IDA этот адрес будет равен ImageBase+AddressOfEntryPoint, т.е. 401510:

Вызов __tmainCRTStartup

Вызов __tmainCRTStartup

Здесь как раз видим вызов __tmainCRTStartup, находящийся выше AddressOfEntryPoint, в котором инициализируется StartupInfo.

Дважды нажав на эту функцию, перейдем внутрь нее и немного пролистаем до момента, пока не увидим вызов функции main (call main):

Вызов main

Вызов main

Здесь же видим, что в функцию main передаются привычные нам аргументы: argc, argv и envp. Это происходит всегда, даже когда разработчик приложения напрямую никак не взаимодействует с этими переменными в исходном коде.

Теперь вы знаете, как найти функцию main самостоятельно и как она вызывается на самом деле, а так же что означают числа рядом с .text в дизассемблере.

Если отладочной информации нет

Давайте посмотрим, как будет выглядеть код без отладочной информаци. Откроем экземпляр IDA без использования отладочной информации. Увидим функцию main, только в менее понятном виде.

Дизассемблер не заметил, что перед ним массив. Поэтому нам нужно методом пристального взгляда определить границы массива: переменную, начиная с которой переменные размещаются регулярно с каким-то фиксированным смещением. В нашем случае это var_70:

Ищем массив

Ищем массив

Затем укажем дизассемблеру наличие массива. Для этого дважды кликнем по первой переменной, с которой начинается массив. Перед нами откроется окно редактирования переменных стека:

Реверс инжиниринг для самых маленьких на практике - 11

Далее выделим все необходимые переменные и с помощью горячей клавиши '*' создадим массив:

Реверс инжиниринг для самых маленьких на практике - 12

Затем переименуем созданный массив с помощью горячей клавиши 'N':

Реверс инжиниринг для самых маленьких на практике - 13

В результате этих манипуляций, код будет выглядеть практически так же красиво, как и при наличии отладочной информации. Но это потребовало от нас определенных усилий.

Реверс инжиниринг для самых маленьких на практике - 14

В реверсе очень важно уметь находить подобные структуры, это сильно помогает при дальнейшем анализе.

Наконец-то реверс

Вернемся к коду с отладочной информацией и продолжим.

Перейдем в раздел Strings (shift+F12) и видим уже знакомую строку, которая приглашает ввести флаг:

Строки

Строки

Дважды кликаем по ней в открывшемся окне и попадаем в секцию, где хранятся инициализированные данные приложения, предназначенные только для чтения – .rdata:

.rdata

.rdata

Чтобы посмотреть, где используется текущий адрес, жмем Ctrl+X и получаем список перекрестных ссылок. Механизм называется XRefs или перекрестные ссылки.

Переходим по единственной перекрестной ссылке:

Перекрестные ссылки

Перекрестные ссылки

Перейдя по ссылке, видим, что строка используется чуть ниже заполнения массива check (адрес 401640) в функции puts (вывод в стандартный поток вывода, в данном случае – консоль):

Реверс инжиниринг для самых маленьких на практике - 18

Ниже (адрес 40165D) замечаем вызов scanf (форматированный ввод из стандартного потока ввода) с параметрами %s и указателем flag. Значит, flag – строка, вводимая пользователем.

Спускаемся еще ниже и видим зануление значения по адресу i (адрес 401662) и на следующей строке безусловный переход на метку loc_4016b2. Метка – название для адреса в секции кода. Переходим по этой метке, дважды нажав на loc_4016b2.

Здесь (адрес 4016B2) мы видим сравнение i с числом 24 (18h), условный переход на метку loc_40166B, который сработает, если i будет меньше или равно 24. Если i будет больше 24, переход не произойдет и будет вызвана функция puts(“GOOD JOB, HACKER”):

Реверс инжиниринг для самых маленьких на практике - 19

Перейдя по метке loc_40166B, видим, что в этом участке кода производятся манипуляции с массивами flag (адрес 401670) и check (адрес 401691):

Реверс инжиниринг для самых маленьких на практике - 20

Первым делом значение i помещается в регистр eax и расширяется до rax командой cdqe (адреса 40166B и 40166E). Физически eax – младшая половина регистра rax:

Схема регистров

Схема регистров
Копаем глубже

Подробнее о регистрах можно узнать в статье Регистры процессора.

Строкой ниже (адрес 401670) в eax помещается значение, находящееся по адресу flag+raxc помощью команды movzx (rax в два раза больше eax, поэтому используется movzx, где zx означается, что данные, находящиеся в eaxдополняются нулями). Сопоставив эту строку с двумя предыдущими, понимаем, что в rax помещается i-ый элемент массива flag.

По адресу 401678 от регистра rax остается только младший байт, и расширяется до знакового четырех байтового числа командой movsx eax, al. Позже будет понятно зачем.

Далее для содержимого eax вызывается функция enc (строки 401682-401687). Параметр функции передается в регистре ecx. Кликнув дважды enc попадаем в ее тело:

Функция enc

Функция enc

Благодаря отладочным символам, видим, что функция возвращает тип int –четырёхбайтовое знаковое число и имеет соглашение о вызовах cdecl, а значит ее результат, возвращаемый в return будет храниться в регистре eax. При этом, параметр letter, передаваемый в функцию – четырёхбайтовое знаковое целое. Именно поэтому ранее мы видели расширение до четырёхбайтового целого.

Копаем глубже

На эту тему на хабре есть статья Соглашения о вызовах.

Расширение до четырех байт нужно для проверки модифицированного flag[i], имеющего однобайтовый тип с некоторым четырёхбайтовым элементом check.

В функции видим, что ecx сохраняется в переменную letter (адрес 401574), а затем, строкой ниже, значение переменной сохраняется в eax.

Следующей строкой со значением регистра eax производят операцию xor eax, 0BADh – побитового исключающего ИЛИ с числом 0x0BAD.

На этом тело функции заканчивается, значит, результатом ее исполнения является:

xor(flag[i], 0x0BAD)

Вернемся к коду, вызывающему эту функцию:

Реверс инжиниринг для самых маленьких на практике - 23

После вызова функции enc видим (адрес 401682), что результат ее выполнения, хранящийся в eax, увеличивается на 0x24 и для полученного значения снова вызывается функция enc.

В результате второго выполнения enc со значением flag[i]произведено действие:

xor(xor(flag[i], 0x0BAD+0x24),0x0BAD)

Разберемся далее построчно:

  1. 40168C: результат второго выполнения enc сохраняется в edx

  2. 40168E и 401691: значение i помещается в rax.

  3. 401693: в eax сохраняется значение check[rax*4], что означает сохранение в eax check[i*4]

  4. 401697-401699: полученное значение для i-го элемента флага сравнивается с check[i*4], и если значения равны, происходит переход на метку loc_4016AE

Переходим на метку и видим (адрес 4016AE) увеличение значения i на единицу:

Реверс инжиниринг для самых маленьких на практике - 24

После увеличения значения i снова попадаем на сравнение с 24. Так мы понимаем, что попали в цикл.

Подтвердить это предположение можно перейдя в графическое представление кода, нажав на пробел. Метка, на которой мы сейчас находимся на скриншоте подсвечена желтым:

Цикл в IDA

Цикл в IDA

В IDA зеленые стрелки означают верный результат при сравнении, красные – неверный, синие – безусловный переход (может быть как признаком команды jmp, так и вовсе отсутствия команды j*).

Обратив внимание на условный переход jle на метку loc_40166B, понимаем – чтобы на нее не переходить и добиться вывода GOOD JOB, HACKER, необходимо пройти целиком весь цикл.

Алгоритм проверки флага

В результате обратной разработки функции проверки введенного флага, имеем уравнение, в котором неизвестным являетсяflag[i]:

(flag[i] ^ 0x0BAD + 36) ^ 0x0BAD = check[i]

Так как xor является обратимой функцией, получим flag[i] из check[i]:

int flag[i] = (check[i] ^ 0x0BAD – 36) ^ 0x0BAD

Далее остается только взять младший байт от flag[i]. Возьмем значения элементов массива из дизассемблированного кода и напишем простую программу на C:

int main()
{
	
	int check[25];
	char flag[25];
	int i;
	check[0] = 1983;
	check[1] = 1963;
	check[2] = 1952;
	check[3] = 1953;
	check[4] = 1982;
	check[5] = 1973;
	check[6] = 87;
	check[7] = 70;
	check[8] = 1969;
	check[9] = 1967;
	check[10] = 80;
	check[11] = 1979;
	check[12] = 93;
	check[13] = 1979;
	check[14] = 1967;
	check[15] = 109;
	check[16] = 73;
	check[17] = 1964;
	check[18] = 72;
	check[19] = 111;
	check[20] = 1979;
	check[21] = 84;
	check[22] = 108;
	check[23] = 1966;
	check[24] = 89;
	for (int i = 0; i <= 24; i++)
	{
		flag[i] = (char)(((check[i] ^ 0x0BAD) - 36) ^ 0x0BAD);
	}
	printf(flag);
}

В результате выполнения получаем флаг, который вставляем в консоль и убеждаемся в его правильности.

Реверс инжиниринг для самых маленьких на практике - 26

В заключение скажем, что можно было так не страдать, сразу нажать волшебную кнопку F5, декомпилировать код и получить что-то вроде этого:

Реверс инжиниринг для самых маленьких на практике - 27

Однако, понимания в реверсе это не добавит. IDA не всегда декомпилирует во что-то читабельное (особенно при отсутствии отладочной информации), и все равно нужно сидеть над ассемблером и изучать, кто на ком стоял.

Заключение

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

Автор:
DevFM

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js