При реверсинге/декомпиляции больших программ написанных на с++, хочется иметь механизм определения функций, принадлежащих к тому или иному классу.
Статическим анализом легко можно определять конструкторы классов, или факт того, является ли функция членом класса или нет.
Например, конструктор, как правило, инициализирует переменные класса, и в асм листинге все это выглядит достаточно узнаваемо:
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).
Статическим анализом легко можно определять конструкторы классов, или факт того, является ли функция членом класса или нет.
Например, конструктор, как правило, инициализирует переменные класса, и в асм листинге все это выглядит достаточно узнаваемо:
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).
> и вобще говоря, еще не решенная
ОтветитьУдалитьС чего бы? Инструменты для символьного исполнения есть и даже вполне работоспособные. То, что они сырые и требуют много ресурсов для получения покрытия близкого к полному -- это уже другой вопрос.
Если речь о символьном исполнении по исходному коду, то да, S2E и иже с ними вроде существуют и работают. Но вот для произвольного бинаря, прежде чем перевести его в какой-нибудь IL с которым будет работать среда для символьного исполнения, придется научится отделять код от данных, а это уже NP-полная проблема.
ОтветитьУдалитьЗдесь две проблемы. В реальном хорошо оптимизированном коде у невиртуальных функций очень часто искажается calling convention и вызов метода для объекта может означать, что это не метод класса объекта, а метод его предка.
ОтветитьУдалитьПочему PIN?
ОтветитьУдалитьА почему бы и нет?
Удалить