Очередная часть про IdaPython. Разбираемся c Api Иды. Пишем свой WinApi Logger.
При внушительном размере исследуемого файла, рассматривать под лупой каждый opcode просто нет времени. Зачастую (даже оптимизирующий) компилятор собирает выходной файл довольно близко к исходным текстам. Давайте посмотрим как сохраняется структура функций, собрав следующий код в MSVC с флагом Minimize Size (/O1):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
DWORD MULx2(DWORD in) { return in * 2; } DWORD MULx4(DWORD in) { return MULx2(MULx2(in)); } void entry() { DWORD out = MULx4(10); ExitProcess(out); } |
Пропустим полученный файл через IDA:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
sub_401000 proc near lea eax, [ecx+ecx] retn sub_401000 endp sub_401004 proc near push 0Ah pop ecx call sub_401000 mov ecx, eax jmp sub_401000 sub_401004 endp start proc near call sub_401004 push eax call ds:ExitProcess start endp |
Что сделал компилятор?
- Оптимизировал сложение двух DWORD в одном opcode (Sub_401000)
- Встроил параметр 0Ah = 10 в тело функции (sub_401004)
- Передал аргументы через регистры вместо стека (sub_401004)
- Заменил call на jmp, использовав чужой retn (sub_401004)
Впечатляет. Получилось похоже на ручную оптимизацию ассемблерного кода!
Покуда компилятор сохраняет структуру функций, удобно рассматривать функцию как чёрную коробку, отмечая что подаётся на вход и получается на выходе. Порой, чтобы узнать что делает новая функция, достаточно знать какие известные нам функции она вызывает.
Одним из моих любых инструментов в OllyDbg был Conditional log, который позволял поставить в теле функции условную точку останова и логировать все параметры. Ближе к концу статьи мы сделаем его аналог на IdaPython.
Где брать примеры?
Отсутствие внятной документации — серьёзный мотиватор для покупки лицензии дающей быстрое решение всех вопросов через саппорт. Похоже, что монопольный доступ к нормальной документации — основа их бизнеса.
IdaPython — не более чем тонкая прослойка до си-функций доступных плагинам.
- Примеры готовых скриптов можно найти на github.
- Большая часть дальнейшего кода основана на DIE.
- Если вы не понимаете что здесь происходит, читайте цикл «От зелёного к красному»
- Все дальнейшие скрипты работают в Ida6.8 для 32 бит. Младшие версии не проверял!
Устанавливаем хуки
Представим, что исследуемый код вызывает функцию Sleep, принуждая текущий поток заснуть на длительное время. Каждый раз ловить его руками не представляется возможным.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
RunPlugin("python", 3) def sleep_hook(): ESP = GetRegValue("ESP") param_addr = ESP + 4 PatchDword(param_addr, 1) def install_sleep_hook(): sleep_addr = LocByName("kernel32_Sleep") AddBpt(sleep_addr) SetBptCnd(sleep_addr, "sleep_hook()") install_sleep_hook() |
Мы подменяем параметр функции на лету, это SpeedHack в чистом виде.
Парсим импорт
Если Вы обратите внимание, в прошлом листинге мы запрашиваем адрес функции Sleep с префиксом «kernel32». Ida автоматически назначает имена:
- Функция внутри кода, скорее всего часть стандартной либы GCC
- Ссылка на функцию в IAT
- Непосредственно функция в Kernel32
Как же нам найти правильную функцию? Через перечисление импорта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
def enum_iat(callback): nimps = idaapi.get_import_module_qty() for i in xrange(0, nimps): current_lib = idaapi.get_import_module_name(i) if not current_lib: continue idaapi.enum_import_names(i, callback) def get_function_name(ea): mask = idc.GetLongPrm(idc.INF_SHORT_DN) function_name = idc.Demangle(idc.GetFunctionName(ea), mask) if function_name: function_name = function_name.split("(")[0] if not function_name: function_name = idc.GetFunctionName(ea) if not function_name: function_name = idc.Name(ea) if not function_name:function_name = "UNKN_FNC_%s" % hex(ea) return function_name def show_iat_callback(iat_addr, name, ord): api_addr = DbgDword(iat_addr) api_name = get_function_name(api_addr) print('0x{:x} {}'.format(api_addr, api_name)) return True def show_iat_list(): enum_iat(show_iat_callback) show_iat_list() |
Остановимся на именовании. GetFunctionName получает уже назначенное имя, а idc.Name просит иду придумать новое. Demangle приводит в читаемый вид имена вроде:
1 2 |
.idata:0041E168 ; struct _iobuf * __cdecl std::_Fiopen(char const *, int, int) .idata:0041E168 extrn ?_Fiopen@std@@YAPAU_iobuf@@PBDHH@Z:dword |
Получаем параметры
WinApi использует __stdcall, все параметры передаются через стек, а ответ возвращает EAX. Зная количество параметров, мы можем прочитать их из стека.
Проблема в том, что существуют и другие соглашения о вызове. Нам нужен точный прототип перехватываемой функции, а лучше готовые API для получения параметров.
Помните, в прошлой статье мы назначали прототип функции с __usercall? Api Иды позволяет как получать прототип функции, так и его парсить!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
def get_stack_arg(offset): RET_ADDR_SIZE = 4 ADDR = idc.GetRegValue('ESP') + RET_ADDR_SIZE + offset return DbgDword(ADDR) def print_api_param(addr): tif = idaapi.tinfo_t() if not idaapi.get_tinfo2(addr, tif): return False funcdata = idaapi.func_type_data_t() if not tif.get_func_details(funcdata): return False for i in xrange(funcdata.size()): name = funcdata[i].name type = idaapi.print_tinfo('', 0, 0, PRTYPE_1LINE, funcdata[i].type, '', '') atype = funcdata[i].argloc.atype() value = False if atype == idaapi.ALOC_REG1: REG_OFFSET = funcdata[i].argloc.reg1() REG_NAME = idaapi.get_reg_name(REG_OFFSET, 4) value = GetRegValue(REG_NAME) if atype == idaapi.ALOC_STACK: STACK_OFFSET = funcdata[i].argloc.stkoff() value = get_stack_arg(STACK_OFFSET) if value is not False: print('{} {} = {}'.format(name, type, value)) |
После запуска скрипта, в текущий Instance пайтона будут добавлены все наши функции и мы сможем вызывать их из консоли IdaPython. Во время отладки, зайдём внутрь любой функции имеющей прототип:
1 2 3 4 |
.text:004049B0 ; size_t __cdecl strlen(const char *STR) .text:004049B0 strlen proc near .text:004049B0 jmp ds:__imp_strlen .text:004049B0 strlen endp |
1 2 |
Python>print_api_param(idc.GetRegValue('EIP')) STR const char * = 4214784 |
Весьма недурно! Наш код умеет воровать параметры из стека и регистров.
Копируем прототипы
Это магия работает только при условии, что функции назначен верный прототип. Ida автоматически назначает их IAT-ссылкам, так что просто скопируем их адресам внутри системных библиотек.
Вы знаете способ лучше? Напишите в комментах!
GetType возвращает строку с прототипом функции, но без указания имени. В ряде случаев (с mangled именами) она возвращает ошибку. Вы видели полную документацию по IdaPython? И я не видел. Таким грязным трюком мы формируем прототип с новым именем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
def copy_iat_type(iat_addr): api_addr = DbgDword(iat_addr) if not api_addr: return False base_type = GetType(iat_addr) if not base_type: return False type_arr = base_type.split('(') if len(type_arr) != 2: return False new_name = get_function_name(api_addr) new_type = type_arr[0] + ' ' + new_name + '(' + type_arr[1] + ';' return SetType(api_addr, new_type) == True def copy_iat_cb(iat_addr, name, ord): if copy_iat_type(iat_addr): print('{} - copied'.format(name)) else: print('{} - failed'.format(name)) return True enum_iat(copy_iat_cb) |
К сожалению, без ошибок в самой IDA, тоже не обошлось:
Удаляем невалидные хуки
IdaPython позволяет вызывать наши callback’и в момент разных отладочных событий. После перезапуска процесса нет никакой гарантии, что старые хуки будут стоять на правильных адресах. Поэтому мы удаляем их в момент завершения процесса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
from idaapi import * IAT_HOOKS = list() def hf_del_hook(api_addr): return DelBpt(api_addr) def hf_set_hook(api_addr): AddBpt(api_addr) IAT_HOOKS.append(api_addr) return SetBptCnd(api_addr, "hf_hook_proc()") def hf_unhook_iat_all(): global IAT_HOOKS for addr in IAT_HOOKS: hf_del_hook(addr) IAT_HOOKS = list() class MyDbgHook(DBG_Hooks): def dbg_process_exit(self, pid, tid, ea, code): hf_unhook_iat_all() try: if debughook: debughook.unhook() except: pass debughook = MyDbgHook() debughook.hook() debughook.steps = 0 |
Финальный скрипт
Скрипт пока очень сырой, так что не даю никаких гарантий касательно его работы.
Возможно из этого вырастет полноценный инструмент, как это было c SignFinder.
Скорее всего, публичных обновлений для SignFinder больше не будет, мы видели чем закончился его релиз.
Давайте посмотрим на возможности нашего скрипта:
Включив логирование интересующих нас WinApi мы легко узнаем что делает функция.
Ключевое отличие нашего скрипта в том что мы можем набить прототип любой функции внутри пользовательского кода и вместо WinApi получать чистую логику приложений!
P/S Если у вас есть интересные семплы, буду рад принять их в комментариях.
Автор, напишите статью как преобразовывать тонну асм кода в простой понятный псевдокод с помощью иды и питона, на примере vmprotect.
ахха! Вот это так запрос!