+ Reply to Thread
Results 1 to 1 of 1

Thread: Пишем простой эмулятор x86 кода с помощью IDAPython

  1. #1

    Default Пишем простой эмулятор x86 кода с помощью IDAPython

    Оригинал: 0xeb.net

    Частенько когда натыкаюсь на IDAPython скрипты, я замечаю, что они используют неэффективные/некорректные функции библиотеки для дизассемблинга инструкций (к примеру idc.GetMnem() или idc.GetDisasm()). Потому в этом посте я постараюсь проиллюстрировать как использовать функция для декодирования из IDAPython для написания простого эмулятора под архитектуру x86. Его цель продемонстрировать корректное использование декодирования интерфейса IDAPython. В конце этого поста вы сможете решать похожие проблемы с помощью этого плагина.

    Обзор

    Давайте начнем со следующей простой программой, которая содержит таблицу из 12 функций, которые вызываются в цикле, а затем их результат выводится на экран. Наша цель в том, чтобы написать эмулятор, который сможет вычислить ответы программы статично, без использования дополнительных эмуляторов.
    Code: C/C++

    #include <stdio.h>
    #include <cstdint>
    #include <time.h>
    #include <stdlib.h>

    typedef uint64_t (*challenge_proto_t)(uint32_t, uint32_t);

    //-------------------------------------------------------------------------
    uint64_t challenge_1(uint32_t c1, uint32_t c2) // 39 operation(s)
    {
    uint64_t result;
    __asm
    {
    pushad
    mov eax, [c1]
    mov edx, [c2]
    not eax
    dec edx
    xor edx, eax
    xor edx, eax
    inc eax
    not eax
    sub edx, 0x27c12466
    inc eax
    dec edx
    not edx
    inc eax
    add eax, 0x273804ca
    xor edx, 0xaa5a1584
    sub eax, edx
    not edx
    xor eax, 0xf94f7d8c
    dec edx
    dec eax
    sub eax, edx
    not edx
    dec edx
    sub edx, 0xd7b41b83
    xor eax, 0xa551a9c7
    add eax, eax
    dec eax
    inc eax
    not eax
    add edx, 0xa551b974
    inc edx
    dec edx
    not edx
    xor eax, 0x200d519
    not edx
    not eax
    sub edx, 0xeb15b7ef
    xor eax, 0xb2558b8c
    xor eax, 0xda288d90
    not eax
    not edx
    mov dword ptr[result], eax
    mov dword ptr[result + 4], edx

    popad
    }
    return result;
    }

    // .
    // .
    // challenge_2 .. challenge_12
    // .
    // .

    challenge_proto_t challenge_funcs[] = {
    challenge_1,
    challenge_2,
    challenge_3,
    challenge_4,
    challenge_5,
    challenge_6,
    challenge_7,
    challenge_8,
    challenge_9,
    challenge_10,
    challenge_11,
    challenge_12
    };

    //-------------------------------------------------------------------------
    int main(int argc, char *argv[])
    {
    if (argc < 4)
    {
    printf("challenge func[0..%d] challenge1-32 challenge2-32 -> result-64\n", _countof(challenge_funcs));
    return -1;
    }

    uint32_t f = atol(argv[1]) % _countof(challenge_funcs);

    uint32_t c1 = atol(argv[2]);
    uint32_t c2 = atol(argv[3]);

    printf("challenge_%d(%d, %d) -> %016I64X\n", f, c1, c2, challenge_funcs[f](c1, c2));

    return 0;
    }

    В реальной жизни часто не хочется разбираться с внутренностями подобной функции, а просто обращатся с ней как с черным ящиком. В этом случае есть несколько вариантов:
    • Использовать Appcall во времени исполнения и вызывать функции напрямую
    • Использовать декомпилятор, чтобы получить алгоритм в псевдокоде, затем перекомпилировать и использовать
    • Извлечь ассемблер функций challenge_funcs, а затем использовать их после реассемблирования
    • Использовать фреймворк для эмуляции (к примеру Unicorn engine) для эмуляции функций
    Давайте попробуем скомпилировать программу и запустить ее с c1=123 и c2=456.
    Code:
    C:\>for /l %a in (0, 1, 11) do @test.exe %a 123 456
    challenge_0(123, 456)  -> 8FDCE2E203FCAAF2
    challenge_1(123, 456)  -> E0317E1AB061ED8B
    challenge_2(123, 456)  -> A0A0E0C2279BE734
    challenge_3(123, 456)  -> 5D18D0A79D07D7D8
    challenge_4(123, 456)  -> 2583B4EEB62E6042
    challenge_5(123, 456)  -> D5261E0275AB9805
    challenge_6(123, 456)  -> F2B3282E143F7927
    challenge_7(123, 456)  -> 9B9B3CBB0169F4CD
    challenge_8(123, 456)  -> EF51086C5D1AF235
    challenge_9(123, 456)  -> FC8A97125C0EA232
    challenge_10(123, 456) -> EEAE8BEB7996D2E7
    challenge_11(123, 456) -> 4F36E6A65AB03929
    Дизассемблинг программы

    Далее попробуем дизассемблировать тестовую программу и найти в ней таблицу challenge_funcs и первую функцию в ней.
    Code: ASM

    ;
    ; The challenge functions as referenced from main()
    ; (12 functions)
    ;
    .rdata:0041749C 00 10 40 00 challenge_funcs dd offset sub_401000
    .rdata:0041749C ; DATA XREF: _main+57↑r
    .rdata:004174A0 90 10 40 00 dd offset sub_401090
    .rdata:004174A4 30 11 40 00 dd offset sub_401130
    .rdata:004174A8 D0 11 40 00 dd offset sub_4011D0
    .rdata:004174AC 80 12 40 00 dd offset sub_401280
    .rdata:004174B0 30 13 40 00 dd offset sub_401330
    .rdata:004174B4 A0 13 40 00 dd offset sub_4013A0
    .rdata:004174B8 30 14 40 00 dd offset sub_401430
    .rdata:004174BC C0 14 40 00 dd offset sub_4014C0
    .rdata:004174C0 30 15 40 00 dd offset sub_401530
    .rdata:004174C4 E0 15 40 00 dd offset sub_4015E0
    .rdata:004174C8 80 16 40 00 dd offset sub_401680

    ;
    ; The first challenge function
    ;
    .text:00401000 ; int __cdecl sub_401000(int c1, int c2)
    .text:00401000 sub_401000 proc near
    .text:00401000 ; CODE XREF: _main+65↓p
    .text:00401000 ; DATA XREF: .rdata:challenge_funcs↓o
    .text:00401000
    .text:00401000 var_8= dword ptr -8
    .text:00401000 var_4= dword ptr -4
    .text:00401000 c1= dword ptr 8
    .text:00401000 c2= dword ptr 0Ch
    .text:00401000
    .text:00401000 55 push ebp
    .text:00401001 8B EC mov ebp, esp
    .text:00401003 83 EC 08 sub esp, 8
    .text:00401006 53 push ebx
    .text:00401007 56 push esi
    .text:00401008 57 push edi
    .text:00401009 60 pusha
    .text:0040100A 8B 45 08 mov eax, [ebp+8]
    .text:0040100D 8B 55 0C mov edx, [ebp+c2]
    .text:00401010 F7 D0 not eax
    .text:00401012 4A dec edx
    .text:00401013 33 D0 xor edx, eax
    .text:00401015 33 D0 xor edx, eax
    .text:00401017 40 inc eax
    .text:00401018 F7 D0 not eax
    .text:0040101A 81 EA 66 24 C1 27 sub edx, 27C12466h
    .text:00401020 40 inc eax
    .text:00401021 4A dec edx
    .text:00401022 F7 D2 not edx
    .
    .
    .
    .text:00401076 F7 D2 not edx
    .text:00401078 89 45 F8 mov [ebp+var_8], eax
    .text:0040107B 89 55 FC mov [ebp+var_4], edx
    .text:0040107E 61 popa
    .text:0040107F 8B 45 F8 mov eax, [ebp+var_8]
    .text:00401082 8B 55 FC mov edx, [ebp+var_4]
    .text:00401085 5F pop edi
    .text:00401086 5E pop esi
    .text:00401087 5B pop ebx
    .text:00401088 8B E5 mov esp, ebp
    .text:0040108A 5D pop ebp
    .text:0040108B C3 retn
    .text:0040108B
    .text:0040108B sub_401000 endp
    .text:0040108B

    Мы можем с легкостью обнаружить таблицу challenge_funcs, поскольку на нее ссылается код из функции main. Первая функция из таблицы, подобно остальным, прослеживается четкий форма, на основе которой мы и будем проектировать наш эмулятор.

    Везде присутствует инструкция pusha (0x401009), после которой следуют две инструкции загружающие начальные значения (0x40100A и 0x40100D). Далее идет последовательность операций (между 0x401010 и 0x401076), работающая с этими регистрами и в конце мы видим, что результаты копируются в локальную переменные (at 0x401078 and 0x40107B) перед popa, который нужен для восстановления регистрам прошлых значений.

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

    Написание эмулятора

    В этом разделе мы напишем эти две функции:
    • scope_challenge_function(): эта функция находит границы инструкций для последующей эмуляции
    • emulate_challenge_function(): эта функция эмулирует инструкции в рамках данных границ
    • Перед тем как мы начнем, определим некоторые глобальные переменные для скрипта:
    Code: Python

    import idc, idautils, idaapi

    challenge_funcs_tbl = 0x41749C
    challenge_funcs_tbl_size = 12

    RESULTS = (
    0x8FDCE2E203FCAAF2,
    0xE0317E1AB061ED8B,
    0xA0A0E0C2279BE734,
    0x5D18D0A79D07D7D8,
    0x2583B4EEB62E6042,
    0xD5261E0275AB9805,
    0xF2B3282E143F7927,
    0x9B9B3CBB0169F4CD,
    0xEF51086C5D1AF235,
    0xFC8A97125C0EA232,
    0xEEAE8BEB7996D2E7,
    0x4F36E6A65AB03929)

    Здесь мы вычислили адрес таблицы функций, ее размер, а также результаты полученные нами ранее (после вызова с параметрами c1=123 и c2=456). Далее мы сможем использовать эту таблицы для проверки нашего кода.

    Для обхода всех функций из таблицы мы можем выполнить:
    Code: Python

    for i in range(0, challenge_funcs_tbl_size):
    func = idc.Dword(challenge_funcs_tbl + i * 4)
    print(">%x: challenge #%d" % (func, i + 1))

    Вывод будет следующим:
    Code:
    >401000: challenge #1
    >401090: challenge #2
    >401130: challenge #3
    >4011d0: challenge #4
    >401280: challenge #5
    >401330: challenge #6
    >4013a0: challenge #7
    >401430: challenge #8
    >4014c0: challenge #9
    >401530: challenge #10
    >4015e0: challenge #11
    >401680: challenge #12
    Беглое введение в декодирование инструкций

    Для декодирования инструкции с помощью IDAPython, следует использовать функцию idautils.DecodeInstruction():
    Code: Python

    # .text:0040101A 81 EA 66 24 C1 27 sub edx, 27C12466h
    inst = idautils.DecodeInstruction(0x40101A)

    Если декодирование будет не успешным, тогда эта функция вернет None. В случае успеха мы получим объект, хранящий информацию об инструкции и ее операндах.

    Далее следуют важные поля класса инструкции:
    • inst.itype: эта целочисленная переменная отображает тип инструкции. Разные опкоды могут иметь одинаковый itype
    • inst.size: размер декодированной инструкции
    • inst.Operands[]: список информации об операндах инструкции (индексация начинается с нуля)
    • inst.Op1 .. OpN: альтернативный способ получения информации
    • inst.ea: линейный адрес декодированной инструкции
    Вам возможно интересно, какая связь между опкодом и itype? Ответ прост: модуль процессора текущей базы данных в IDA ответственен за заполнение itype, на основе опкода. В IDA SDK можно обнаружить заголовочный файл allins.hpp. Этот файл содержит перечисления для всех поддерживаемых модулей процессора вместе с соответствующей поддерживаемой инструкцией:
    Code: C/C++

    // Excerpt from allins.hpp

    // x86/x64 itypes
    enum
    {
    NN_null = 0, // Unknown Operation
    NN_aaa, // ASCII Adjust after Addition
    NN_aad, // ASCII Adjust AX before Division
    NN_aam, // ASCII Adjust AX after Multiply
    NN_aas, // ASCII Adjust AL after Subtraction
    .
    .
    .
    NN_jz, // Jump if Zero (ZF=1)
    NN_jmp, // Jump
    NN_jmpfi, // Indirect Far Jump
    NN_jmpni, // Indirect Near Jump
    NN_jmpshort, // Jump Short (not used)
    NN_lahf, // Load Flags into AH Register
    .
    .
    .
    // Pentium III Pseudo instructions

    NN_cmpeqps, // Packed Single-FP Compare EQ
    NN_cmpltps, // Packed Single-FP Compare LT
    NN_cmpleps, // Packed Single-FP Compare LE
    NN_cmpunordps, // Packed Single-FP Compare UNORD
    .
    .
    .
    }

    Я не знаю почему, но NN_ префикс использовается для всех типов инструкций для x86/x64 процессоров.

    Вот простой пример для декодирования и проверки типа инструкции:
    Code: Python

    # .text:00402085 74 09 jz short loc_402090
    inst = idautils.DecodeInstruction(0x402085)
    print("YES" if inst.itype == idaapi.NN_jz else "NO")

    Соответственно можно проверить декодированную инструкцию с одной из необходимых idaapi.NN_xxx констант.

    Что касается операндов, то к ним можно получить доступ с помощью inst.Operands[] или inst.OpN. Для получения количества элементов не следует полагаться на длинну массива, поскольку он всегда аллоцируется с размером UA_MAXOP == 8 (определен в файле ida.hpp). Вместо этого следует проитерироваться по элементам и проверить, что элемент имеет тип, отличный от o_void.

    Операнд инструкции определен с помощью структуры op_t, определенной в ua.hpp.

    Некоторые из ее полей:
    • op.flags: флаги операндов
    • op.dtype: тип операнда, один из констант dt_xxx. По этому полю можно определить размер операнда (1 == dt_byte, 2 == dt_word, и т.д)
    • op.type: тип операнда, один из констант o_xxx
    • specflag1.. specflag4: флаги, специфичные для процессора
    Далее приведен список поддерживаемых типов операндов (o_xxx):
    • o_void: операнд отсутствует
    • o_reg: операнд является регистром (al, ax,es,ds…)
    • o_mem: прямая ссылка на память (DATA)
    • o_phrase: ссылка на память [Base Reg + Index Reg]
    • o_displ: ссылка на память [Base Reg + Index Reg + Displacement]
    • o_imm: константа
    • o_far: константа, ссылающаяся на удаленный код (CODE)
    • o_near: константа, ссылающаяся на не удаленный код (CODE)
    • o_idpspec0 .. o_idpspec5: флаги, специфичные для процессора
    Помимо этого существуют дополнительные члены операндов, назначение которых определяется типом операнда:
    • op.reg: номер регистра
    • op.phrase: индексный регистр, с доступом к памяти (o_phrase).
    • op.value: константа (o_imm) или внешнее смещение (o_displ).
    • op.addr: адрес памяти, используемой операндом (o_mem, o_far, o_displ, o_near).
    Когда операнд имеет тип o_reg или o_phrase, тогда значения op.reg/op.phrase содержат значение регистра (в виде констант перечисления). Подобно именам NN_xxx, IDA SDK также предоставляет константы для имен регистров и их значения. Однако это так лишь для x86/x64 модуля процессора. Вот отрывок из заголовочного файла intel.hpp:
    Code: C/C++

    enum RegNo
    {
    R_ax = 0,
    R_cx,
    R_dx,
    R_bx,
    R_sp,
    R_bp,
    R_si,
    R_di,
    R_r8,
    R_r9,
    R_r10,
    R_r11,
    R_r12,
    R_r13,
    R_r14,
    R_r15,
    .
    .
    .
    }

    К сожалению эти перечисления не видны в IDAPython, но по крайней мере мы знаем достаточно для определения этих значений самим:
    Code: C/C++

    REG_EAX = 0
    REG_EDX = 2
    REG_EBP = 5
    .
    .
    .
    REG_NAMES = { REG_EAX: 'eax', REG_EDX: 'edx', REG_EBP: 'ebp' ...}

    Вот пример, как мы можем полностью дизассемблировать инструкцию:
    Code: Python

    # .text:0040106F 35 90 8D 28 DA xor eax, 0DA288D90h
    out = ''
    inst = idautils.DecodeInstruction(0x40106F)
    out += "XOR " if inst.itype == idaapi.NN_xor else ""
    out += "EAX" if (inst.Op1.type == idaapi.o_reg and inst.Op1.reg == 0) else ""
    out += ", 0x%08X" % inst.Op2.value if (inst.Op2.type == idaapi.o_imm) else ""
    print(out)
    # Outputs: XOR EAX, 0xDA288D90

    Для дополнительной информации можно обратиться к файлам intel.hpp, allins.hpp, ua.hpp, idp.hpp.

    Определение границ функций

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

    Стоит обратить внимание, что для этих целей можно использовать функцию FindBinary(), но она не соответствует цели этой статьи. Исключительно для демонстрирования кода выше, следующий способ позвояет искать шаблоны кода лишь с помощью декодирования инструкций:
    Code: Python

    def scope_challenge_function(func_ea):
    f = idaapi.get_func(func_ea)
    if f is None:
    return (False, "No function at address!")

    emu_start, emu_end = f.startEA, f.endEA

    ea = emu_start

    #
    # Find the start of the emulation pattern
    #
    stage = 0
    while ea <= emu_end:
    inst = idautils.DecodeInstruction(ea)
    if inst is None:
    return (False, "Could not decode")

    # Advance to next instruction
    ea += inst.size

    # mov (eax|edx), [ebp+?]
    if (inst.itype == idaapi.NN_mov) and (inst.Operands[0].type == idaapi.o_reg) and \
    (inst.Operands[1].type == idaapi.o_displ) and (inst.Operands[1].phrase == REG_EBP):
    # mov eax, [ebp+8]
    if (stage == 0) and (inst.Operands[0].reg == REG_EAX) and (inst.Operands[1].addr == 8):
    stage = 1
    # mov edx, [ebp+0xC]
    elif (stage == 1) and (inst.Operands[0].reg == REG_EDX) and (inst.Operands[1].addr == 0xC):
    stage = 2
    emu_start = ea
    elif (stage == 2) and (inst.itype == idaapi.NN_popa):
    # Let's decode backwards twice and double check the pattern
    ea2 = idc.PrevHead(ea)

    # Disassemble backwards
    for _ in range(0, 2):
    ea2 = idc.PrevHead(ea2)

    inst = idautils.DecodeInstruction(ea2)
    if (inst.itype == idaapi.NN_mov) and (inst.Op1.type == idaapi.o_displ) and \
    (inst.Op1.reg == 5):
    if inst.Op2.reg == 2 and stage == 2:
    stage = 3
    elif inst.Op2.reg == 0 and stage == 3:
    stage = 4
    emu_end = ea2
    break

    break


    if stage != 4:
    return (False, "Could not find markers")

    return (True, (emu_start, emu_end))

    Для декодирования инструкции необходимо продвинутся по адресу декодирования (переменная ea в данном случае) на inst.size после каждого успешного декодирования. После этого следует проверить itype инструкции, а затем и операнды.

    Заметьте, на втором этапе (stage==2), я начал декодирование в обратную сторону. Для прохода назад по ассемблерному код можно использовать функцию idc.PrevHead() для получения начала предыдущей инструкции (строка 37). Проверка функции:
    Code:
    Python>ok, info = scope_challenge_function(0x401000)
    Python>if ok: print("start=%08X end=%08X" % (info[0], info[1]))
    start=00401010 end=00401078
    Эмулирование инструкций

    На предыдущем шаге мы смогли получить границы промежутка инструкций для эмуляции. Теперь напишем простую функцию эмуляции, поддерживающую ограниченное число инструкций (not, dec, inc, xor, sub и add):
    Code: Python

    def emulate_challenge_function(info, c1, c2, dbg = False):
    emu_start, emu_end = info
    if dbg:
    print("Emulating from %x to %x (%d, %d)" % (emu_start, emu_end, c1, c2))

    # Reset registers
    regs = {
    REG_EAX: c1,
    REG_EDX: c2
    }

    def get_opr_val(inst, regs):
    if inst.Op2.type == o_imm:
    return (True, inst.Op2.value)
    elif inst.Op2.type == idaapi.o_reg:
    return (True, regs[inst.Op2.reg])
    else:
    return (False, 0)

    ea = emu_start
    while ea < emu_end:
    out = ">%x: " % ea
    ok = True
    inst = idautils.DecodeInstruction(ea)
    ea += inst.size
    if inst.itype == idaapi.NN_not:
    out += "NOT"
    regs[inst.Op1.reg] = ~regs.get(inst.Op1.reg, 0) & 0xffffffff
    elif inst.itype == idaapi.NN_dec and inst.Op1.type == idaapi.o_reg:
    out += "DEC"
    regs[inst.Op1.reg] = (regs.get(inst.Op1.reg, 0) - 1) & 0xffffffff
    elif inst.itype == idaapi.NN_inc and inst.Op1.type == idaapi.o_reg:
    out += "INC"
    regs[inst.Op1.reg] = (regs.get(inst.Op1.reg, 0) + 1) & 0xffffffff
    elif inst.itype == idaapi.NN_xor:
    ok, val = get_opr_val(inst, regs)
    regs[inst.Op1.reg] = (regs.get(inst.Op1.reg, 0) ^ val) & 0xffffffff
    out += "XOR %08X" % val
    elif inst.itype == idaapi.NN_sub:
    ok, val = get_opr_val(inst, regs)
    regs[inst.Op1.reg] = (regs.get(inst.Op1.reg, 0) - val) & 0xffffffff
    out += "SUB %08X" % val
    elif inst.itype == idaapi.NN_add:
    ok, val = get_opr_val(inst, regs)
    regs[inst.Op1.reg] = (regs.get(inst.Op1.reg, 0) + val) & 0xffffffff
    out += "ADD %08X" % val
    else:
    ok = False

    # Dump registers
    for k, v in regs.items():
    out += (" [%s: %08X] " % (REG_NAMES.get(k, "%x" % k), v))

    if not ok:
    return (False, "Emulation failed at %08X" % ea)

    if dbg:
    print(out)

    return (True, (regs[REG_EDX] << 32) | regs[REG_EAX])

    В начале функции словарь regs заполняется начальными значениями регистров. В роли ключа словаря выступает op.reg. Любой неинициализированный регистр будет содержать ноль. Функция эмуляции далее входит в цикл и декодирует каждую инструкцию. Для каждой инструкции она проверяет в тип (для выбора необходимой операции) и ее операнды (для того, чтобы получить нужные значения). В конце цикла возвращается 64-битное значений.

    Мы можем проверить корректность эмулятора сравнивая его значения со значениями, полученными ранее:
    Code: Python

    for i in range(0, challenge_funcs_tbl_size):
    func = idc.Dword(challenge_funcs_tbl + i * 4)

    ok, info = scope_challenge_function(func)
    if ok:
    ok, val = emulate_challenge_function(info, 123, 456, dbg)
    if (val != RESULTS[i]):
    print("Mistmatch #%d: %16X vs %16X" % (i, val, RESULTS[i]))
    break

    else:
    print("Failed to scope challenge function #%d" % i)

    © Translated by Kitsu special for r0 Crew
    Last edited by kitsu; 27-10-2018 at 11:02.

  2. Пользователь сказал cпасибо:
    Darwin (28-10-2018)
+ Reply to Thread

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
All times are GMT. The time now is 01:17
vBulletin® Copyright ©2000 - 2018
www.reverse4you.org