Реверс-инжиниринг — это сканворд. Как я впервые нормально понял Ghidra. Часть 2
Что оказалось внутри функции на 500 строк, где аналогия со сканвордом ломается, минимальный маршрут по Ghidra для новичка и набор идиом, которые стоит начать узнавать.В первой части — стена из local_117c и FUN_00405050, строки, xrefs, агрессивное переименование, динамический буфер, byteswap, TLV и init/update/final. Здесь — что из этого в итоге собралось и какие выводы я бы забрал с собой.
Что получилось в итоге
В начале у меня была функция на 500+ строк, которая выглядела как фарш: local_1098, FUN_0040bf40, DAT_00415c30, указатели, какие-то условия, какие-то переходы.
В итоге функция оказалась довольно обычным пайплайном: сначала она ищет файл лицензии — через SG_LIC_PATH, абсолютный путь или подъём по каталогам; потом читает его целиком в буфер; потом разбирает заголовок и TLV-payload; потом собирает ключ, считает MAC-like значение и сравнивает его с тем, что лежит в файле. Если после payload остаётся хвост — проверяет ещё и его. Онли инженерная процедура.
найти файл ─▶ прочитать ─▶ заголовок ─▶ TLV ─▶ ключ ─▶ MAC ─▶ сравнение
И вот если написать это списком, звучит просто. Ну даже скучно!
Но прикол в том, что в начале этого списка не было. Вообще. Был просто фарш. Он появился из маленьких шагов:
нашёл строку — перешёл по xref — заглянул в соседнюю функцию — переименовал — увидел импорт — узнал идиому — переименовал ещё — нашёл byteswap — предположил заголовок — увидел TLV — узнал init/update/final — проверил гипотезу по сравнению — уточнил имена.
Ура!
Именно поэтому реверс для меня стал похож на сканворд. Потому что каждое подтверждённое место ограничивает соседние.
Почему не надо пытаться понять всё
Тут ещё важный психологический момент: когда только заходишь в реверс, кажется, что ты должен понять всё: каждую переменную, каждую ветку, флаг и байт.
Ну иначе как будто не считается. Но это ловушка.
В реальности у реверса нет края. В сканворде сетка конечная и у неё есть все ответы. А в бинарнике могут быть тысячи функций, куча веток, странные условия, мёртвый код, старые совместимости, оптимизации компилятора и всё вот это счастье.
Нужно понимать достаточно для задачи.
Если задача — описать формат файла, то можно не разбирать весь остальной модуль. Если задача — понять проверку целостности, то не обязательно идеально назвать каждое поле лицензии. Если задача — найти, откуда берётся путь, то можно не уходить в криптографию.
Это нормальная инженерная граница.
Какие-то поля можно оставить как:
unknown_field_4
maybe_expire_date
looks_like_machine_id
some_feature_flags
И это лучше, чем притвориться, что ты всё понял.
Где сканворд ломается
Аналогия хорошая, но не идеальная.
Сканворд — честная игра. Там есть фиксированная сетка и правильные ответы. Если слово не подходит, оно конфликтует с пересечениями.
Реверс не такой.
- Там нет гарантии единственного решения. Один и тот же кусок кода может одинаково хорошо объясняться несколькими гипотезами. Поле может быть датой, версией, лимитом, timestamp, флагами — и пока не будет дополнительного контекста, ты не узнаешь точно.
- Можно долго жить с ошибочной гипотезой. Особенно если ты дал ей слишком уверенное имя. Назвал
expected_hmac, а потом всё вокруг начал подгонять под HMAC. Хотя правильнее было быexpected_macили вообщеexpected_sig. - Статика не всегда достаточна. Иногда надо идти в динамику: запускать, ставить брейкпоинты, смотреть реальные значения, логировать аргументы, подсовывать разные входные файлы.
Ghidra даёт карту. Но иногда надо выйти на местность. В сканворде такого нет. Там не нужно запускать слово под отладчиком.
Хотя было бы весело — идея для игр Zachtronics.
Минимальный набор действий в Ghidra
Если вы только открыли Ghidra и не знаете, куда вообще нажимать, я бы начал не с героического чтения ассемблера, а с очень простого маршрута.
Строки:
Window → Defined Strings
Или поиск строк:
Search → For Strings
Потом правый клик по интересной строке:
References → Show References to Address
Потом смотрим функцию, где строка используется.
Потом переименовываем переменные правым кликом, функции, какие аргументы и что возвращают, ходим по ссылкам. На старте этого уже достаточно — не надо сразу становиться шумахером ассемблера. Надо научиться находить опорные клетки.
строки ─▶ xref ─▶ переименовать ─▶ дальше
Какие идиомы стоит начать узнавать
Вот несколько паттернов, которые мне прямо помогли в этом разборе.
Динамический буфер с удвоением:
if (capacity - size < chunk_size) {
capacity <<= 1;
buf = realloc(buf, capacity);
}
Скорее всего, читают или собирают данные неизвестного размера.
Чтение big-endian значения:
val = read_be32(ptr);
ptr += 4;
Часто встречается в бинарных форматах, сетевых структурах, заголовках.
TLV-цикл:
tag = *ptr++;
length = read_length(ptr);
switch (tag) {
...
}
ptr += length;
Это структурированный объект из полей переменной длины.
Init / Update / Final:
ctx_init(&ctx, key, key_len);
ctx_update(&ctx, data, data_len);
result = ctx_final(&ctx);
Похоже на хэш, MAC, HMAC, шифр или другую контекстную обработку данных.
Сравнение после вычисления:
if (computed != expected) {
error();
}
Почти всегда проверка целостности, подписи, checksum, MAC, пароля или чего-то такого.
Суть не в том, чтобы выучить весь справочник паттернов. Суть в том, что после нескольких раз они начинают прилипать к глазам. Ты видишь блок и уже не читаешь каждую строку. Ты узнаёшь форму.
Почему это вообще успокаивает
Мне кажется, самое сложное в первом контакте с Ghidra — не техника, а ощущение. Ты открываешь функцию и думаешь: «Я ничего не понимаю. Наверное, я тупой. Наверное, реверс не для меня. Наверное, тут нужны какие-то люди, которые в детстве вместо сказок читали Intel Manual (о нет, таких не было)».
Но нет!
Ощущение «я ничего не понимаю» — это отличное стартовое состояние! Иначе это не реверс. Ты просто ещё не нашёл первое слово. Как только появляется первая строка, xref, нормальное имя — становится легче. Хаос начал получать форму.
Ты постепенно переписываешь машинный фарш в понятную карту.
Что забрать с собой
Что я бы сказал себе в начале этого разбора:
Не пытайся героически читать функцию сверху вниз. Сначала найди опорные места — строки, импорты, ошибки, константы, маленькие функции. Потом ходи по xrefs и переименовывай всё, что хоть немного начало проясняться. Не навсегда, а карандашом.
Идиомы тоже не надо учить как теорию. Увидел realloc с удвоением — запомнил. Увидел TLV — запомнил. Увидел init/update/final — запомнил. Через какое-то время ты начинаешь узнавать не отдельные строки, а форму.
Не пытайся заполнить всю сетку. В реальном реверсе достаточно понять механизм на уровне, который решает задачу.
И не пугайся первой стены. Она всегда такая.
████████████████ ████ ◆ ████ ████████████████
Но с дырочкой.