上一篇讲解了如何加载一个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。