menu Alkaid #二进制初学者 / 网络安全 / 大龄CTF退役选手
Writing a simple x86 emulator with IDAPython 译文(机翻原文加补充)
3320 浏览 | 2021-03-16 | 分类:心路历程,汇编语言 | 标签:

原文链接:https://0xeb.net/2018/02/writing-a-simple-x86-emulator-with-idapython/

通常,当我偶然发现IDAPython脚本时,会发现它们使用效率低下/错误的IDAPython API来反汇编或解码指令(例如,使用idc.GetMnem()或idc.GetDisasm())。 因此,在这篇博客文章中,我将说明如何使用IDAPython的IDA指令解码功能来编写一个非常简单的x86模拟器。 目的是演示指令解码IDAPython API的正确使用。 到本文结尾,您应该能够使用IDAPython解决类似的问题。

Overview

让我们从下面的示例程序开始,其中包含一个包含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并直接查询质询函数。
  • 使用反编译器以C伪代码提取算法并重新编译,然后使用它。
  • 提取挑战功能汇编列表,重新组装并使用它。
  • 使用仿真框架(例如Unicorn引擎)来仿真质询功能。

让我们编译该程序并以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

Disassembling the program

让我们反汇编测试程序,并找到Challenge_funcs表和第一个Challenge函数:

;
; 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之间),最后我们看到了正在复制的结果 在使用popa还原所有寄存器之前,请先将其返回到局部变量(在0x401078和0x40107B处)。

我们将使用此代码模式编写一个小的函数,该函数标识执行计算的指令的边界。 然后,我们将编写另一个函数,以模拟给定范围内的代码并返回结果。

Writing the emulator

在本节中,我们将实现两个功能:

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)

我们从上面的反汇编中推导出了挑战函数表及其大小。 我们还定义了RESULTS变量,该变量包含使用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

Quick intro to instruction decoding

要使用IDAPython解码指令,请使用idautils.DecodeInstruction()函数:

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

如果解码失败,则此函数返回None。 如果解码成功,我们将获得一个指令对象,其中包含有关指令及其操作数的信息。

这些是重要的指令属性:

  • inst.itype:这是代表指令类型的整数。 不同的操作码具有相同的itype,因此操作码!= itype。
  • inst.size:这是已解码指令的大小。
  • inst.Operands []:这是一个从零开始的数组,其中包含操作数信息。(在ida7.5中改为inst.ops[])
  • inst.Op1 .. OpN:这些是Operands数组中基于1的别名。
  • 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_xxxx常量之一进行比较,可以直观地检查解码后的指令是什么。

至于操作数,可以通过inst.Operands []或inst.OpN访问它们。 要获取已解码指令使用的操作数数量,您不应该依赖Operands数组的长度,因为它始终会解析为UA_MAXOP == 8(请参见ida.hpp)。 取而代之的是,遍历每个操作数,并查看其类型是否为o_void。

使用ua.hpp头文件中定义的op_t结构类型定义指令操作数。

一些操作数成员是:

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:内存Reg [基本Reg +索引Reg +位移]。

o_imm:立即值。

o_far:立即远地址(CODE)。

o_near:紧邻地址(CODE)。

o_idpspec0 ..o_idpspec5:处理器特定的标志。

还有其他操作数成员,其含义因操作数的类型而异:

op.reg:寄存器号(o_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了解更多信息。

Scoping the challenge functions

之前,我们了解了如何遍历挑战功能表并检索每个挑战功能的地址。 现在,让我们编写一个使用指令解码器的函数来查找要模拟的指令的边界。

请注意,我可以使用IDAPython的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,然后相应地检查操作数。

请注意,在第二阶段,我开始向后解码。 要在适当的反汇编清单中向后移动,可以使用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

Emulating instructions

在上一步中,我们设法检索了仿真边界的开始和结束地址。 现在,让我们编写一个简单的仿真功能,该功能仅支持一组有限的指令(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)

希望本文对您有所帮助。 请不要犹豫,在本文中提出问题和/或指出错误。 您可以从此处下载本文中使用的文件:

http://0xeb.net/wp-content/uploads/2018/02/ida-emulate-example.zip

password:123

You might also like:

温柔正确的人总是难以生存,因为这世界既不温柔,也不正确

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!