六:操作数
可以使用idc.get_openrand_typed(ea,n)得到操作数的类型。ea是地址,n是索引
这里有8种不同类型的操作数类
0_void 如果一个指令木有任何操作数它将返回0
0_reg 如果一个操作数是一个普通的寄存器将返回此类型。这个值在内部表示为1.
o_mem 如果一个操作数是直接内存引用,它将返回这个类型,这个值在内部表示为2.这种类型是有用的在date段查找引用。
o_phrase 这个操作数被返回则这个操作数包含一个基本的寄存器或者一个索引寄存器,这个值在内部表示为3.
o_displ这个操作数被返回则 操作数包含寄存器和一个位移值 这个位移值是一个整数,例如ox18。这是常见的当一条指令访问在一个结构体中。在内部,它表示为4的值。
o_for 这个操作数不是很常见当逆向x86或者x86_64 时 它是用来寻找操作数的访问立即数远地址的,它在内部表示为6。
o_near 不是很常见,寻找近地址 内部表示为7。
举个粒子:
当逆向可执行文件的内存dump时,操作数不能被识别为偏移
seg000:00BC1388 push 0Ch
seg000:00BC138A push 0BC10B8h
seg000:00BC138F push [esp+10h+arg_0]
seg000:00BC1393 call ds:_strnicmp
push 0bc10b8h是一个内存偏移量。如果我们将其改为数据类型,我们会看到字符串偏移量。如果要自动化,如下:
idc.get_inf_attr(INF_MIN_EA) 获得调用最小地址
idc.get_inf_attr(INF_MAX_EA) 获得调用最大地址
idc.op_plain_offset(ea, n, base) 将操作数转化为偏移量 第一个参数是地址,第二个参数n是操作数索引,第三个参数base是基地址
min = idc.get_inf_attr(INF_MIN_EA)
max = idc.get_inf_attr(INF_MAX_EA)
# for 每个已知函数
for func in idautils.Functions():
flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
# 跳过 library 和 thunk 函数
if flags & FUNC_LIB or flags & FUNC_THUNK:
continue
dism_addr = list(idautils.FuncItems(func))
for curr_addr in dism_addr:
if idc.get_operand_type(curr_addr, 0) == 5 and (min < idc.get_operand_value(curr_addr, 0) < max):
idc.OpOff(curr_addr, 0, 0)
if idc.get_operand_type(curr_addr, 1) == 5 and (min < idc.get_operand_value(curr_addr, 1) < max):
idc.op_plain_offset(curr_addr, 1, 0)
七:基本块
基本块是木有分支的直线代码序列,有一个单入口点和一个单出口点组成。
在分析程序的控制流时,基本块非常用。在使用函数的图分解视图时,通常可以观察到rda对基本块的表示。使用基本块进行分析的一些值得注意的粒子是 用于识别循环或者控制流混淆哦。当一个基本块将控制权转移到另一个块时,下一个块称为后继承块,前一个块称为predecessors。
.text:00401034 push esi
.text:00401035 push edi
.text:00401036 push 0Ah ; Size
.text:00401038 call ds:malloc
.text:0040103E mov esi, eax
.text:00401040 mov edi, offset str_encrypted
.text:00401045 xor eax, eax ; eax = 0
.text:00401047 sub edi, esi
.text:00401049 pop ecx
.text:0040104A
.text:0040104A loop: ; CODE XREF: _main+28↓j
.text:0040104A lea edx, [eax+esi]
.text:0040104D mov cl, [edi+edx]
.text:00401050 xor cl, ds:b_key ; cl = 0
.text:00401056 inc eax
.text:00401057 mov [edx], cl
.text:00401059 cmp eax, 9 ; index
.text:0040105C jb short loop
.text:0040105E push esi
.text:0040105F push offset str_format
.text:00401064 mov byte ptr [esi+9], 0
.text:00401068 call w_vfprintf
.text:0040106D push esi ; Memory
.text:0040106E call ds:free
.text:00401074 add esp, 0Ch
.text:00401077 xor eax, eax ; eax = 0
.text:00401079 pop edi
.text:0040107A pop esi
.text:0040107B retn
.text:0040107B _main endp
以上函数分为三个块,异或从0x40104a开始。0x401050是异或的关键点。
ea = 0x0401050
f = idaapi.get_func(ea)
fc = idaapi.FlowChart(f, flags=idaapi.FC_PREDS)
for block in fc: #这个函数的所有块
print("ID: %i Start: 0x%x End: 0x%x" % (block.id, block.start_ea,block.end_ea))
if block.start_ea <= ea < block.end_ea:
print(" Basic Block selected")
successor = block.succs() #包含后续地址的生成器
for addr in successor:
print(" Successor: 0x%x" % addr.start_ea)
pre = block.preds() #包含前身地址的生成器
for addr in pre:
print(" Predecessor: 0x%x" % addr.end_ea)
if ida_gdl.is_ret_block(block.type): #返回块
print(" Return Block")
>>> Successor: 0x40104a
>>> ID: 1 Start: 0x40104a End: 0x40105e
>>> Basic Block selected
>>> Successor: 0x40105e
>>> Successor: 0x40104a
>>> Predecessor: 0x40104a
>>> Predecessor: 0x40105e
>>> ID: 2 Start: 0x40105e End: 0x40107c
>>> Predecessor: 0x40105e
>>> Return Block
每个块包含以下属性:
id:函数中每块都有一个唯一索引,从0开始
type:有以下类型
-
- fcb_normal:表示普通块,内部为0
- fcb_indjump:是否以间接跳转结束,内部为1
- fcb_ret:返回块,内部为2
- fcb_cndret:是否为条件返回,内部为3
- fcb_noret:无返回块,内部为4
- fcb_enoret:不属于函数且没有返回的块,内部为5
- fcb_extern:外部普通块,内部为6
- fcb_error:通过函数结束来传递执行的块,内部为7
- start_ea:基本块开始地址
- end_ea:基本快结束地址
- preds:一个函数,它返回一个包含所有前身地址的生成器
- succs:一个函数,它返回包含所有后续地址的生成器。
八:结构
在编译过程中,结构布局,结构名称和结构类型将会被删除。
重构结构和正确标记结构成员可以极大帮助逆向过程。
下面是x86 shellcode中常见的代码片段。
完整代码遍历器结构包含 线程环境块(TEB) 和 进程环境快(PEB) 来找到kernell32.dll的基地址。
seg000:00000000 xor ecx, ecx
seg000:00000002 mov eax, fs:[ecx+30h]
seg000:00000006 mov eax, [eax+0Ch]
seg000:00000009 mov eax, [eax+14h]
shellcode的下一步是遍历PE文件,查找window api。
由于要解析所有不同结构,除非对结构偏移量进行标记,否则很容易丢失。
使用下列代码标注对应结构名称:
idc.add_default_til 加载 类型库(TIL), TIL是IDA自己的c/c++格式头文件。它包含结构、枚举、联合和其他数据类型的定义。在IDA中可以通过shift+F11打开。idc.add_default_til会返回加载状态。
在TIL加载后,使用import_type将TIL各个定义导入带IDB中。
idc.import_type(idx, type_name),第一个参数是type的索引,每一个类型都有一个所以和id。-1代表这类型应该将该类型添加到IDA导入类型的末尾。
idc.get_struc_id 获得结构id
idc.op_stroff(ea, n, strid, delta) 偏移地址处增加名字。
第一个参数是包含将要被标记的偏移量的指令的地址(ea)。第二个参数是操作数,在下面的例子中,因为我们想要更改mov eax, fs:[ecx+30h]中的0x30标签,我们需要为第二个参数传递一个值1。第三个参数是类型id,第四个参数通常是0.
status = idc.add_default_til("ntapi")
if status:
idc.import_type(-1, "_TEB")
idc.import_type(-1, "PEB")
idc.import_type(-1, "PEB_LDR_DATA")
ea = 2
teb_id = idc.get_struc_id("_TEB")
idc.op_stroff(ea, 1, teb_id, 0)
ea = idc.next_head(ea) #获得下一个指令地址
peb_ldr_id = idc.get_struc_id("PEB_LDR_DATA")
idc.op_stroff(ea, 1, peb_ldr_id, 0)
ea = idc.next_head(ea)
idc.op_stroff(ea, 1, peb_ldr_id, 0)
idc.del_struc(id) 删除结构
idc.add_struc(index, name, is_union) 增加结构 第一个参数是索引,-1添加到后面,第二个参数是结构名字,第三个参数是定义的新结构是否为union,0代表不是union。
后面的待更新:(没学会自己。。。)