指令粒度介绍完毕之后,该从基本块粒度进行说明了。所涉及到的API速查表格如下:
模块 |
API |
功能 |
idaapi |
get_func(ea) |
获取地址ea所在函数 |
idaapi |
FlowChart(f=None, bounds=None, flags=0) |
获取函数f或者地址范围bounds的所有基本块集合 |
基本块简介
先简单说一下基本块的概念,基本块(Basic Block)是一段可以被顺序执行的指令集,一般被视为程序的基本组成单元。一个程序的控制流就是由基本块组成的有向图。在一个基本块的执行过程中,它通常是由前驱基本块(predecessor)执行到当前基本块,然后当前基本块执行完毕后到后继基本块(successor)。
因此,大概也可以知道基本块的重要组成了:
- 起始地址:基本块的第一条指令
- 结束地址:基本块的最后一条指令
- 指令集:基本块中所有的指令,也是要执行的指令集
- 转移指令:用于跳转到下一个基本块
基本块具有一个特性,那就是如果传递进行的寄存器值、使用到的内存值是一样的,那么在基本块执行完毕的寄存器集的值、内存值也是一样的。这个特性可以用于对基本块级别的仿真模拟,例如判断最后的跳转是否会被采纳。基本块在模糊测试中也被用于记录执行路径,例如AFL就是在基本块的起始地址进行了插桩,如果测试值使得基本块执行到了新的路径,那么就说明这个测试值潜力更大。
基本块的概念说完了,在IDA反汇编的Graph disassemble view中(空格切换,233)可以看到函数的基本块。如下就是在IDA中显示的两个基本块:
这两个基本块的作用是通过strcmp
函数判断传入到cgibin的环境变量,选择使用哪一个接口函数来处理数据。
基本块1:调用phpcgi
1 2 3 4 5 6 7 8 9 10 11
| .text:00402E1C .text:00402E1C loc_402E1C: .text:00402E1C la $t9, strcmp .text:00402E20 move $a0, $s0 # s1 .text:00402E24 sw $a2, 0x20+var_8($sp) .text:00402E28 jalr $t9 ; strcmp .text:00402E2C li $a1, aPhpcgi # "phpcgi" .text:00402E30 lw $gp, 0x20+var_10($sp) .text:00402E34 lw $a2, 0x20+var_8($sp) .text:00402E38 bnez $v0, loc_402E4C .text:00402E3C lui $a1, 0x42 # 'B'
|
基本块2:调用dlapn.cgi
1 2 3 4 5 6 7 8 9 10 11
| .text:00402E4C .text:00402E4C loc_402E4C: .text:00402E4C la $t9, strcmp .text:00402E50 move $a0, $s0 # s1 .text:00402E54 sw $a2, 0x20+var_8($sp) .text:00402E58 jalr $t9 ; strcmp .text:00402E5C li $a1, aDlapnCgi # "dlapn.cgi" .text:00402E60 lw $gp, 0x20+var_10($sp) .text:00402E64 lw $a2, 0x20+var_8($sp) .text:00402E68 bnez $v0, loc_402E7C .text:00402E6C lui $a1, 0x42 # 'B'
|
基本块操作
在IDAPython中,对基本块的操作通常是先根据一个指令地址得到指令所在的函数,然后获取到函数由基本块组成的控制流图,再遍历图中的所有基本块。如果要知道目标指令是在函数的哪一个基本块,遍历基本块然后判断。
如下的代码是遍历地址ea
所在函数的所有基本块,然后判断ea
在哪一个基本块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import idaapi import ida_gdl
ea = 0x00402E4C func = idaapi.get_func(ea) func_control_flow = idaapi.FlowChart(func, flags=idaapi.FC_PREDS)
for basic_block in func_control_flow: print("ID: %i, Start: 0x%x, End: 0x%x" % (basic_block.id, basic_block.start_ea, basic_block.end_ea)) if basic_block.start_ea <= ea < basic_block.end_ea: print(" ea at Here") break succs_blocks = basic_block.succs() for addr in succs_blocks: print(" Successor: 0x%x" % addr.start_ea) pre_sblocks = basic_block.preds() for addr in pre_sblocks: print(" Predecessor: 0x%x" % addr.end_ea) if ida_gdl.is_ret_block(basic_block.type): print(" Return Block") ''' ID: 0, Start: 0x402da0, End: 0x402e10 Successor: 0x402e10 Successor: 0x402e1c ID: 1, Start: 0x402e10, End: 0x402e1c Successor: 0x4032e0 Predecessor: 0x402e10 ID: 2, Start: 0x402e1c, End: 0x402e40 Successor: 0x402e40 Successor: 0x402e4c Predecessor: 0x402e10 ID: 3, Start: 0x402e40, End: 0x402e4c Successor: 0x4032e0 Predecessor: 0x402e40 ID: 4, Start: 0x402e4c, End: 0x402e70 ea at Here '''
|
我之前写过一个判断MIPS命令注入的代码,大概就是,先定位对system
代码作为ea
,然后获取到ea
所在的基本块,再通过广度优先搜索得到基本块的所有前驱,直到在前驱基本块中得到所有的参数(MIPS是通过a0
、a1
、a2
、a3
寄存器进行传参),后续看能不能把代码整理出来。
调用idaapi.FlowChart()
实际上是调用的ida_gdl.FlowChart(f=None, bounds=None, flags=0)
,传参如下:
f
:通过get_func(ea)
获取
bounds
:如果f
参数不指定则在bounds
tuple类型(start, end)
范围获取基本块集合
flags
:FC_XXX
标准位,具体可以在此处查询
对于每一个基本块,都有如下的一些属性:
id
:基本块的ID
start_ea
:基本块的起始地址
end_ea
:基本块的结束地址
type
:基本块类型,如下依次数值从0~7
fcb_normal
:正常的基本块
fcb_indjump
:基本块以间接跳转结束
fcb_ret
:返回基本块,估计是函数ret
结束时的基本块
fcb_cndret
:条件返回的基本块
fcb_noret
:基本块不返回
fcb_enoret
:不属于函数的基本块,且不返回
fcb_extern
:外部的基本块(没有理解到含义)
fcb_error
:跳过不执行的基本块(不知道是否这样)
preds()
:获取当前基本块的所有前驱基本块集合
succs()
:获取当前基本块的所有后继基本块集合