Реверс-инжиниринг — это сканворд. Как я впервые нормально понял Ghidra. Часть 2

Николай Носков 5 мин

В первой части — стена из 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 остаётся хвост — проверяет ещё и его. Онли инженерная процедура.

И вот если написать это списком, звучит просто. Ну даже скучно!

Но прикол в том, что в начале этого списка не было. Вообще. Был просто фарш. Он появился из маленьких шагов:

нашёл строку — перешёл по xref — заглянул в соседнюю функцию — переименовал — увидел импорт — узнал идиому — переименовал ещё — нашёл byteswap — предположил заголовок — увидел TLV — узнал init/update/final — проверил гипотезу по сравнению — уточнил имена.

Ура!

Именно поэтому реверс для меня стал похож на сканворд. Потому что каждое подтверждённое место ограничивает соседние.

Почему не надо пытаться понять всё

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

Ну иначе как будто не считается. Но это ловушка.

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

Нужно понимать достаточно для задачи.

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

Это нормальная инженерная граница.

Какие-то поля можно оставить как:

unknown_field_4
maybe_expire_date
looks_like_machine_id
some_feature_flags

И это лучше, чем притвориться, что ты всё понял.

Где сканворд ломается

Аналогия хорошая, но не идеальная.

Сканворд — честная игра. Там есть фиксированная сетка и правильные ответы. Если слово не подходит, оно конфликтует с пересечениями.

Реверс не такой.

  1. Там нет гарантии единственного решения. Один и тот же кусок кода может одинаково хорошо объясняться несколькими гипотезами. Поле может быть датой, версией, лимитом, timestamp, флагами — и пока не будет дополнительного контекста, ты не узнаешь точно.
  2. Можно долго жить с ошибочной гипотезой. Особенно если ты дал ей слишком уверенное имя. Назвал expected_hmac, а потом всё вокруг начал подгонять под HMAC. Хотя правильнее было бы expected_mac или вообще expected_sig.
  3. Статика не всегда достаточна. Иногда надо идти в динамику: запускать, ставить брейкпоинты, смотреть реальные значения, логировать аргументы, подсовывать разные входные файлы.

Ghidra даёт карту. Но иногда надо выйти на местность. В сканворде такого нет. Там не нужно запускать слово под отладчиком.

Хотя было бы весело — идея для игр Zachtronics.

Минимальный набор действий в Ghidra

Если вы только открыли Ghidra и не знаете, куда вообще нажимать, я бы начал не с героического чтения ассемблера, а с очень простого маршрута.

Строки:

Window → Defined Strings

Или поиск строк:

Search → For Strings

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

References → Show References to Address

Потом смотрим функцию, где строка используется.

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

Какие идиомы стоит начать узнавать

Вот несколько паттернов, которые мне прямо помогли в этом разборе.

Динамический буфер с удвоением:

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 — запомнил. Через какое-то время ты начинаешь узнавать не отдельные строки, а форму.

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

И не пугайся первой стены. Она всегда такая.

Но с дырочкой.