Модернизация IDA Pro. Исправляем косяки процессорных модулей

Модернизация IDA Pro. Исправляем косяки процессорных модулей


Привет всем,


Спустя довольно таки продолжительное время с момента написания первой статьи я всё таки решил, пусть и по чуть-чуть, но писать статьи на тему модификации/улучшения всеми любимой и одновременно презираемой IDA Pro.


В этой статье пойдёт речь о том, как всё таки правильно исправлять косяки в тех процессорных модулях, исходников которых у вас нет, а косяки прям таки жить не дают. К сожалению, не все указанные ниже проблемы можно отнести к косякам. Да, и, основная, по сути, причина, по которой не хочется слать баг-репорт разработчикам: исправление выйдет для той версии, к которой получат доступ не все желающие (ну, сами понимаете почему).


Локализуем баги


Примечание: здесь и далее будут рассматриваться ошибки в модуле Motorola M68000 (моём самом любимом и очень часто используемом).


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

Кажется, что ошибки здесь нет. Более того, её наличие не мешает анализу. Но, опкод дизассемблирован неправильно. Посмотрим на дизазм в каком-нибудь онлайн-дизассемблере:

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


Косяк два: "зеркала" для оперативной памяти, и некоторых других регионов. Т.к. адресация в m68k 24-битная, то все обращения к старшим (или наоборот, младшим) регионам, должны быть переадресованы на один и тот же диапазон, как и перекрёстные ссылки.


Косяк три (скорее, даже не косяк, а отсутствие функционала): так называемые lineA (1010) и lineF (1111) эмуляторы. Это такие опкоды, на которые не хватило основного набора команд, поэтому они должны обрабатываться специальным образом векторами прерываний. Размер опкодов зависит лишь от реализации на уровне обработчика. Я видел лишь двухбайтовую реализацию. Будем добавлять.


Косяк четыре: trap #N инструкции не дают cref-ов на сами обработчики "ловушек".


Косяк пять: movea.w инструкция должна делать из word-ссылки полный xref на адрес, но мы имеем только word-число.


Исправляем баги (шаблон)


Для того, чтобы понять, как исправлять конкретный процессорный модуль, нужно понять, какие возможности на эту тему у нас есть в принципе и что из себя представляет "фикс".


Собственно, "исправлялка" представляет из себя обычный плагин. Его, вроде как, можно написать и на Python, но, я всё делал в "плюсах". Страдает лишь портабельность, но, если кто-то возьмётся переписать плагин на Python — буду очень благодарен.


Для начала создадим пустой DLL-проект в Visual Studio: File->New->Project->Windows Desktop Wizard->Dynamic link library (.dll), поставив также галку Empty Project, и сняв все остальные:


Распакуем IDA SDK, и пропишем его в макросах Visual Studio (я буду использовать 2017), чтобы в будущем можно было легко ссылаться на него. Заодно мы добавим макрос для пути к IDA Pro.


Заходим в View->Other Windows->Property Manager:


Т.к. мы работаем с версией SDK 7.0, компиляция будет происходить x64-компилятором. Поэтому выбираем Debug | x64->Microsoft.Cpp.x64.user->Properties:


Жмём кнопку Add Macro в разделе User Macros, и прописываем там макрос IDA_SDK с указанием пути, по которому вы распаковали SDK:


Так же поступаем с IDA_DIR (путь к Вашей IDA Pro):

Замечу, что IDA ставится по умолчанию в %Program Files%, что требует прав администратора.


Давайте также удалим Win32 конфигурацию (в данной статье я не буду затрагивать компиляцию по x86 системы), оставив только x64-вариант.


Создадим пустой файл ida_plugin.cpp. Код пока не добавляем.
Теперь появилась возможность выставить кодировку, и другие настройки для C++:




Пропишем инклуды:


И библиотеки из SDK:



Теперь добавим шаблон кода:


Код ida_plugin.cpp
#include <ida.hpp> #include <idp.hpp> #include <ua.hpp> #include <bytes.hpp> #include <loader.hpp> #include <offset.hpp>  #define NAME "M68000 proc-fixer plugin" #define VERSION "1.0"  static bool plugin_inited; static bool my_dbg;  //-------------------------------------------------------------------------- static void print_version() {     static const char format[] = NAME " v%s
"; info(format, VERSION); msg(format, VERSION); } //-------------------------------------------------------------------------- static bool init_plugin(void) { if (ph.id != PLFM_68K) return false; return true; } #ifdef _DEBUG static const char* const optype_names[] = { "o_void", "o_reg", "o_mem", "o_phrase", "o_displ", "o_imm", "o_far", "o_near", "o_idpspec0", "o_idpspec1", "o_idpspec2", "o_idpspec3", "o_idpspec4", "o_idpspec5", }; static const char* const dtyp_names[] = { "dt_byte", "dt_word", "dt_dword", "dt_float", "dt_double", "dt_tbyte", "dt_packreal", "dt_qword", "dt_byte16", "dt_code", "dt_void", "dt_fword", "dt_bitfild", "dt_string", "dt_unicode", "dt_3byte", "dt_ldbl", "dt_byte32", "dt_byte64", }; static void print_insn(const insn_t *insn) { if (my_dbg) { msg("cs=%x, ", insn->cs); msg("ip=%x, ", insn->ip); msg("ea=%x, ", insn->ea); msg("itype=%x, ", insn->itype); msg("size=%x, ", insn->size); msg("auxpref=%x, ", insn->auxpref); msg("segpref=%x, ", insn->segpref); msg("insnpref=%x, ", insn->insnpref); msg("insnpref=%x, ", insn->insnpref); msg("flags["); if (insn->flags & INSN_MACRO) msg("INSN_MACRO|"); if (insn->flags & INSN_MODMAC) msg("OF_OUTER_DISP"); msg("]
"); } } static void print_op(ea_t ea, const op_t *op) { if (my_dbg) { msg("type[%s], ", optype_names[op->type]); msg("flags["); if (op->flags & OF_NO_BASE_DISP) msg("OF_NO_BASE_DISP|"); if (op->flags & OF_OUTER_DISP) msg("OF_OUTER_DISP|"); if (op->flags & PACK_FORM_DEF) msg("PACK_FORM_DEF|"); if (op->flags & OF_NUMBER) msg("OF_NUMBER|"); if (op->flags & OF_SHOW) msg("OF_SHOW"); msg("], "); msg("dtyp[%s], ", dtyp_names[op->dtype]); if (op->type == o_reg) msg("reg=%x, ", op->reg); else if (op->type == o_displ || op->type == o_phrase) msg("phrase=%x, ", op->phrase); else msg("reg_phrase=%x, ", op->phrase); msg("addr=%x, ", op->addr); msg("value=%x, ", op->value); msg("specval=%x, ", op->specval); msg("specflag1=%x, ", op->specflag1); msg("specflag2=%x, ", op->specflag2); msg("specflag3=%x, ", op->specflag3); msg("specflag4=%x, ", op->specflag4); msg("refinfo["); opinfo_t buf; if (get_opinfo(&buf, ea, op->n, op->flags)) { msg("target=%x, ", buf.ri.target); msg("base=%x, ", buf.ri.base); msg("tdelta=%x, ", buf.ri.tdelta); msg("flags["); if (buf.ri.flags & REFINFO_TYPE) msg("REFINFO_TYPE|"); if (buf.ri.flags & REFINFO_RVAOFF) msg("REFINFO_RVAOFF|"); if (buf.ri.flags & REFINFO_PASTEND) msg("REFINFO_PASTEND|"); if (buf.ri.flags & REFINFO_CUSTOM) msg("REFINFO_CUSTOM|"); if (buf.ri.flags & REFINFO_NOBASE) msg("REFINFO_NOBASE|"); if (buf.ri.flags & REFINFO_SUBTRACT) msg("REFINFO_SUBTRACT|"); if (buf.ri.flags & REFINFO_SIGNEDOP) msg("REFINFO_SIGNEDOP"); msg("]"); } msg("]
"); } } #endif static bool ana_addr = 0; static ssize_t idaapi hook_idp(void *user_data, int notification_code, va_list va) { switch (notification_code) { case processor_t::ev_ana_insn: { insn_t *out = va_arg(va, insn_t*); if (ana_addr) break; ana_addr = 1; if (ph.ana_insn(out) <= 0) { ana_addr = 0; break; } ana_addr = 0; #ifdef _DEBUG print_insn(out); #endif for (int i = 0; i < UA_MAXOP; ++i) { op_t &op = out->ops[i]; #ifdef _DEBUG print_op(out->ea, &op); #endif } return out->size; } break; case processor_t::ev_emu_insn: { const insn_t *insn = va_arg(va, const insn_t*); } break; case processor_t::ev_out_mnem: { outctx_t *outbuffer = va_arg(va, outctx_t *); //outbuffer->out_custom_mnem(mnem); //return 1; } break; default: { #ifdef _DEBUG if (my_dbg) { msg("msg = %d
", notification_code); } #endif } break; } return 0; } //-------------------------------------------------------------------------- static int idaapi init(void) { if (init_plugin()) { plugin_inited = true; my_dbg = false; hook_to_notification_point(HT_IDP, hook_idp, NULL); print_version(); return PLUGIN_KEEP; } return PLUGIN_SKIP; } //-------------------------------------------------------------------------- static void idaapi term(void) { if (plugin_inited) { unhook_from_notification_point(HT_IDP, hook_idp); plugin_inited = false; } } //-------------------------------------------------------------------------- static bool idaapi run(size_t /*arg*/) { return false; } //-------------------------------------------------------------------------- const char comment[] = NAME; const char help[] = NAME; //-------------------------------------------------------------------------- // // PLUGIN DESCRIPTION BLOCK // //-------------------------------------------------------------------------- plugin_t PLUGIN = { IDP_INTERFACE_VERSION, PLUGIN_PROC | PLUGIN_MOD, // plugin flags init, // initialize term, // terminate. this pointer may be NULL. run, // invoke plugin comment, // long comment about the plugin // it could appear in the status line // or as a hint help, // multiline help about the plugin NAME, // the preferred short name of the plugin "" // the preferred hotkey to run the plugin };

Исправляем баги (разбираемся в шаблоне)


Функции print_op() и print_insn() нужны для того, чтобы понять, какие флаги выставлены текущим процессорным модулем для определённых инструкций. Это нужно, если мы хотим найти какие-то флаги для имеющихся опкодов, чтобы потом использовать их при исправлении.


Собственно, телом нашей "исправлялки" является функция hook_idp(). В ней для наших нужд потребуется реализовать три колбэка:


  1. processor_t::ev_ana_insn: нужен, если в процессорном модуле отсутствует реализация некоторых опкодов
  2. processor_t::ev_emu_insn: тут можно создать кросс-рефы на данные/код, на которые ссылаются новые опкоды (либо не ссылаются старые)
  3. processor_t::ev_out_mnem: новые опкоды должны как-то выводиться. Это всё здесь

Функция init_plugin() не даёт загрузиться нашей исправлялке в других процессорных модулях.
Ну и, самое главное — вешаем весь колбэк на события процессорного модуля:


hook_to_notification_point(HT_IDP, hook_idp, NULL);

Трюк с глобальной переменной ana_addr нужен для того, чтобы ana_insn не уходил в рекурсию при попытке получить информацию об инструкции, которую мы не парсим вручную. Да, увы, этот "костыль" тянется очень давно, ещё со старых версий.

Исправление для проблемы №1


Для того, чтобы нормально решить эту проблему пришлось много повозиться с дебажным выводом, который я как раз реализовал для этой задачи. Я знал, что в некоторых случаях IDA успешно выводит ссылки относительно PC (в инструкциях, где происходит прыжок под таблице смещений, которая находится недалеко от текущей инструкции, плюс регистр-индекс), но для инструкции lea правильного отображения адресации не реализовано. В итоге, я нашёл такую инструкцию с прыжком, и выяснил, какие флаги нужно установить, чтобы PC со скобками отображался:



Исправление проблемы №1
case processor_t::ev_ana_insn: {     insn_t *out = va_arg(va, insn_t*);      if (ana_addr)         break;      ana_addr = 1;      if (ph.ana_insn(out) <= 0)     {         ana_addr = 0;         break;     }      ana_addr = 0;      for (int i = 0; i < UA_MAXOP; ++i)     {         op_t &op = out->ops[i];          switch (op.type)         {         case o_near:         case o_mem:         {             if (out->itype != 0x76 || op.n != 0 ||                 (op.phrase != 0x09 && op.phrase != 0x0A) ||                 (op.addr == 0 || op.addr >= (1 << 23)) ||                 op.specflag1 != 2) // lea table(pc),Ax                 break;              short diff = op.addr - out->ea;             if (diff >= SHRT_MIN && diff <= SHRT_MAX)             {                 out->Op1.type = o_displ;                 out->Op1.offb = 2;                 out->Op1.dtype = dt_dword;                 out->Op1.phrase = 0x5B;                 out->Op1.specflag1 = 0x10;             }         } break;         }     }      return out->size; } break;

Исправление для проблемы №2


Тут всё просто. Просто маскируем адреса на конкретный диапазон: 0xFF0000-0xFFFFFF (для RAM) и 0xC00000-0xC000FF (для видеопамяти VDP). Тут главное фильтровать по типу операнда o_near и o_mem.


Исправление проблемы №2
case processor_t::ev_ana_insn: {     insn_t *out = va_arg(va, insn_t*);      if (ana_addr)         break;      ana_addr = 1;      if (ph.ana_insn(out) <= 0)     {         ana_addr = 0;         break;     }      ana_addr = 0;      for (int i = 0; i < UA_MAXOP; ++i)     {         op_t &op = out->ops[i];          switch (op.type)         {         case o_near:         case o_mem:         {             op.addr &= 0xFFFFFF; // for any mirrors              if ((op.addr & 0xE00000) == 0xE00000) // RAM mirrors                 op.addr |= 0x1F0000;              if ((op.addr >= 0xC00000 && op.addr <= 0xC0001F) ||                 (op.addr >= 0xC00020 && op.addr <= 0xC0003F)) // VDP mirrors                 op.addr &= 0xC000FF;         } break;         }     }      return out->size; } break;

Исправление для проблемы №3


Собственно, чтобы добавить нужный опкод, необходимо:


  1. Определить индексы для новых опкодов. Все новые индексы должны начинаться с CUSTOM_INSN_ITYPE
  2. lineA/lineF опкоды срабатывают, если в коде встречаются байты: 0xA0/0xF0. Значит, читаем один байт
  3. Получить ссылку на вектор-обработчик. В первых 64-х двордах заголовка в моём случае находятся вектора прерываний. На позициях 0x0A и 0x0B находятся обработчики lineA/lineF:
    value = get_dword(0x0A * sizeof(uint32)); // ... value = get_dword(0x0B * sizeof(uint32));
  4. В ev_emu_insn добавляем cref-ы на обработчики и на следующую инструкцию, чтобы code-flow не прерывался:
        insn->add_cref(insn->Op1.addr, 0, fl_CN); // code ref     insn->add_cref(insn->ea + insn->size, insn->Op1.offb, fl_F); // flow ref
  5. В ev_out_mnem выводим наш кастомный опкод:
    const char *mnem = (outbuffer->insn.itype == M68K_linef) ? "line_f" : "line_a"; outbuffer->out_custom_mnem(mnem);


Решение проблемы №3
enum m68k_insn_type_t {     M68K_linea = CUSTOM_INSN_ITYPE,     M68K_linef, }; /* after includes */  case processor_t::ev_ana_insn: {     insn_t *out = va_arg(va, insn_t*);      if (ana_addr)         break;      uint16 itype = 0;     ea_t value = out->ea;     uchar b = get_byte(out->ea);      if (b == 0xA0 || b == 0xF0)     {         switch (b)         {         case 0xA0:             itype = M68K_linea;             value = get_dword(0x0A * sizeof(uint32));             break;         case 0xF0:             itype = M68K_linef;             value = get_dword(0x0B * sizeof(uint32));             break;         }          out->itype = itype;         out->size = 2;          out->Op1.type = o_near;         out->Op1.offb = 1;         out->Op1.dtype = dt_dword;         out->Op1.addr = value;         out->Op1.phrase = 0x0A;         out->Op1.specflag1 = 2;          out->Op2.type = o_imm;         out->Op2.offb = 1;         out->Op2.dtype = dt_byte;         out->Op2.value = get_byte(out->ea + 1);     }      return out->size; } break; case processor_t::ev_emu_insn: {     const insn_t *insn = va_arg(va, const insn_t*);      if (insn->itype == M68K_linea || insn->itype == M68K_linef)     {         insn->add_cref(insn->Op1.addr, 0, fl_CN);         insn->add_cref(insn->ea + insn->size, insn->Op1.offb, fl_F);         return 1;     } } break; case processor_t::ev_out_mnem: {     outctx_t *outbuffer = va_arg(va, outctx_t *);      if (outbuffer->insn.itype != M68K_linea && outbuffer->insn.itype != M68K_linef)         break;      const char *mnem = (outbuffer->insn.itype == M68K_linef) ? "line_f" : "line_a";      outbuffer->out_custom_mnem(mnem);     return 1; } break;

Исправление для проблемы №4


Решается так: находим опкод для инструкции trap, получаем индекс из инструкции индекс, и берём по индексу вектор-обработчик. Получится что-то типа того:



Решение проблемы №4
case processor_t::ev_emu_insn: {     const insn_t *insn = va_arg(va, const insn_t*);      if (insn->itype == 0xB6) // trap #X     {         qstring name;         ea_t trap_addr = get_dword((0x20 + (insn->Op1.value & 0xF)) * sizeof(uint32));         get_func_name(&name, trap_addr);         set_cmt(insn->ea, name.c_str(), false);         insn->add_cref(trap_addr, insn->Op1.offb, fl_CN);         return 1;     } } break;

Исправление для проблемы №5


Тут тоже всё просто: сначала фильтруем по операции movea.w. Затем, если операнд типа word, и ссылается на RAM, делаем ссылку по-крутому, относительно базы 0xFF0000. Выглядеть это будет так:



Исправление проблемы №5
case processor_t::ev_ana_insn: {     insn_t *out = va_arg(va, insn_t*);      if (ana_addr)         break;      ana_addr = 1;      if (ph.ana_insn(out) <= 0)     {         ana_addr = 0;         break;     }      ana_addr = 0;      for (int i = 0; i < UA_MAXOP; ++i)     {         op_t &op = out->ops[i];          switch (op.type)         {         case o_imm:         {             if (out->itype != 0x7F || op.n != 0) // movea                 break;              if (op.value & 0xFF0000 && op.dtype == dt_word) {                 op.value &= 0xFFFF;             }         } break;         }     }      return out->size; } break; case processor_t::ev_emu_insn: {     const insn_t *insn = va_arg(va, const insn_t*);      for (int i = 0; i < UA_MAXOP; ++i)     {         const op_t &op = insn->ops[i];          switch (op.type)         {         case o_imm:         {             if (insn->itype != 0x7F || op.n != 0 || op.dtype != dt_word) // movea                 break;              op_offset(insn->ea, op.n, REF_OFF32, BADADDR, 0xFF0000);         } break;         }     } } break;

Выводы


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


Ссылка на исходник: https://github.com/lab313ru/m68k_fixer

Alt text

Подписывайтесь на каналы "SecurityLab" в TelegramTelegram и TwitterTwitter, чтобы первыми узнавать о новостях и эксклюзивных материалах по информационной безопасности.