内容简介:上一篇讲解了如何加载一个Luac文件到IDA Pro当中,加载进入idb数据库的内容犹如切好洗净的食材,并不能粗暴的直接展示给用户,还需要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类型的代码段,进行反汇编处理。由于篇幅的原因,这篇就到此这止了,有兴趣的读者,可以在此基础上,实现函数的创建、代码与数据的交叉引用、自动添加注释等功能。最终实现的效果如图所示:
完整的luac_proc.py文件可以在这里找到: https://github.com/feicong/lua_re 。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- “统治”移动处理器市场的Arm为何明年Q1才发布AI处理器?
- AMD 7nm EPYC处理器亮相:性能提升一倍,世界最强X86处理器
- 36.Django内容处理器
- Bean的后置处理器
- 处理器是如何调度进程的?
- JVM进阶 -- 浅谈注解处理器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Python深度学习
[美] 弗朗索瓦•肖莱 / 张亮 / 人民邮电出版社 / 2018-8 / 119.00元
本书由Keras之父、现任Google人工智能研究员的弗朗索瓦•肖莱(François Chollet)执笔,详尽介绍了用Python和Keras进行深度学习的探索实践,涉及计算机视觉、自然语言处理、生成式模型等应用。书中包含30多个代码示例,步骤讲解详细透彻。由于本书立足于人工智能的可达性和大众化,读者无须具备机器学习相关背景知识即可展开阅读。在学习完本书后,读者将具备搭建自己的深度学习环境、建......一起来看看 《Python深度学习》 这本书的介绍吧!