R0 CREW

Gray Hat Python: Глава 11 - IDAPython (Перевод: Prosper-H)

Перейти к содержанию

Intro

IDA Pro [1] – это дизассемблер, который долгое время остается самым мощным инструментом статического анализа. Произведенный компанией Hex-Rays SA [2], во главе с ее легендарным главным архитектором Ильфаком Гильфановым, IDA Pro содержит множество аналитических возможностей. Она может анализировать бинарные файлы для большинства имеющихся архитектур, работать на различных платформах и имеет встроенный отладчик. Наряду со своими основными возможностями, IDA Pro имеет свой собственный скриптовый язык IDC, а так же SDK предоставляющее разработчикам полный доступ к IDA Plugin API.

В 2004 году, используя открытую архитектуру IDA, Gergely Erdélyi и Ero Carrera выпустили IDAPython, плагин который предоставляет реверс инженерам полный доступ к скриптовому ядру IDC, IDA Plugin API и всем стандартным модулям, поставляемым вместе с Python. Что позволяет разрабатывать мощные скрипты, для выполнения задач автоматического анализа в IDA, используя чистый Python. IDAPython используется как в коммерческих продуктах, таких как BinNavi [3], так и в открытых проектах, таких как PaiMei [4] и PyEmu (который рассмотрен в Главе 12).

В начале этой главы мы рассмотрим шаги по установке IDAPython на IDA Pro 5.2. Далее мы рассмотрим некоторые наиболее часто используемые функции, предоставляемые IDAPython. Под конец завершим главу написанием нескольких скриптов, позволяющих ускорить некоторые общие задачи реверс инженера, с которыми ему приходится часто сталкиваться.

11.1 Установка IDAPython

Для установки IDAPython вначале нужно загрузить установочные файлы, используйте для этого следующую ссылку:

[INDENT]http://idapython.googlecode.com/files/idapython-1.0.0.zip.[/INDENT]

После того как вы скачали zip-архив, разархивируйте его в любую директорию. Внутри распакованной директории вы увидите папку plugins, которая будет содержать файл python.plw. Вам нужно скопировать python.plw в папку plugins IDA Pro, при установке по умолчанию она будет расположена в “C\Program Files\IDA\plugins”. Далее, из разархивированной директории IDAPython, скопируйте папку python в родительский каталог IDA, который при установке по умолчанию будет следующим “C:\Program Files\IDA”.

Чтобы убедиться в корректной установке – просто загрузите любой исполняемый файл в IDA, как только она закончит свой автоматический анализ, вы увидите вывод, в нижней панели IDA, показывающий успешную установку IDAPython. Вывод в панели IDA Pro должен выглядеть так, как показано на Рис. 11-1.

Рис. 11-1: Вывод панели IDA Pro отображающий успешную установку IDAPython

Теперь, когда IDAPython успешно установлен, в меню File IDA Pro появилось две дополнительные опции (Рис. 11-2).

Рис. 11-2: Меню File после установки IDAPython

Двумя новыми опциями стали Python File и Python command, а также горячие клавиши к ним. Если нужно выполнить простую команду Python, можно нажать на Python command, после чего появится диалоговое окно, позволяющее вводить Python-команды. Результат выполнения этих команд можно увидеть в “Output window” IDA Pro. Пункт меню Python command используется для выполнения автономных скриптов IDAPython. Теперь, когда у вас установлен и работает IDAPython, давайте рассмотрим некоторые наиболее часто используемые функции поддерживаемые им.

11.2 Функции IDAPython

IDAPython полностью совместим с IDC. Это значит, что любой вызов функции, которую поддерживает IDC http://www.hex-rays.com/idapro/idadoc/162.htm[5], также можно использовать в IDAPython. Мы рассмотрим некоторые из функций, которые вы обычно будите использовать при написании скриптов IDAPython. Они предоставят прочную основу, для того чтобы начать разрабатывать собственные скрипты. Язык IDC поддерживает свыше 100 вызовов функций. Поэтому, в этой главе, дан далеко не исчерпывающий их список, но он поощрит вас исследовать его лучше самостоятельно.

11.2.1 Полезные функции

Ниже представлена пара полезных функций, которые пригодятся в большинстве ваших скриптах:

ScreenEA()
Получает адрес того, где в настоящий момент размещен курсок (мышки) на экране IDA. Это позволяет выбрать начальную точку, для запуска сценария.

GetInputFileMD5()
Возвращает MD5-хеш двоичного файла, загруженного в IDA. Это полезно для отслеживания изменений в загружаемых файлах.

11.2.2 Сегменты

В IDA двоичные файлы разбиваются на сегменты, каждый сегмент имеет определенный класс (CODE, DATA, BSS, STACK, CONST или XTRN). Следующие функции предоставляют возможность получить информацию о сегментах, которые содержатся в двоичном файле.

FirstSeg()
Возвращает начальный адрес первого сегмента в двоичном файле.

NextSeg()
Возвращает начальный адрес следующего сегмента в двоичном файле или BADADDR, если сегментов больше не нет.

SegByName( string SegmentName )
Возвращает начальный адрес сегмента основанного на имени сегмента. Например, вызывая ее с .text (в качестве параметра) будет возвращен начальный адрес сегмента «кода» для двоичного файла.

SegEnd( long Address )
Возвращает конец сегмента, основанного на адресе, содержащемся в пределах данного сегмента.

SegStart( long Address )
Возвращает начало сегмента, основанного на адресе, содержащемся в пределах данного сегмента.

SegName( long Address )
Возвращает имя сегмента, основанного на любом адресе в пределах данного сегмента.

Segments()
Возвращает список начальных адресов для всех сегментов в конкретном двоичном файле.

11.2.3 Функции

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

Functions( long StartAddress, long EndAddress )
Возвращает список всех функций, начальные адреса которых содержатся между StartAddress и EndAddress.

LocByName( string FunctionName )
Возвращает адрес функции, основываясь на ее имени.

GetFuncOffset( long Address )
Преобразует адрес, в пределах функции, в строку, которая показывает имя функции и смещение внутри функции.

GetFunctionName( long Address )
Возвращает имя функции, основываясь на преданном адресе, который ей принадлежит.

11.2.4 Перекрестные ссылки

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

CodeRefsTo( long Address, bool Flow )
Возвращает список ссылок кода на данный адрес. Логический флаг Flow говорит IDAPython следовать или не следовать нормальному потоку кода при определении перекрестных ссылок.

CodeRefsFrom( long Address, bool Flow )
Возвращает список ссылок кода из данного адреса.

DataRefsTo( long Address )
Возвращает список ссылок данных на данный адрес. Полезно для слежения за использованием глобальной переменной в исследуемом файле.

DataRefsFrom( long Address )
Возвращает список ссылок данных из данного адреса.

11.2.5 Debugger Hooks

Одной очень хорошей фичей, которую поддерживает IDAPython, является возможность определять debugger hook в IDA и устанавливать обработчики различных отладочных событий, которые могут возникнуть. Хотя IDA обычно не используется для отладки, есть моменты, когда просто легче запустить встроенный отладчик IDA, чем переключаться на другой инструмент. Мы будем использовать один из этих debugger hooks позже при создании простого инструмента покрытия кода (code coverage tool). Чтобы установить debugger hook, вначале нужно определить основной класс debugger hook, а затем определить различные обработчики событий в этом классе. Мы будем использовать следующий класс в качестве примера:

class DbgHook(DBG_Hooks):
# Event handler for when the process starts
# Обработчик события, срабатывающий при запуске процесса
def dbg_process_start(self, pid, tid, ea, name, base, size):
return

# Event handler for process exit
# Обработчик события, срабатывающий при завершении процесса 
def dbg_process_exit(self, pid, tid, ea, code):
return

# Event handler for when a shared library gets loaded
# Обработчик события, срабатывающий при загрузке библиотеки
def dbg_library_load(self, pid, tid, ea, name, base, size):
return

# Breakpoint handler
# Обработчик срабатывающий при срабатывании точки останова
def dbg_bpt(self, tid, ea):
return

Этот класс содержит некоторые общие обработчики отладочных событий, которые можно использовать при создании простых отладочных скриптов в IDA. Чтобы установить debugger hook используйте следующий код:

debugger = DbgHook()
debugger.hook()

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

AddBpt( long Address )
Устанавливает программный брейкпойнт на указанный адрес.

GetBptQty()
Возвращает количество брейкпойнтов установленных в настоящий момент.

GetRegValue( string Register )
Получает значение регистра основываясь на его имени.

SetRegValue( long Value, string Register )
Устанавливает значение для указанного регистра.

11.3 Example Scripts

Теперь давайте создадим несколько простых скриптов, которые могут помочь в некоторых общих задачах, с которыми вы столкнетесь при реверсинге двоичных файлов. Мы создадим скрипты для: поиска перекрестных ссылок, на опасные вызовы функций; мониторинга вызываемых функций с использованием IDA debugger hook и вычисления размера переменных стека для всех функций в двоичном файле.

11.3.1 Поиск перекрестных ссылок на опасные функции

Существует ряд опасных функций, например, функции по работе со строками (strcpy, sprintf) или функции по работе с памятью (memcpy), при не правильном использовании которых могут возникать различные проблемы. При исследовании двоичных файлов нам нужно уметь легко находить подобные функции, поскольку они могут представлять не малый интерес, например, для исследователя багов. Давайте создадим простой скрип, чтобы отследить эти функции и определить места, откуда они вызываются. Помимо этого установим красный цвет фона для каждой вызываемой функции, что бы мы могли легко видеть эти вызовы при просмотре графиков сгенерированных IDA. Откройте новый Python файл, назовите его cross_ref.py и введите следующий код:

cross_ref.py

from idaapi import *

danger_funcs = ["strcpy","sprintf","strncpy"]

for func in danger_funcs:
    (#1): addr = LocByName( func )

    if addr != BADADDR:

        # Grab the cross-references to this address

        (#2): cross_refs = CodeRefsTo( addr, 0 )

        print "Cross References to %s" % func
        print "-------------------------------"
        for ref in cross_refs:

            print "%08x" % ref
            
            # Color the call RED
            (#3): SetColor( ref, CIC_ITEM, 0x0000ff)

Мы начинаем с получения адреса опасной функции (#1), затем проверяем его, чтобы убедиться, что это допустимый адрес в пределах двоичного файла. Затем получаем все перекрестные ссылки на код, который делает вызов опасной функции (#2), и проходим в цикле по их списку, распечатывая их адреса и раскрашивая вызываемые инструкции (#3), так чтобы мы могли видеть их на графиках IDA. Попробуйте использовать скрипт на файле war-ftpd.exe в качестве примера. Когда запустите скрипт, вы должны увидеть вывод, подобный тому, как показано в Листинге 11-1.

Листинг 11-1: Вывод после использования cross_ref.py

Cross References to sprintf
-------------------------------
004043df
00404408
004044f9
00404810
00404851
00404896
004052cc
0040560d
0040565e
004057bd
004058d7
...

Все перечисленные адреса, являются местоположениями вызовов функции sprintf и если вы посмотрите на эти адреса в графике IDA, вы должны увидеть, что инструкции раскрашены, как показано на Рис. 11-3.

Рис. 11-3: Вызов sprintf окращеный с помощью скрипта cross_ref.py

11.3.2 Function Code Coverage

При выполнении динамического анализа, довольно полезно понимать какой код выполняется во время исполнения исследуемого файла. Покрытие кода (code coverage) является полезным показателем, который помогает понять, как работает программа. Мы будем использовать IDAPython для перебора всех функций, в исследуемом двоичном файле, с последующей установкой точек останова на начало каждого адреса. Затем мы запустим отладчик IDA и используем debugger hook для вывода уведомления, во время каждого срабатывания точек останова. Откройте новый Python файл, назовите его func_coverage.py и введите следующий код:

func_coverage.py

from idaapi import *

class FuncCoverage(DBG_Hooks):
    
    # Our breakpoint handler
    def dbg_bpt(self, tid, ea):
        print "[*] Hit: 0x%08x" % ea
        return

# Add our function coverage debugger hook
(#1):  debugger = FuncCoverage()
debugger.hook()

current_addr = ScreenEA()

# Find all functions and add breakpoints
(#2):  for function in Functions(SegStart( current_addr ), SegEnd( current_addr )):
    (#3):  AddBpt( function )
    SetBptAttr( function, BPTATTR_FLAGS, 0x0 )

(#4):  num_breakpoints = GetBptQty()

print "[*] Set %d breakpoints." % num_breakpoints

Вначале мы устанавливаем наш debugger hook (#1), так чтобы он вызывался во время каждого отладочного события. Затем перебираем все адреса функций (#2) и устанавливаем точки останова на каждый адрес (#3). Вызов SetBptAttr устанавливает флаг (BPTATTR_FLAGS), чтобы отладчик не останавливался при каждом срабатывании брейкпойнтов; если его не установить, то придется в ручную возобновлять отладку, после каждого срабатывания точек останова. Затем мы распечатываем общее количество установленных брейкпойнтов (#4). Обработчик брейкпойнтов распечатывает адреса каждой сработавшей точки останова, используя для этого переменную ea, которая на самом деле является ссылкой на регистр EIP, во время срабатывания брейкпойнта. Теперь запустите отладчик (горячая клавиша F9). После чего вы должны увидеть вывод, отображающий функции на которых сработал брейкпойнт. Это должно дать вам очень высокоуровневое представление того какие функции вызываются и в каком порядке они выполняются.

11.3.3 Вычисление размера стека

Иногда при оценке двоичного файла на возможные уязвимости, важно знать размер стека, в определенных вызовах функций. Это может рассказать вам, есть ли просто указатели, передающиеся в функцию, или еще есть буферы, выделенные в стеке, которые могут представлять интерес, если вы можете контролировать количество передаваемых данных в эти буферы (что может привести к общей уязвимости переполнения буфера). Давайте напишем код для перебора всех функции, который покажет все функции с выделенными в стеке буферами, которые могут представлять нас интерес. Вы можете объединить этот скрипт, с нашим предыдущим примером, для отслеживания вызова всех интересных функций, во время выполнения отладки. Откройте новый Python файл, назовите его stack_calc.py и введите следующий код:

stack_calc.py

from idaapi import *

(#1): var_size_threshold = 16
current_address = ScreenEA()

(#2): for function in Functions(SegStart(current_address), SegEnd(current_address)):
    (#3): stack_frame = GetFrame( function )
    
    frame_counter = 0
    prev_count = -1

    (#4): frame_size = GetStrucSize( stack_frame )

    while frame_counter < frame_size:

        (#5): stack_var = GetMemberNames( stack_frame, frame_counter )

        if stack_var != "":

            if prev_count != -1:

                (#6): distance = frame_counter - prev_distance

                if distance >= var_size_threshold:
                    print "[*] Function: %s -> Stack Variable: %s (%d bytes)" % ( GetFunctionName(function), prev_member, distance )

            else:
                prev_count = frame_counter
                prev_member = stack_var

                (#7): try:
                    frame_counter = frame_counter + GetMemberSize(stack_frame,
frame_counter)

                except:
                    frame_counter += 1
        else:
            frame_counter += 1

Вначале устанавливаем размер порога, который определяет, насколько большой должна быть переменная стека прежде, чем мы будем считать ее буфером (#1); 16 байт приемлемый размер, но не стесняйтесь экспериментировать с различными размерами. Затем начинаем перебирать все функции (#2), получая объект кадра стека (stack frame) для каждой функции (#3). Далее используем функцию GetStrucSize (#4), для определения размера кадра стека в байтах. Затем начинаем перебирать стековый фрейм (stack frame) байт за байтом, пытаясь определить, присутствует ли стековая переменная в каждом байте смещения (#5). Если она присутствует, вычитаем текущее байтовое смещение из предыдущей переменной стека (#6). Основываясь на расстоянии между этими двумя переменными, можно определить размер переменной. Если расстоянием не достаточно большое, пытаемся определить размер текущей переменной стека (#7) и увеличиваем счетчик на размер текущей переменной. Если не можем определить размер переменной, то просто увеличиваем счетчик на один байт и продолжаем цикл. После применения этого скрипта к двоичному файлу, вы должны видеть вывод (если есть несколько буферов выделенных в стеке), как показано ниже в Листинге 11-2.

Листинг 11-2: Вывод скрипта stack_calc.py показывает выделенные в стеке буферы и их размеры

[*] Function: sub_1245 -> Stack Variable: var_C(1024 bytes)
[*] Function: sub_149c -> Stack Variable: Mdl (24 bytes)
[*] Function: sub_a9aa -> Stack Variable: var_14 (36 bytes)

Теперь вы имеете основу, которая позволит вам использовать IDAPython. Помимо этого у вас есть некоторые полезные скрипты, которые вы можете легко расширять, объединять или улучшать. Пара минут на написание скрипта в IDAPython может спасти вас от часов ручного реверсинга, а время, как известно, является самым ценным активом. Теперь давайте взглянем на PyEmu, x86 эмулятор, основанный на Python, который является прекрасным примером работы IDAPython в действии.

© Translated by Prosper-H from r0 Crew