суббота, 22 декабря 2012 г.

Определение функций класса с использованием статического и динамического анализа

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

Статическим анализом легко можно определять конструкторы классов, или факт того, является ли функция членом класса или нет.
Например, конструктор, как правило, инициализирует переменные класса, и в асм листинге все это выглядит достаточно узнаваемо:

3002E5A9 sub_3002E5A9    proc near                                  
3002E5A9                 xor     eax, eax
3002E5AB                 push    esi
3002E5AC                 mov     esi, ecx
3002E5AE                 mov     [esi], eax
3002E5B0                 mov     [esi+4], eax
3002E5B3                 mov     [esi+8], eax
3002E5B6                 mov     [esi+0Ch], eax
3002E5B9                 mov     [esi+10h], eax
3002E5BC                 mov     [esi+14h], eax
3002E5BF                 mov     [esi+18h], eax
3002E5C2                 mov     [esi+1Ch], eax
3002E5C5                 mov     [esi+20h], eax
3002E5C8                 mov     [esi+24h], eax
3002E5CB                 mov     [esi+28h], eax
3002E5CE                 mov     [esi+2Ch], eax
3002E5D1                 call    sub_300AC9DD
3002E5D6                 mov     eax, esi
3002E5D8                 pop     esi
3002E5D9                 retn
3002E5D9 sub_3002E5A9    endp

Что касается принадлежности функции к классу, тут тоже все очевидно. Как гласит стандарт, для не статических функций-членов ключевое слово this является не явно передаваемым в ф-цию адресом того объекта, для которого вызывается функция.

В асм листинке (для компиляторов от микрософт), указатель на объект класса всегда приходит в ф-цию не явно, через регистр ecx.

Пример:

3002F131                 lea     ecx, [esi+34h]
3002F134                 mov     [esi+2Ch], ebx
3002F137                 mov     [esi+30h], ebx
3002F13A                 call    sub_3002E5A9

Таким образом, статическим анализом довольно просто ответить на два вышеупомянутых вопроса. А вот ответить на вопрос, является ли ф-ция func1 членом класса A, или членом класса B - уже сложнее. Но использовав динамический анализ, это становится довольно простым делом.

Итак, используемые для статического анализа инструменты:

1) IDA PRO
2) Скрипты IdaPython ( генерация листинга ф-ций, принадлежащих тому или иному классу )

Инструменты для динамического анализа:

1) PIN ( http://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool )
2) Python скрипты для преобразования pin output logs в читабельный формат

Основная мысль:

* Имея на руках все ф-ции принадлежащие классам( все это определяется в статике ), прогоняем приложение в динамике(способов много, трассировка, эмуляция, динамическая рекомпиляция) и получаем значение ecx на входе в ф-цию.

Алгоритм(кратко):

1) В статике находим все функции, принадлежащие классам
2) Их адреса записываем в input data file for PIN
3) Пишем модуль для PIN, который на вход принимает файл с адресами ф-ций принадлежащих классам. А на выход дает файл содержащий пары: адрес ф-ции класса + значение ecx регистра.
4) Прогоняем исследуемое приложение под PIN, получаем output file с результатами
5) Парсим результаты, используя имена функций из базы IDA, в результате получаем: имя ф-ции + содержимое регистра ecx

Алгоритм(чуть более подробно):

Пожалуй единственное, что может вызвать затруднение, это нахождение всех ф-ций принадлежащих к классам.

Алгоритм примерно такой:

* перечисляем все ф-ции и исследуемом приложении
* строим для каждой ф-ции базовые блоки
* для каждого блока анализируем все инструкции
* у инструкций анализируем операнды
* вводим понятие состояний для регистров ( STATE_INIT, STATE_SAVE, STATE_RESTORE, STATE_MODIFICATE и так далее )
* ведем списки состояний для базовых блоков

Тогда для всех ф-ций, у второго операнда с типом reg и регистром ecx, нужно будет найти состояние STATE_SAVE. Причем в списке блока это состояние должно быть первым.

Пример:

3002F110 sub_3002F110    proc near
3002F110                 push    ebx
3002F111                 push    esi
3002F112                 push    [esp+8+arg_4]
3002F116                 mov     esi, ecx        <== интересующая инструкция
3002F118                 call    sub_301AC430

Список состояний для регистра ecx в данном случае будет содержать первым состоянием STATE_SAVE. Это означает, что регистр сразу же начинает использоваться, без инициализации. То есть, вспоминая про this, ф-ция является членом класса, записываем её в список.

* на выходе будем иметь список ф-ций принадлежащий классам исследуемого приложения.

Что касается модуля PIN, то там все тривиально:

1) устанавливается ф-ция IMG_AddInstrumentFunction, в колбеке которой указываем, за каким приложением/модулем нужно следить(нужно для оптимизации процесса).
2) устанавливается ф-ция INS_AddInstrumentFunction, в колбеке которой указываем, какие аргументы нас интересуют(адрес инструкции и содержимое ecx):

INS_InsertCall( ins, IPOINT_BEFORE, (AFUNPTR)InstructionHandler, IARG_INST_PTR, IARG_REG_VALUE, REG_ECX, IARG_END );

Результаты всего этого бедлама и их анализ:

Во-первых, несмотря на фильтрацию инструкций только от нужного exe/dll в PIN, результатов для большого приложения придется ждать довольно долго ( десятки минут ). Во-вторых, результаты придется анализировать.

Выглядит все примерно следующим образом:
...
sub_30090C3F, ecx = 0x141af0a0
sub_30069312, ecx = 0x12f460
sub_30070AA7, ecx = 0x14192000
sub_30070AA7, ecx = 0x14192000
sub_3008FB93, ecx = 0x141af0a0
sub_30084CA2, ecx = 0x141af0a0
sub_3000D6B6, ecx = 0x189dc8
sub_3000242F, ecx = 0x189de4
...

Сразу заметно, что для ф-ций sub_30090C3F, sub_3008FB93, sub_30084CA2 значение ecx одинаково. Следовательно, они принадлежат к одному классу. Виртуальные ф-ции для такого способа также не являются проблемой. Конструкторы находятся также, они просто будут первыми в списке.

Еще стоит упомянуть о том, что при выполнении исследуемой программы, вызываются не все ф-ции. Соответственно, не все ф-ции принадлежащие классам можно восстановить.

Но полное покрытие это уже совсем другая задача, и вобще говоря, еще не решенная (см. информацию по symbolic execution).

5 комментариев:

  1. > и вобще говоря, еще не решенная

    С чего бы? Инструменты для символьного исполнения есть и даже вполне работоспособные. То, что они сырые и требуют много ресурсов для получения покрытия близкого к полному -- это уже другой вопрос.

    ОтветитьУдалить
  2. Если речь о символьном исполнении по исходному коду, то да, S2E и иже с ними вроде существуют и работают. Но вот для произвольного бинаря, прежде чем перевести его в какой-нибудь IL с которым будет работать среда для символьного исполнения, придется научится отделять код от данных, а это уже NP-полная проблема.

    ОтветитьУдалить
  3. Здесь две проблемы. В реальном хорошо оптимизированном коде у невиртуальных функций очень часто искажается calling convention и вызов метода для объекта может означать, что это не метод класса объекта, а метод его предка.

    ОтветитьУдалить