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

15 ИЮНЯ 2022

------------------------------------------------------------------------

Наш телеграм-канал: t.me/@mailsgun_online

Youtube: youtube.com/@mailsgun_online

------------------------------------------------------------------------

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

  • Tutorial

Привет всем,

Спустя довольно-таки продолжительное время с момента написания первой статьи я всё-таки решил, пусть и по чуть-чуть, но писать статьи на тему модификации/улучшения 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%sn";
    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("]n");
    }
}
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("]n");
    }
}
#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 = %dn", 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
    enum m68k_insn_type_t
    {
    M68K_linea = CUSTOM_INSN_ITYPE,
    M68K_linef,
    };
  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

Source: habr1

Ну и напоследок напоминаю вам о нашем новом проекте vkserfing bot.