首页
论坛
专栏
课程

[翻译]使用 IDAPython 写一个简单的x86模拟器

zplusplus
1
2018-3-11 10:20 2288

使用 IDAPython 写一个简单的x86模拟器

我经常遇到一些IDAPython脚本,注意到这些脚本通常使用低效或者错误的IDAPython的API来反汇编或者解码指令(比如,使用idc.GetMenem()或者idc.GetDisasm())。因此,在本篇文章中,我将阐述如何使用IDAPython的指令解码函数来编写一个非常简单的x86模拟器。本文的目的是通过编写这个模拟器来演示IDAPython API的正确使用方式。读完本文后,你应该就能使用IDAPpython解决一些相似的问题了。

目录

  • 概览
  • 反汇编程序
  • 编写模拟器
  • 指令解码快速入门
  • 确定挑战函数的范围
  • 模拟执行指令

概览

让我们从下边的列表展示的示例代码开始,其中包含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 engine)来模拟挑战函数

先让我们来编译一下这个程序,然后使用参数c1=123c2=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)后边跟着两条寄存器赋初值的指令(0x40100A0x40100D),然后有一系列针对这两个寄存器的操作(从0x4010100x401076),最后在popa指令恢复所有的寄存器钱,结果被赋值到了局部变量中(0x4010780x40107B)。

 

我们将根据这个代码模式来编写一个小小的函数使其能够识别计算过程的代码序列,然后,编写另一个函数来根据给定的范围来进行计算最后返回值。

编写模拟器

本节将实现两个函数:

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

我们从之前的反汇编中可以推导出挑战函数表以及它的大小,所以此处定义了一个RESULT变量来存储使用参数c1=123c2=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 :表示指令类型的整数型属性。不同的opcode可能具有相同的itype,但是opcode不是itype
  • inst.size:表示解码之后指令的长度。
  • inst.Operands[]:以0为索引起始的数组,用来保存操作数相关信息。
  • inst.Op1..inst.OpN:以1为索引起始的操作数数组别名。
  • inst.ea:指令的线性地址

你可能会想知道opcode和它的itype之间到底是什么关系。其实很简单,在IDA中,开源数据库处理器模块负责根据opcode来填充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_t的成员:

  • op.flags:操作数的标志
  • op.dtype:操作数的类型。dt_xxx常量,可以通过该常量来获取操作数的字节大小(1 == dt_byte,2 == dt_word等等)。
  • op.type:操作数类型。o_xxx常量。
  • specflags1...specflags4:处理器相关标志。

以下是受支持的操作数类型(o_xxx):

  • o_void:没有该操作数。
  • o_reg:该操作数是寄存器(ax,al,es,ds等等)
  • o_mem:直接寻址(数据)
  • o_phrase:[基址+变址]寻址
  • o_displ:[基址+变址+偏移]寻址
  • o_imm:立即数
  • o_far:直接远地址(far address,代码)
  • o_near:直接近地址(near address,代码)
  • o_dispspec0...o_dispspec5:处理器相关标志。

还有一些操作数成员的含义因操作数的类型而异:

  • op_reg:寄存器编号(o_reg
  • op_phrase:内存访问中的索引寄存器(o_phrase
  • op_value:立即数(o_imm)或偏移(o_displ
  • op_addr: 操作数使用的内存地址(o_memo_faro_displo_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.hppallins.hppua.hppidp.hpp

确定挑战函数的范围

之前我们想出了如何通过挑战函数表来去顶挑战函数地址的方法。现在让我们来写一个使用指令解码来确定需要被模拟的指令的范围的函数吧。

 

请注意:我使用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))

解码指令的基本模式是在每次成功的解码之后通过inst.size(在此例中是ea变量)预先解码地址。之后,应该检查指令的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

模拟执行指令

在上一步中,我们设法获得了模拟执行的开始和结束地址范围,现在,来写一个极简的模拟函数吧(仅支持有限的指令 NOTDECINCXORSUBADD):

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作为key,其他的未赋初始化值的寄存器将会被置为0。模拟函数将会进入一个循环中解码每一条指令,它将检查每一条指令的类型(来确定什么操作需要被模拟)以及该指令的操作数(来确定如何得到需要的值)。循环结束之后,将返回一个64-bit的值。

 

我们可以将模拟函数返回的值和之前刚开始运行时得到的值进行比较来验证模拟的是否正确。

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)

我希望这篇文章对你有所帮助,如果想提出问题或者文中出现错误,请不要犹豫,联系我。可以从这里下载到本文中所使用的文件:文件下载:密码123

 

原文地址:http://0xeb.net/2018/02/writing-a-simple-x86-emulator-with-idapython/

 

本文由看雪论坛zplusplus翻译



[防守篇]2018看雪.TSRC CTF 挑战赛(团队赛)11月1日征题开启!

最后于 2018-3-11 10:21 被zplusplus编辑 ,原因: 上传附件
上传的附件:
最新回复 (6)
安全圈套 2018-3-11 12:17
2

0

沙发
无名侠 8 2018-3-14 19:19
3

0

原文博客不错啊。。
yzlong 2018-3-14 20:29
4

0

BinCat
聖blue 2018-3-14 23:29
5

0

BJChangan 2018-4-20 11:06
6

0

这个真的强,最近也在想用idapython写一个简单的变量传播静态分析脚本。。。。。。摔
qiantang 2018-5-18 17:01
7

0

真是及时雨,我正在使用idapython写一个反混淆工具,但是发现idapython很多的接口跟网上的资料说的不一样,有些甚至都找不到了,正在发愁呢,真是谢谢楼主分享
返回