Lua程序逆向之为Luac编写IDA Pro处理器模块

栏目: Lua · 发布时间: 7年前

内容简介:上一篇讲解了如何加载一个Luac文件到IDA Pro当中,加载进入idb数据库的内容犹如切好洗净的食材,并不能粗暴的直接展示给用户,还需要IDA Pro中的处理器模块对内容进行下一步的反汇编渲染与指令功能注释,才能最终装盘食用。处理器模块的工作就是:解析不同段的内容,确定代码段后,通过指定的指令格式解析与构造指令;确定指令使用的数据类型、寄存器与助记符;执行代码段的线性式代码反汇编;为指令标记注释与交叉引用等。

Lua程序逆向之为Luac编写IDA Pro处理器模块

上一篇讲解了如何加载一个Luac文件到IDA Pro当中,加载进入idb数据库的内容犹如切好洗净的食材,并不能粗暴的直接展示给用户,还需要IDA Pro中的处理器模块对内容进行下一步的反汇编渲染与指令功能注释,才能最终装盘食用。

处理器模块的工作就是:解析不同段的内容,确定代码段后,通过指定的指令格式解析与构造指令;确定指令使用的数据类型、寄存器与助记符;执行代码段的线性式代码反汇编;为指令标记注释与交叉引用等。

处理器模块架构

IDA Pro没有详细的文档描述如何开发处理器模块,最有效的学习途径是阅读IDA Pro程序中自带的开源的处理器模块代码。IDA Pro的处理器模块比文件加载器在架构上要更加晦涩难懂,实现起来也要复杂得多。

本篇写作时,对应的IDA Pro版本为国内众所周知的IDA Pro版本7.0,实验环境为macOS 10.12平台,处理器模块的开发选择使用Python。在IDA Pro软件的加载器目录(macOS平台):/Applications/IDAPro7.0/ida.app/Contents/MacOS/procs中,有着3个 Python 编写的处理器模块代码,分别是spu.py、ebc.py、msp430.py,如果安装了IDA Pro的开发SDK,在其中的module/script目录下也会找到这些模块,另外,还会包含一个proctemplate.py模板。

理论上,本节编写的Luac处理器模块,放到Windows等其他平台上,不需要进行任何的修改,也可以很好的工作。 本次参考使用到的代码是ebc.py模块,因为它的实现代码量不算最少,但在指令的解码处理上,代码更加直观。

处理器模块要求py中有一个定义为PROCESSOR_ENTRY()的方法,它的返回值是一个processor_t类型的类结构,IDA Pro通过检查这个类的字段,与回调它的方法,来完成指令的处理。一个精简的代码架构如下:

class ebc_processor_t(processor_t):
    ...
    # ----------------------------------------------------------------------
    def __init__(self):
        processor_t.__init__(self)
        self.PTRSZ = 4 # Assume PTRSZ = 4 by default
        self.init_instructions()
        self.init_registers()
        ...

...

def PROCESSOR_ENTRY():
    return ebc_processor_t()

ebc_processor_t类中有很多的回调函数,它们都会在特定的场景下触发执行,所有的回调方法,可以在当前版本的IDA Pro的ida_idp.py文件中,查看processor_t的类型声明得知,不过可以发现,processor_t的声明是由swig自动生成的桥接到C的代码,看不出任何有价值的地方,在实际编写代码时,可能需要查看Python编写的处理器模块的回调函数注释,来理解回调的参数与使用场景,也可以直接查看processor_t类型在 C语言 中的声明,它的定义可以在SDK的include目录下的idp.hpp头文件中找到,在实现上,SDK中也包含了很多C语言编写的处理器模块,代码也很有参考价值。

这里的ebc_processor_t的__init__()方法中,会调用init_instructions()初始化处理器模块用到的指令,以及调用init_registers()初始化处理器模块用到的寄存器信息,这是一种通用的设置流程,我们在下面的代码中也采用这种方式完成Luac的相关初始化。

Luac处理器模块的实现

下面来动手实现Luac的处理器模块,同样的,它只支持基于 Lua 5.2生成的Luac文件。 将ebc.py模块复制一份改名为loacproc.py。并修改ebc_processor_t为lua_processor_t,它的`_init()`代码不需要进行修改,代码如下:

def __init__(self):
    processor_t.__init__(self)
    self.PTRSZ = 4 # Assume PTRSZ = 4 by default
    self.init_instructions()
    self.init_registers()

self.PTRSZ描述了使用到的指针类型所占的字节大小,对于32位的Luac来说,它的值为4,通常只有在编写64位的程序处理器模块时,它的值才是8。init_instructions()与init_registers()分别用来初始化指令与寄存器列表,我们需要修改它的方法的实现部分。

在开始讲解指令与寄存器的修改前,我们先看看processor_t中需要修改的一些字段,它们的片断如下:

PLFM_LUAC = 99

class lua_processor_t(processor_t):
    # IDP id ( Numbers above 0x8000 are reserved for the third-party modules)
    id = PLFM_LUAC

    # Processor features
    flag = PR_DEFSEG32 | PR_USE64 | PRN_HEX | PR_RNAMESOK | PR_NO_SEGMOVE | PR_TYPEINFO

    # Number of bits in a byte for code segments (usually 8)
    # IDA supports values up to 32 bits
    cnbits = 8

    # Number of bits in a byte for non-code segments (usually 8)
    # IDA supports values up to 32 bits
    dnbits = 8

    # short processor names
    # Each name should be shorter than 9 characters
    psnames = ['Luac']

    # long processor names
    # No restriction on name lengthes.
    plnames = ['Lua Byte code']

    # size of a segment register in bytes
    segreg_size = 0

    # Array of typical code start sequences (optional)
    # codestart = ['\x60\x00']  # 60 00 xx xx: MOVqw         SP, SP-delta

    # Array of 'return' instruction opcodes (optional)
    # retcodes = ['\x04\x00']   # 04 00: RET

    # You should define 2 virtual segment registers for CS and DS.
    # Let's call them rVcs and rVds.

    # icode of the first instruction
    instruc_start = 0

    #
    #      Size of long double (tbyte) for this processor
    #      (meaningful only if ash.a_tbyte != NULL)
    #
    tbyte_size = 0

    segstarts = {}
    segends = {}

    ...

id字段是一个数值的ID值,用来标识处理器模块,IDA Pro中定义了一些已经存在的id,它们的定义在ida_idp.py中可以找到,如下所示:

# processor_t.id
PLFM_386        = 0       # Intel 80x86
PLFM_Z80        = 1       # 8085, Z80
PLFM_I860       = 2       # Intel 860
...
PLFM_EBC        = 57      # EFI Bytecode
PLFM_MSP430     = 58      # Texas Instruments MSP430
PLFM_SPU        = 59      # Cell Broadband Engine Synergistic Processor Unit

我们这里将其设置为PLFM_LUAC,只要定义它为一个与系统上不冲突的数值即可。

flag字段描述了处理器用到的一些特性,用样可以在ida_idp.py中可以查看processor_t.flag小节中的可选值,例如PR_USE64表示支持64位的寻址方式,PR_NO_SEGMOVE表示不支持段移动,即不允许调用move_segm()接口,PR_TYPEINFO表示支持类型信息,即支持在IDA Pro中载入til中的类型。

cnbits字段与dnbits字段表示对于代码段与非代码段,一个字节占用多少位,通常取值8。

psnames字段用来设置处理器模块的短名称,这里设置为”Luac”。还记得上一节如下的代码么:

idaapi.set_processor_type("Luac", SETPROC_ALL|SETPROC_FATAL)

当注册了该名称后,文件加载器就可以通过idaapi.set_processor_type()来设置该处理器模块了。

plnames字段是长名称,起到描述性的作用。

segreg_size字段描述段寄存器的大小,当前面的flag字段包含了PR_SEGS标志,则需要设置它的值,这里取值为0。

codestart与retcodes用于描述函数的开始与结束的指令特征,用于IDA Pro线性扫描时,自动生成函数信息。

instruc_start为指令列表的起始索引。

tbyte_size字段描述long double类型的字节大小,这里没有用到,设置为0即可。

segstarts与segends用来记录段的开始与结束地址,这两个字段在其他的代码回调处很有用。

接下来还需要设置一个assembler字段,描述了反汇编的一些信息。包括设置反汇编器的名称,各种数据类型的助记符,比如字节、字、双字通常设置为db、dw、dd,这在其他的处理器模块中常见。然后是各种保留关键字与逻辑操作的助记符,这些内容在luac_proc中,可以选择保留或者删除。

在init_instructions()的内部实现中,被要求设置一个class idef,该类型用于描述指令的具体信息,包括:指令的名称、解码回调程序、规犯标志、注释等。当然,也可以选择不实现它。在本例中,选择了使用idef辅助进行指令处理,它的定义如下:

class idef:
    """
    Internal class that describes an instruction by:
    - instruction name
    - instruction decoding routine
    - canonical flags used by IDA
    """
    def __init__(self, name, cf, d, cmt = None):
        self.name = name
        self.cf  = cf
        self.d   = d
        self.cmt = cmt

为了方便解码指令,这里定义了一张指令表self.itable,它列出了Luac中涉及到的所有指令,如下所示:

self.itable = {
    0x00: idef(name='MOVE', d=self.decode_MOVE, cf=CF_USE1 | CF_USE2, cmt=''),
    0x01: idef(name='LOADK', d=self.decode_LOADK, cf=CF_USE1 | CF_USE2, cmt=self.cmt_LOADK),
    0x02: idef(name='LOADKX', d=self.decode_LOADKX, cf=CF_USE1 | CF_USE2, cmt=''),
    0x03: idef(name='LOADBOOL', d=self.decode_LOADBOOL, cf=CF_USE1 | CF_USE2 | CF_USE3, cmt=''),
    0x04: idef(name='LOADNIL', d=self.decode_LOADNIL, cf=CF_USE1 | CF_USE2, cmt=''),
    0x05: idef(name='GETUPVAL', d=self.decode_GETUPVAL, cf=CF_USE1 | CF_USE2, cmt=''),
    ...
    0x26: idef(name='VARARG', d=self.decode_VARARG, cf=CF_USE1 | CF_USE2, cmt=''),
    0x27: idef(name='EXTRAARG', d=self.decode_EXTRAARG, cf=CF_USE1, cmt=''),
}

CF_USE1与CF_USE2标志表示使用了第一个操作数与第二个操作数,与之类似的还有CF_JUMP,表示这是一条跳转类型的指令,CF_CALL表示这是一条call类型的指令,所有支持的标志可以在ida_idp.py的instruc_t.feature小节查看。

完成这张表后,需要使用它来填充处理器模块的instruc字段,代码如下:

# Now create an instruction table compatible with IDA processor module requirements
Instructions = []
i = 0
for x in self.itable.values():
    d = dict(name=x.name, feature=x.cf)
    if x.cmt != None:
        d['cmt'] = x.cmt
    Instructions.append(d)
    setattr(self, 'itype_' + x.name, i)
    i += 1

# icode of the last instruction + 1
self.instruc_end = len(Instructions) + 1

# Array of instructions
self.instruc = Instructions

# Icode of return instruction. It is ok to give any of possible return
# instructions
self.icode_return = self.itype_RETURN

instruc_end字段为指令列表的结束索引,它对应着前面的instruc_start字段。

instruc字段通过Instructions进行设置,它只取了指令的名称与标志两个字段。

icode_return字段指明可能的返回指令,itype_RETURN是前面通过setattr()设置的RETURN指令。

下面看看指令的解码部分,即前面self.itable中定义的如self.decode_MOVE与self.decode_LOADK部分。

self.decode_MOVE的实现如下:

def decode_MOVE(self, insn, a, b, c, ax, bx, sbx):
    """
    OP_MOVE,/*    A B    R(A) := R(B)                    */
    """
    insn.Op1.type = o_reg
    insn.Op1.reg = a
    insn.Op1.dtype = dt_dword

    insn.Op2.type = o_reg
    insn.Op2.reg = b
    insn.Op2.dtype = dt_dword

    return True

可以看到,实现方法上,主要是填充inst指令的两个操作数,因为它的最终展示形式形如:

MOVE      R(A), R(B)

在填充时,除了指定它是否为寄存器类型o_reg外,还需要设置它的具体值a与b,当a为2,b为1时,它生成的反汇编代码为:

MOVE       R2, R1

然后以LOADK指令为例,它的解码回调为self.decode_LOADK,代码如下:

def decode_LOADK(self, insn, a, b, c, ax, bx, sbx):
    """
    OP_LOADK,/*    A Bx    R(A) := Kst(Bx)                    */
    """
    insn.Op1.type = o_reg
    insn.Op1.reg = a
    insn.Op1.dtype = dt_dword

    insn.Op2.type = o_displ
    insn.Op2.reg = bx
    insn.Op2.dtype = dt_dword

    return True

这一次,Op2的类型不为o_reg,而是o_displ,这是指针类型的数据,这里在最终解码时,我们会判断它操作的是否为Upvalue,来进一步确定它是UpValue,还是Constant常量,如果是前者,我们输出时会指定U开头,如果是后者,输出时会指定K开头。至于具体的判断方法,则是将指令的Op的specval值设置为1。如decode_SETUPVAL()的实现:

def decode_SETUPVAL(self, insn, a, b, c, ax, bx, sbx):
    """
    OP_SETUPVAL,/*    A B    UpValue[B] := R(A)                */
    """
    insn.Op1.type = o_reg
    insn.Op1.reg = a
    insn.Op1.dtype = dt_dword

    insn.Op2.type = o_displ
    insn.Op2.reg = b
    insn.Op2.dtype = dt_dword
    insn.Op2.specval = 1

    return True

接下来就是一条条的实现每一条指令的解码回调,这就是一个体力活。

接着是初始化寄存器的部分,代码如下:

def init_registers(self):
    """This function parses the register table and creates corresponding ireg_XXX constants"""
    # Registers definition
    self.reg_names = [
        # General purpose registers
        # #define MAXSTACK    250
        # >>> for i in xrange(250):
        # ...     print("\"R%d\"," % i)
        "R0",
        "R1",
        "R2",
        "R3",
        "R4",
        "R5",
        "R6",
        "R7",
        "R8",
        ...
        # Fake segment registers
        "CS",
        "DS"
    ]

    # Constants definition
    self.constant_names = [
        # #define MAXSTACK    250
        # >>> for i in xrange(250):
        # ...     print("\"K%d\"," % i)
        "K0",
        "K1",
        "K2",
        "K3",
        "K4",
        "K5",
        "K6",
        "K7",
        "K8",
        ...
    ]

    # Upvalues definition
    self.upvalue_names = [
        # #define MAXSTACK    250
        # >>> for i in xrange(250):
        # ...     print("\"U%d\"," % i)
        "U0",
        "U1",
        "U2",
        "U3",
        "U4",
        "U5",
        "U6",
        "U7",
        "U8",
        ...
    ]

    # Create the ireg_XXXX constants
    for i in xrange(len(self.reg_names)):
        setattr(self, 'ireg_' + self.reg_names[i], i)

    # Create the iconst_XXXX constants
    for i in xrange(len(self.constant_names)):
        setattr(self, 'iconst_' + self.constant_names[i], i)

    # Create the iupval_XXXX constants
    for i in xrange(len(self.upvalue_names)):
        setattr(self, 'iupval_' + self.upvalue_names[i], i)

    # Segment register information (use virtual CS and DS registers if your
    # processor doesn't have segment registers):
    self.reg_first_sreg = self.ireg_CS
    self.reg_last_sreg  = self.ireg_DS

    # number of CS register
    self.reg_code_sreg = self.ireg_CS

    # number of DS register
    self.reg_data_sreg = self.ireg_DS

主要是定义了寄存器名称表reg_names,常量名称表constant_names,UpValue名称表upvalue_names。以及为这些表各自设置名称属性。

完成了这两卡的初始化,接着就是实现处理器模块的回调了。重要的有,notify_ana(),作用是解码每一条指令,它的实现代码如下:

def notify_ana(self, insn):
    """
    Decodes an instruction into insn
    """
    # take opcode byte
    b = insn.get_next_dword()

    # the 6bit opcode
    opcode = b & 0x3F
    arg_a = GET_BITS(b, 6, 13)
    arg_b = GET_BITS(b, 23, 31)
    arg_c = GET_BITS(b, 14, 22)
    arg_ax = GET_BITS(b, 6, 31)
    arg_bx = GET_BITS(b, 14, 31)
    arg_sbx = GET_BITS(b, 14, 31) - 131071

    print("opcode:%x, a:%x, b:%x, c:%x, ax:%x, bx:%x, sbx:%d" % (opcode, arg_a, arg_b, arg_c, arg_ax, arg_bx, arg_sbx))

    # opcode supported?
    try:
        ins = self.itable[opcode]
        # set default itype
        insn.itype = getattr(self, 'itype_' + ins.name)
    except:
        return 4

    # call the decoder
    return insn.size if ins.d(insn, arg_a, arg_b, arg_c, arg_ax, arg_bx, arg_sbx) else 0

解析32位的指令,取它的opcode、arg_a、arg_b、arg_c、arg_ax、arg_bx、arg_cx等值,然后根据不同的opcode索引查表,设置指令的itype字段,最后返回指令的长度即可。

notify_out_insn()用于输出完整指令,out_mnem()用于输出助记符,notify_out_operand()用于输出操作数,后两个回调是前一个回调的两个拆分,这几个回调加在一起,可以处理指令输出的全部细节。notify_out_insn()与out_mnem()的实现对于多数的反汇编引擎部分是一样的,这里不去细究,主要看看notify_out_operand(),它的实现如下:

def notify_out_operand(self, ctx, op):
    """
    Generate text representation of an instructon operand.
    This function shouldn't change the database, flags or anything else.
    All these actions should be performed only by u_emu() function.
    The output text is placed in the output buffer initialized with init_output_buffer()
    This function uses out_...() functions from ua.hpp to generate the operand text
    Returns: 1-ok, 0-operand is hidden.
    """
    #print("notify_out_operand called. op:%x" % op.type)
    optype = op.type
    fl     = op.specval
    def_arg = is_defarg(get_flags(ctx.insn.ea), op.n)

    if optype == o_reg:
        ctx.out_register(self.reg_names[op.reg])

    elif optype == o_imm:
        # for immediate loads, use the transfer width (type of first operand)
        if op.n == 1:
            width = self.dt_to_width(ctx.insn.Op1.dtype)
        else:
            width = OOFW_32 if self.PTRSZ == 4 else OOFW_64
        ctx.out_value(op, OOFW_IMM | width)

    elif optype in [o_near, o_mem]:
        r = ctx.out_name_expr(op, op.addr, idc.BADADDR)
        if not r:
            ctx.out_tagon(COLOR_ERROR)
            ctx.out_btoa(op.addr, 16)
            ctx.out_tagoff(COLOR_ERROR)
            remember_problem(PR_NONAME, ctx.insn.ea)

    elif optype == o_displ:
        is_upval = fl
        if is_upval:
            ctx.out_register(self.upvalue_names[op.reg])    #Upvalues
        else:
            ctx.out_register(self.constant_names[op.reg])   #Constants

        #if op.addr != 0 or def_arg:
        #    ctx.out_value(op, OOF_ADDR | (OOFW_32 if self.PTRSZ == 4 else OOFW_64) | signed | OOFS_NEEDSIGN)

    else:
        return False

    return True

根据不同的操作数类型,调用不同的方法进行输出。ctx.out_register负责输出寄存器;ctx.out_value负责输出立即数;而对于UpValue与Constant的输出,这里借用了ctx.out_register来输出,只是使用了不同的名称组。

取这里,指令的基本的反汇编就算完成了,IDA Pro在应用该处理器模块时,会线性的扫描所有的CODE类型的代码段,进行反汇编处理。由于篇幅的原因,这篇就到此这止了,有兴趣的读者,可以在此基础上,实现函数的创建、代码与数据的交叉引用、自动添加注释等功能。最终实现的效果如图所示:

Lua程序逆向之为Luac编写IDA Pro处理器模块

完整的luac_proc.py文件可以在这里找到: https://github.com/feicong/lua_re


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Pro Git (Second Edition)

Pro Git (Second Edition)

Scott Chacon、Ben Straub / Apress / 2014-11-9 / USD 59.99

Scott Chacon is a cofounder and the CIO of GitHub and is also the maintainer of the Git homepage ( git-scm.com ) . Scott has presented at dozens of conferences around the world on Git, GitHub and the ......一起来看看 《Pro Git (Second Edition)》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试