利用angr符号执行去除ollvm虚假控制流
利用angr的SM(Simulation Managers)模拟管理器,通过调用SM的step()方法可以寻找到被混淆函数所有active状态的基本块(可达的基本块)。去除虚假控制流混淆的思路就是:先找到目标函数所有的基本块,然后将可达的基本块去除之后剩下的就是不可达的基本块,最后将不可达的基本块以及会跳转到不可达基本块的跳转指令进行nop即可去混淆。目标函数混淆之前的反编译代码如下:
利用-mllvm -bcf -mllvm -bcf_loop=3 -mllvm -bcf_prob=40
对其添加虚假控制流混淆后反编译代码如下:
其对应的控制流程图如下,混淆后增加了很多的虚假块。
利用angr去除虚假控制流#
找到所有的基本块#
利用angr的接口函数proj.analyses.CFGFast
,但是应用过程发现此函数好像存在一个暂未解决的bug,对于android平台arm/arm64
的可执行文件此接口并不支持(对于linux平台的arm/arm64是支持的),会出现如下错误angr.errors.SimProcedureError: static_exits() is not implemented for (SimProcedure __libc_init)
,具体原因请参考链接https://github.com/angr/angr/issues/967
。
因为我对angr还不是太熟悉无法解决上述问题,所以自己实现基本块的获取。如果函数只进行了虚假控制流混淆而未进行控制流平坦化的话,其基本块的寻找方法与包含控制流平坦化的基本块寻找方法略有差别,因为其基本块除了包含以跳转指令结尾的基本块之外还包含不以跳转指令结尾的基本块。(更新:问题已经解决,通过设置CFGFast的范围为指定函数可以解决上述错误)
所以获取基本块的思路是先获取所有以跳转指令结尾的基本块,然后获取所有基本块中跳转指令跳转到的目标基本块,这样就可以获取到所有的基本块信息。
def find_block(file_data):
global dict_code_blocks
global md
#寻找并保存以跳转指令结尾的基本块
is_new_block = 1
for i in md.disasm(file_data[func_start_address : func_end_address], func_start_address):
if is_new_block:
current_start_address = i.address
is_new_block = 0
if(i.group(ARM_GRP_JUMP) or i.id == ARM_INS_POP):
#需要过滤外部函数调用指令
if i.operands[0].type == ARM_OP_IMM:
if (i.operands[0].value.imm < func_start_address) or \
(i.operands[0].value.imm > func_end_address):
continue
elif i.operands[0].type == ARM_OP_REG:
if i.id != ARM_INS_POP:
continue
is_new_block = 1
dict_code_blocks[current_start_address] = {'start_address': current_start_address, \
'end_address': i.address}
#寻找不以跳转指令结尾的基本块
dict_code_blocks_tmp = {}
for v in dict_code_blocks:
for i in md.disasm(file_data[dict_code_blocks[v]['start_address'] : dict_code_blocks[v]['end_address'] + 4], \
dict_code_blocks[v]['start_address']):
if i.group(ARM_GRP_JUMP):
if i.operands[0].type == ARM_OP_IMM:
if (i.operands[0].value.imm < func_start_address) or \
(i.operands[0].value.imm > func_end_address):
continue
elif i.operands[0].type == ARM_OP_REG:
continue
#如果跳转指令对应的块地址还不在已经获取的基本块列表中,就寻找跳转地址所在的块并将其分割成两个块
if i.operands[0].value.imm not in dict_code_blocks:
for k in dict_code_blocks:
if i.operands[0].value.imm > dict_code_blocks[k]['start_address'] and \
i.operands[0].value.imm < dict_code_blocks[k]['end_address']:
dict_code_blocks_tmp[i.operands[0].value.imm] = {'start_address': i.operands[0].value.imm ,\
'end_address' : dict_code_blocks[k]['end_address'] ,\
'queue_start_address' : k ,\
'queue_end' : i.operands[0].value.imm - 4}
#保存不以跳转指令结尾的基本块
for i in dict_code_blocks_tmp:
dict_code_blocks[dict_code_blocks_tmp[i]['queue_start_address']]['end_address'] = dict_code_blocks_tmp[i]['queue_end']
dict_code_blocks[dict_code_blocks_tmp[i]['start_address']] = {'start_address' : dict_code_blocks_tmp[i]['start_address'] ,\
'end_address' : dict_code_blocks_tmp[i]['end_address']}
符号执行获取所有可达基本块#
通过angr的SM模拟管理器提供的step()方法从函数的入口开始符号执行,step每一次执行都会寻找SM包含的所有active状态的基本块可能到达的下一个基本块,然后SM将包含下一个active状态的基本块,之前active状态的基本块将被移除。当SM不再包含active状态的基本块时证明此函数不再有可达的基本块。
while len(simgr.active):
print(simgr.active)
for active in simgr.active:
# delete execd block
blocks.discard(active.addr - proj.loader.main_object.mapped_base)
#对每一个active块进行执行
simgr.step()
将step每一次执行后SM所包含的active状态的基本块(可达的基本块)从基本块列表中移除后,即可获取到此函数所有不可达的基本块。
nop所有不可达基本块#
nop_op = 0xE320F000
for block_addr in blocks:
size = dict_code_blocks[block_addr]['end_address'] - dict_code_blocks[block_addr]['start_address'] + 4
binfile[block_addr: block_addr + size] = nop_op.to_bytes(4, 'little') * int(size / 4)
nop所有跳转到不可达基本块的跳转指令#
因为不可达块永远不会执行,所以所有会跳转到不可达块的跳转指令都需要进行nop
for block_addr in dict_code_blocks:
for ins in md.disasm(file_data[dict_code_blocks[block_addr]['start_address'] : dict_code_blocks[block_addr]['end_address'] + 4], \
dict_code_blocks[block_addr]['start_address']):
if (ins.group(ARM_GRP_JUMP)):
if ins.operands[0].type == ARM_OP_IMM:
if (ins.operands[0].value.imm < func_start_address) or \
(ins.operands[0].value.imm > func_end_address):
continue
elif ins.operands[0].type == ARM_OP_REG:
continue
# 如果有跳转指令会跳转到不可达块就进行patch
if ins.operands[0].value.imm in blocks:
binfile[ins.address: ins.address + ins.size] = nop_op.to_bytes(4, 'little') * int(ins.size / 4)
break
最后去混淆的反编译代码如下,可以看到程序的基本逻辑已经被还原了。
删除不透明谓词去除虚假控制流#
因为控制流平坦化本质是通过不透明谓词构造恒成立的条件,ida无法识别确定这些不透明谓词的结果所以无法对程序进行优化。如果让ida能够确定不透明谓词的结果,那么ida就会自动对程序进行优化。将不透明谓词表达式对应的x,y变量所在的段设置为只读,并将其值设置为0。
然后再次F5后ida就可以将程序进行优化,优化后的结果如下,与之前通过angr符号执行去混淆的反编译结果基本相同。
参考:https://bbs.kanxue.com/thread-266005.htm
https://blog.csdn.net/qq_45323960/article/details/124440184
https://github.com/angr/angr/issues/967
https://blog.csdn.net/jitaliangliang111/article/details/104085410
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理