R0 CREW

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

Источник: habrahabr.ru

Ниже представлен конспект рассказа Дмитрия Склярова о реверс-инжиниринге.

Дмитрий Скляров — доцент кафедры информационной безопасности МГТУ им. Баумана и аналитик компании Positive Technologies. Работает в области информационной безопасности более 13 лет. Разработчик алгоритма программы Advanced eBook Processor.

Реверсинг — это, конечно, не самая простая дисциплина из области IT. Тем не менее, чтобы получить результат, то есть понять, что делает программа, не всегда необходимо анализировать каждую строчку кода и каждую ассемблерную команду. Иногда достаточно ряда простых логических умозаключений и умения «думать как программист». Понять, что я имею в виду, нам помогут два примера анализа, взятые из моей практики.

Что внутри черного ящика?

С первой задачей я столкнулся на CTF. CTF — это, своего рода, олимпиадное программирование, только для специалистов в области информационной безопасности. Задание называлось «Donn Beach». Итак, нам дано:

  • некоторый программно реализованный «чёрный ящик» — исполняемый файл, преобразующий входную последовательность в выходную
  • значение, которое должно получиться на выходе

Необходимо определить последовательность из 12 байт, которую нужно подать на вход.

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

Разумеется, на первом этапе программа грузится в дизассемблер. Вот два фрагмента кода, которые он выдал:

Здесь легко заметить строчки, которые остались от процесса сборки. Скорее всего, эти сообщения специально заложены организаторами, чтобы легче было догадаться, о чём идёт речь. При взгляде на строчки vm_set_params и vm_get_output в голове сразу возникает вопрос: что такое vm? При этом ничего, кроме virtual machine, в общем-то, в голову не приходит. Таким образом, можно сразу предположить, что здесь используется виртуальная машина и, исходя из этого, строить дальнейший анализ.

Мы видим четыре идущих подряд вызова функции (точки между ними — это просто подготовка параметров). Что может стоять между загрузкой параметров и извлечением результата? Логично предположить, что там происходит инициализация и запуск виртуальной машины. Попробуем дизассемблировать код инициализации VM.

Итак, мы смотрим на код vm_init, и видим достаточно большое количество не совсем понятных операций. Несмотря на то, что с языком ассемблера для Intel-совместимых компьютеров я знаком уже почти 20 лет, инструкции с плавающей точкой мне приходится анализировать крайне редко. Поэтому, просто посмотрев на этот код, без помощи справочника я не могу сказать, что он делает. Какие выводы все же можно сделать по этому фрагменту кода?

  • Мы работаем с 64-битовыми ММХ-регистрами
  • Код функции состоит из 420 команд
  • Ничего не понятно

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

У нас есть функция vm_init, которая как-то загружает регистры из начальных данных. На вход подаётся восемь 32-битовых значений. Что, если задать этим значениям константные числа, и пометить байты от 00 до 1F?

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

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

  • огромное количество кодов для работы с multimedia-регистрами
  • при разборе потока команд виртуальной машины видно, что каждый код команды занимает 1 байт и бывает 1 аргумент (опционально)
  • у каждого байта команды есть своя функция обработки
  • сложность каждой функции обработки примерно такая же, как и в vm_init (так что разбирать её нет ни малейшего желания: это сложный и наверняка не самый эффективный путь)
  • большинство инструкций, на самом деле, отображаются в один и тот же обработчик (вероятно, это функция nop)
  • вся обработка идёт в одном цикле

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

Я написал небольшой отладчик, но вместо того, чтобы смотреть в отладчике пошагово, что делает программа, решил проследить состояние всех ММX-регистров в каждой точке. В результате был создан лог-файл, в котором для каждой текущей команды записывалось состояние VM до и после выполнения, а также код операции и один дополнительный байт, чтобы можно было восстановить логику, зная коды операций.

Вот что у меня получилось:

На вход подаётся значение в регистре R0 и на выходе оно оказывается на стеке. Видно, что значение регистра R6 уменьшается, а R7, наоборот, увеличивается.

Из этого можно сделать вывод, что R6 — это счётчик-указатель стека, который уменьшается при добавлении данных на стек, R7 — это указатель команд. При этом команда с кодом операции 0D-00 перенесла значение из нулевого регистра в стек.

Значение, которое было в регистре R4, оказалось опять же на стеке, а код команды 0D04 от предыдущей отличается только аргументом. Из этого можно предположить, что 0D, — код операции push, — это размещение данных на стеке, а значение в первом байте после кода операции — это указатель на номер регистра, который будет использован в этой операции.

Состояние регистров до и после выполнения различаются только одним регистром, который я назвал указатель команд (Instruction Pointer) — это значит, что никаких изменений не произошло. Значит, можно предположить, что код операции 06 — nop, который ничего не делает.

В данном случае аргументом было значение FF, оно оказалось на стеке, а указатель стека уменьшился. Легко сделать предположение, что данная команда загружает значение, которое было в аргументе, как 32-битовое значение в стек.

При выполнении этой команды значение на стеке оказалось во втором регистре. Таким образом, код операции 4С — это извлечение из стека, а аргумент, идущий после него, это номер регистра. Можно сделать такие предположения просто посмотрев на пары вход-выход,.

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

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

К сожалению, я решил эту задачу через 15 минут после окончания зачётного времени, то есть сдать её мы не успели, но сам процесс решения показался мне достаточно интересным. Почему всё получилось так просто? Потому что авторы задания реализовали всё состояние виртуальной машины в ММХ-регистрах процессора.

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