«Bro, what…?» #1. Почему strings и file не спасают реверс-инженера? Серия 2

«Bro, what…?» #1. Почему strings и file не спасают реверс-инженера? Серия 2

В прошлой серии мы осмотрели бинарник снаружи: узнали архитектуру через file, вытащили строки через strings и даже запустили программу в изолированной среде. Мы увидели ее поведение, но так и не поняли алгоритм. Консольные утилиты хороши для первичного осмотра, но они не ответят на главные вопросы: почему программа принимает example, но отвергает test? И что за число она просит ввести?

1.4. Статический анализ: дизассемблеры и декомпиляторы

Консольные утилиты (file, strings, readelf) дали начальное понимание: мы знаем архитектуру, извлекли строки, увидели поведение программы при запуске. Но они не отвечают на главные вопросы:

  • Почему программа принимает строку example, но отвергает test?
  • Что за число она просит ввести и как оно вычисляется?
  • Как устроена логика, которая скрывается за строками?

Мы знаем, «что» программа делает: запрашивает строку, проверяет ее, затем запрашивает число и сравнивает. Но мы не знаем, «как» она это делает.

1.4.1. Как работает компиляция

Напомню, как программа вообще создается. Программист пишет код на языке высокого уровня, например, на C++:

int main() { int x = 10; return x + 5; }

Компилятор переводит этот код в машинный язык – набор инструкций, которые понимает процессор:

PUSH RBP MOV RBP,RSP MOV dword ptr [RBP + local_c],0xa MOV EAX,dword ptr [RBP + local_c] ADD EAX,0x5 POP RBP RET

Пример скомпилирован без оптимизаций (-O0), поэтому пролог «классический»: PUSH RBP; MOV RBP, RSP. В третьем разделе мы увидим, как тот же код выглядит после оптимизаций и почему это меняет все наше представление о локальных переменных.

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

Представьте, что вы перевели книгу с русского на китайский, а затем с китайского обратно на русский. Даже если переводчик идеален, вы получите совсем другой текст – смысл сохранится, но слова и выражения будут другими. Так и здесь, компилятор выбрасывает имена переменных, перестраивает код, оптимизирует его. Обратное преобразование – декомпиляция – принципиально не может восстановить исходный код в точности.

Компиляция – однонаправленный процесс
Компиляция – однонаправленный процесс

1.4.2 Что делают дизассемблеры и декомпиляторы

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

Декомпиляция пытается восстановить высокоуровневый код (псевдокод на C/C++) по ассемблеру. Это более удобный уровень для анализа, но важно помнить, что декомпилятор не восстанавливает исходный код, а переводит низкоуровневый язык (ассемблер), на более понятный высокоуровневый псевдокод (Си). Декомпилятор может ошибаться, пропускать детали и добавлять лишние конструкции.

1.4.3. Почему нам нужен и тот, и другой

В нашем анализе мы будем использовать оба подхода:

  • Декомпилятор (в Ghidra) – дает нам быстрый обзор и позволяет понять логику на высоком уровне.
  • Дизассемблер (в Ghidra и GDB) – когда декомпилятор ошибается или показывает странности, мы спускаемся на уровень инструкций и проверяем, что на самом деле происходит.

1.4.4. Знакомство с Ghidra

Ghidra (иногда буду называть Гидрой) – это бесплатный инструмент с открытым исходным кодом, разработанный Агентством национальной безопасности США (АНБ). Она умеет и дизассемблировать, и декомпилировать. Ghidra загружает бинарник, анализирует его структуру, находит функции, восстанавливает поток управления и показывает код, который можно читать почти как исходный.

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

В следующем разделе загрузим исследуемую crackme в Ghidra и начнем с поиска функции main – именно с нее начинается вся логика программы.

Конец второй серии

Мы поняли, что консольных утилит катастрофически мало. Нам нужен инструмент, который сможет превратить машинные байты обратно в читаемый код. И здесь на сцену выходит тяжелая артиллерия – Ghidra. В следующей части мы загрузим бинарник в декомпилятор и найдем скрытую функцию main.

Продолжение в следующей серии...

P.S.

Полный текст статьи:

Подписывайтесь на мой канал в Telegram или ВКонтакте.

1