Во время реверс-инжиниринга есть два пути. Первый — трушный и хардовый: использовать GDB и там отслеживать цепочку вызовов, потом реверсить в IDA Pro. Однако, когда есть исходный код, можно облегчить себе задачу и прибегнуть к Eclipse-CDT. Мы начнем реверс с использования Eclipse-CDT, потом перейдем GDB и закончим IDA Pro.
Еще по теме: Пример реверс инжиниринга с использованием Ghidra
Пример реверса-инжиниринга в Eclipse-CDT
Eclipse-CDT — это среда разработки на C и C++, основанная на платформе Eclipse. Среди ее возможностей — настройка управляемой сборки для разных тулчейнов, стандартная сборка make, навигация по исходному коду, разнообразные инструменты исследования кода, инструменты рефакторинга и генерации кода, а также инструменты для визуальной отладки, включая просмотр памяти, регистров и дизассемблера.
Так как программа написана на Java, необходимо скачать Java SDK:
1 |
sudo apt install default-jdk |
Скачаем программу и распакуем ее:
1 2 |
wget https://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/2021-03/R/eclipse-cpp-2021-03-R-linux-gtk-x86_64.tar.gz](https://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/2021-03/R/eclipse-cpp-2021-03-R-linux-gtk-x86_64.tar.gz tar -xzvf eclipse-cpp-2021-03-R-linux-gtk-x86_64.tar.gz |
В итоге распаковки получаем набор файлов, среди которых будет исполняемый файл eclipse. Запускаем его.
Нас встречает вот такое, вполне информативное окошко.
В меню выбираем File → Import, после этого задаем язык программирования, в нашем случае это C/C++. Выбираем пункт Existing Code as Makefile Project, поскольку у нас есть makefile.
Makefile — это файл, который используется для автоматизации сборки и компилирования программ. Он содержит все необходимые проверки, настройки и команды.
Далее называем проект, выбираем директорию с программой, в которой присутствует makefile, и указываем компилятор — Linux GCC.
Следующий шаг — подготовка параметров отладки. Переходим на вкладку Run —> Debug Configurations.
Далее выбираем C/C++ Application, а на вкладке Main — нужный проект. На вкладке Arguments вставим путь до нашего краш‑файла.
Теперь жмем Debug и попадаем в главное окно.
В левом списке можно увидеть ошибку Segmentation fault, что означает потенциальное переполнение буфера. Также мы видим места в коде, где была обнаружена эта ошибка.
Теперь нужно понять, почему возникает ошибка и как это можно проэксплуатировать.
Идем копаться в интернете и узнаём, что эти ошибки потенциально могут быть связаны или с переполнением кучи (CVE-2009-3895), или с обычным отказом в обслуживании, которое толком ни к чему не приводит (CVE-2012-2836). Пока что нельзя с уверенностью сказать, к какой именно CVE относятся эти краши, поэтому продолжим изучение.
Просмотрев потенциально уязвимые места, которые выявила Eclipse-CDT, я обнаружил функцию exif_data_load_data_thumbnail().
Однако определить, почему упала программа и что в это время происходило с памятью, в Eclipse-CDT у меня не получилось. Поэтому переходим к GDB.
Пример реверса-инжиниринга используя GDB
GDB пригодится нам, чтобы определить цепочку вызовов функций (см. также Инструкция по использованию отладчика GDB).
Загружаем программу в GDB и запускаем с указанием краш‑файла:
1 2 |
gdb -q exif run crash.jpeg |
Отловить место падения нам поможет команда bt.
bt — это команда в GDB, которая отображает стек вызовов для текущего потока. Стек вызовов содержит информацию о функциях, вызываемых в текущей точке программы. GDB выведет список, начиная с самой высокоуровневой функции и заканчивая функцией, в которой произошла ошибка.
Программа упала в этой функции, причем внутри этой функции, видимо, происходит копирование чего‑то в кучу. Этот факт подтверждает участок кода, на котором остановился отладчик, а также регистры.
На самом деле понятно, почему произошел краш. Регистр rsi указывает на пустую ячейку динамической памяти. Так как такого чанка не существует, а программа пытается перенести его в xmm0, возникает ошибка. Вот этот участок кода доказывает нашу догадку:
1 |
0x7ffff7cc498d <__memmove_sse2_unaligned_erms+13> movups xmm0, xmmword ptr [rsi] |
Пример реверса-инжиниринга в IDA Pro
Подключаем к делу IDA Pro, чтобы понять, что происходило до момента вызова уязвимой функции. Мы можем полностью восстановить цепочку вызовов и определить место падения программы. Давай пройдемся по всем сработавшим функциям:
- main() загружает файл, а затем вызывает exif_loader_get_data() для генерации ExifData;
- exif_loader_get_data() запрашивает место для ExifData и затем вызывает exif_data_load_data() для чтения потока байтов из адреса loader->buf длиной loader->bytes_read. Проще говоря, выделяется место для чтения файла;
- exif_data_load_data() проверяет заголовок, после чего вызывает exif_data_load_data_content();
- exif_data_load_data_content() считывает количество записей, и для каждой выполняется маршрутизация по тегу. Если тег — EXIF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, то есть 0x0202, то считывается длина миниатюры. Если значение thumbnail_offset уже установлено, то вызывается exif_data_load_data_thumbnail, чтобы разобрать миниатюры;
- exif_data_load_data_thumbnail() копирует данные в динамическую память и дальше выводит на экран.
Разберем подробнее последнюю функцию.
В ее начале идет проверка размера и смещения, если их сумма больше числа ds, то программа завершается.
После этого проверяется память: если она выделена, то очищается.
Дальше выделяется новая память и проверяется, выделена ли она.
Последний блок, который у меня выделен красным, и есть место, в котором программа падает.
У функции void memcpy(void *dest, const void *src, size_t n) три аргумента:
- *dst — указатель на область памяти, в которую будут скопированы данные;
- src — указатель на область памяти, из которой будут скопированы данные;
- n — количество байтов, которые нужно скопировать.
В контексте исследуемой программы для memcpy() первый аргумент — данные, второй аргумент — смещение, третий аргумент — размер.
Благодаря GDB я выяснил, что регистр rdx хранит в себе размер, регистр rsi — указатель на область памяти, из которой надо копировать, а смещение хранится в регистре rbp. Причем адрес rsi получается путем сложения смещения и начала адреса кучи, в которую был загружен файл. Это подтверждает инструкция lea rsi, [r13+rbp+0] и регистры.
В регистре rbp хранится значение 0x00000000FFFFFEC3, которое является переменной offset. В регистре r13 — у казатель на кучу 0x000055F902C7C9A6. После сложения получаем значение 0x55fa02c7c869, которое хранится в rsi и указывает в никуда. Собственно, что и требовалось доказать.
Продебажив программу еще несколько раз, я понял, что все эти значения берутся из самого файла, а не вычисляются. Я открыл краш‑файл в HEX-редакторе и по сигнатуре нашел значение для переменной offset 0x00000000FFFFFEC3 и размер 0x8cb.
То есть если изменить эти значения и загрузить в программу, то можно попасть в какой‑то другой чанк и перетереть его. Однако, исследовав память, я выяснил, что никуда это не приводит и проэксплуатировать этот баг не получится. Поэтому, к сожалению, до полного роадмэпа взлома мы в этот раз не дотянули.
Заключение
В этой статье я постарался показать шаги реверсера на реальном кейсе. К великому сожалению, на практике бывает такое, что уязвимость не получается проэксплуатировать и баг приводит только к отказу в обслуживании. Однако в процессе мы научились искать и собирать таргет, фаззить исполняемый файл, пользоваться статическим анализатором кода и исследовать уязвимое место в бинаре.
ПОЛЕЗНЫЕ ССЫЛКИ:
- Реверс прошивок роутера
- Лучшие программы для реверс-инжиниринга
- Обзор фреймворка для реверс-инжиниринга Ghidra