R0 CREW

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

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

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

Обзор

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

#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.

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 и первую функцию в ней.

;
; 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(): эта функция эмулирует инструкции в рамках данных границ
  • Перед тем как мы начнем, определим некоторые глобальные переменные для скрипта:
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). Далее мы сможем использовать эту таблицы для проверки нашего кода.

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

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

Вывод будет следующим:

>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():

# .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. Этот файл содержит перечисления для всех поддерживаемых модулей процессора вместе с соответствующей поддерживаемой инструкцией:

// 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 процессоров.

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

# .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:

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, но по крайней мере мы знаем достаточно для определения этих значений самим:

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

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

# .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(), но она не соответствует цели этой статьи. Исключительно для демонстрирования кода выше, следующий способ позвояет искать шаблоны кода лишь с помощью декодирования инструкций:

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). Проверка функции:

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):

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-битное значений.

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

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