Unicorn反混淆:恢复被OLLVM保护的程序(一)

news2024/11/26 21:24:20

一、目标

现在很多程序利用ollvm的控制流平坦化来增加逆向分析的难度。 控制流平坦化 (control flow flattening)的基本思想主要是通过一个主分发器来控制程序基本块的执行流程,例如下图是正常的执行流程

1:show1

经过控制流平坦化后的执行流程就如下图:

1:show2

这样可以模糊基本块之间的前后关系,增加程序分析的难度。

二、分析

这里我们以 check_passwd_arm_flat 为例来尝试恢复被ollvm混淆后的程序。先拖进ida,从流程图上可以看到典型的 控制流平台化 之后的结果:

1:cfg

恢复的流程是 分块→找出真实块→确定真实块之间的调用关系→Patch二进制程序

分块

分块我们使用 angr 来实现。

filename = "./check_passwd_arm_flat"
start_addr = 0x83B0
end_addr = 0x87D4

project = angr.Project(filename, load_options={'auto_load_libs': False})
print(hex(project.entry))
cfg = project.analyses.CFGFast(regions=[(start_addr,end_addr)],normalize='True',force_complete_scan=False)

target_function = cfg.functions.get(start_addr)
#将angr的cfg转化为转化为类似ida的cfg
supergraph = am_graph.to_supergraph(target_function.transition_graph) 

找出真实块、序言、retn块和无用块

1.函数的开始地址为序言块的地址
2.无后继的块为retn块

# get prologue_node and retn_node
prologue_node = None
for node in supergraph.nodes():if supergraph.in_degree(node) == 0:prologue_node = nodeif supergraph.out_degree(node) == 0 and len(node.out_branches) == 0:retn_node = node

print("序言块={},retn块={}".format(hex(prologue_node.addr),hex(retn_node.addr))) 

在本例中,真实块的特点如下:

1.序言的后继为主分发器
2.后继为主分发器的块为预处理器
3.后继为预处理器的块为真实块
4.剩下的为无用块

Tip:

在实战中,需要具体分析出 主分发器, 不一定序言块之后的就一定是主分发器,也不一定存在预处理器。

def get_relevant_nop_nodes(supergraph, main_dispatcher_node, prologue_node, retn_node):# relevant_nodes = list(supergraph.predecessors(pre_dispatcher_node))relevant_nodes = []nop_nodes = []for node in supergraph.nodes():# print(hex(node.addr))# 和主分发器有联系,并且size大于8的,认为是真实块if supergraph.has_edge(node, main_dispatcher_node) and node.size > 8:# XXX: use node.size is faster than to create a blockrelevant_nodes.append(node)# print(hex(node.addr))continueif node.addr in (prologue_node.addr, retn_node.addr, main_dispatcher_node.addr):continue# 非真实块的默认要干掉nop_nodes.append(node)return relevant_nodes, nop_nodes 

输出结果:

******************* 真实块 ************************
序言块: 0x83b0
主分发器: 0x87d0
retn块: 0x87c0
真实块: ['0x875c', '0x86f4', '0x8794', '0x8658', '0x8628', '0x8714', '0x86d4', '0x87ac', '0x8770', '0x8694', '0x864c', '0x8734', '0x8788', '0x86b0', '0x867c'] 

确定真实块之间的调用关系

网上确定真实块之间的调用关系大多都是用 angr 的符号执行来实现,不过angr是个强大的工程,强练容易走火入魔,需要从基础练起。 无名侠 提供了一个Unicorn模拟执行的思路来寻找两个真实块的关系。所以本文使用Unicorn来确定真实块之前的调用关系。

我们只想得到ollvm路径,而不是真实代码块的运行结果,因此要尽可能屏蔽非ollvm的内存操作。具体屏蔽方法稍后介绍。下面这段代码初始化Unicorn的虚拟CPU,并映射程序代码内存以及栈空间,最后调用hook_add设置UC_HOOK_CODE和UC_HOOK_MEM_UNMAPPED的事件回调。UC_HOOK_CODE回调会在每条指令执行前被调用,UC_HOOK_MEM_UNMAPPED会在内存异常的时候调用。

# 初始化
load_base = 0
emu = Uc(UC_ARCH_ARM, UC_MODE_ARM | UC_MODE_LITTLE_ENDIAN)

# 映射代码段 0x8000是 check_passwd_arm_flat 代码段的基址
emu.mem_map(0x8000, 4 * 1024 * 1024)
emu.mem_write(0x8000,binByte)

STACK_ADDR = 0x7F000000
STACK_SIZE = 1024 * 1024
start_addr = None

emu.mem_map(STACK_ADDR, STACK_SIZE)
emu.hook_add(UC_HOOK_CODE, hook_code)
emu.hook_add(UC_HOOK_MEM_UNMAPPED,hook_mem_access) 

真实块之间的关系有两种:1、顺序 2、分支,针对本文的例子,真实块里面的分支指令有 moveq movne movlt movgt,是时候祭出这个表了:

条件字段表

条件后缀标志寄存器含义
EQZ == 1等于
NEZ == 0不等于
CS/HSC == 1无符号大于或相同
CC/LOC == 0无符号小于
MIN == 1负数
PLN == 0整数或零
VSV == 1溢出
VCV == 0无溢出
HIC == 1 and Z == 0无符号大于
LSC == 1 or Z == 0无符号小于或相同
GEN == V有符号大于或等于
LTN != V有符号小于
GTZ == 0 and N == V有符号大于
LEZ == 1 or N != V有符号小于或等于
AL任何始终。不可用于B{cond}中
Tip:

分支指令需要具体情况具体分析,没有通用一劳永逸的解决。除非多费点功夫把所有的分支指令都处理一遍。我怀疑 angr 的符号执行就是把这个活给干了。

# 分支处理
if ins.mnemonic != 'mov' and ins.mnemonic.startswith('mov'):print(">>> branch 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))if branch_control == 1 :vZ = (uc.reg_read(UC_ARM_REG_CPSR) & 0x40000000) >> 30vN = (uc.reg_read(UC_ARM_REG_CPSR) & 0x80000000) >> 31vV = (uc.reg_read(UC_ARM_REG_CPSR) & 0x10000000) >> 28if ins.mnemonic == 'moveq' or ins.mnemonic == 'movne' :if vZ == 0:uc.reg_write(UC_ARM_REG_CPSR,uc.reg_read(UC_ARM_REG_CPSR) | 0x40000000)print("Z 0->1 change cpsr = 0x%x" % uc.reg_read(UC_ARM_REG_CPSR))else:uc.reg_write(UC_ARM_REG_CPSR,uc.reg_read(UC_ARM_REG_CPSR) & 0xBFFFFFFF)print("Z 1->0 change cpsr = 0x%x" % uc.reg_read(UC_ARM_REG_CPSR))elif ins.mnemonic == 'movgt':if vZ == 0 and vN == vV:uc.reg_write(UC_ARM_REG_CPSR,uc.reg_read(UC_ARM_REG_CPSR) | 0x40000000)print("GT 0->1 change cpsr = 0x%x" % uc.reg_read(UC_ARM_REG_CPSR))else:uc.reg_write(UC_ARM_REG_CPSR,uc.reg_read(UC_ARM_REG_CPSR) & 0x2FFFFFFF)print("GT 1->0 change cpsr = 0x%x" % uc.reg_read(UC_ARM_REG_CPSR))elif ins.mnemonic == 'movlt':if vN != vV :uc.reg_write(UC_ARM_REG_CPSR,uc.reg_read(UC_ARM_REG_CPSR) & 0x6FFFFFFF)print("lt != -> = change cpsr = 0x%x" % uc.reg_read(UC_ARM_REG_CPSR))else:uc.reg_write(UC_ARM_REG_CPSR,uc.reg_read(UC_ARM_REG_CPSR) & 0xEFFFFFFF)print("lt = -> != change cpsr = 0x%x" % uc.reg_read(UC_ARM_REG_CPSR))else:print(">>> None " + ins.mnemonic) 

启动虚拟机的函数叫find_path,用于寻找真实块的下一个代码块。branch为分支控制。 如果branch = 1,则虚拟机在遇到movxx指令的时候会走movxx条件分支。

def find_path(uc,start_addr,branch = None):global block_startaddrglobal distAddrglobal isSucessglobal branch_controltry:block_startaddr = start_addrisSucess = FalsedistAddr = 0branch_control = branchuc.emu_start(start_addr,0x10000)print("emu end..")except UcError as e:pc = uc.reg_read(UC_ARM_REG_PC)if pc != 0:print("find_path UcError: %spc:%x" % (e,pc))return Noneelse:print("find_path ERROR: %spc:%x" % (e,pc))if isSucess:return distAddrreturn None 

控制流集成 使用队列的方式来路径搜索,起始搜索从函数入口开始。函数入口根据offset变量指定。 queue中的元素是一个二元组,第一项为执行地址,第二项为寄存器环境。每次搜索开始的时候从queue中获取一个将要搜索的真实块,设置寄存器,调用find_path搜索下一个真实块,将搜索到的真实块与新寄存器放入队列(保证上下文完整)使用这样做的好处就是可以搜索任意队列中的代码块,并且寄存器环境一定是和该代码块一致的。

queue = [(push_entry,None)]
flow = defaultdict(list)
patch_instrs = {}

while len(queue) != 0:env = queue.pop()pc = env[0]set_context(emu,env[1])if pc in flow:#print "???"continueflow[pc] = []print('------------------- run %#x---------------------' % pc)block = project.factory.block(pc)has_branches = False# 寻找有分支的代码块for ins in block.capstone.insns:if ins.insn.mnemonic != 'mov' and ins.insn.mnemonic.startswith('mov'):if pc not in patch_instrs:patch_instrs[pc] = inshas_branches = Trueif has_branches:# 有分支的代码块跑两次,一次正常,一次分支ctx = get_context(emu)p1 = find_path(emu,pc,0)if p1 != None:queue.append((p1,get_context(emu)))flow[pc].append(p1)set_context(emu,ctx)p2 = find_path(emu,pc,1)if p1 == p2:p2 = Noneif p2 != None:queue.append((p2,get_context(emu)))flow[pc].append(p2)else:p = find_path(emu,pc)if p != None:queue.append((p,get_context(emu)))flow[pc].append(p)

print("Emulation arm code done") 

路径探索,需要禁用掉一切函数调用、非栈空间内存访问,当虚拟机指令有内存操作需求时,判断目标内存地址范围是否在栈中,如果不在栈中则跳过该指令, 在本例中有一些内存访问代码段的固定值,这部分指令需要支持。 禁用的指令有bl、blx,只要识别bl前缀即可。

flag_pass = False

for b in ban_ins:if ins.mnemonic.find(b) != -1:flag_pass = Truebreak

if ins.op_str.find('[') != -1:if ins.op_str.find('[sp') == -1:flag_pass = Truefor op in ins.operands:# print(op.type)if op.type == ARM_OP_MEM:addr = 0if op.value.mem.base != 0:addr += uc.reg_read(reg_ctou(ins.reg_name(op.value.mem.base)))elif op.value.index != 0:addr += uc.reg_read(reg_ctou(ins.reg_name(op.value.mem.index)))elif op.value.disp != 0:addr += op.value.disp# 内存操作在栈区域if addr >= 0x7F000000 and addr < 0x7F000000 + 1024 * 1024 :flag_pass = False# 内存操作在代码区域if addr >= 0x8000 and addr < 0x9000: flag_pass = False

if flag_pass:print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))uc.reg_write(UC_ARM_REG_PC, address + size)return 

最后打印出找到的真实块之前的调用关系

************************flow******************************
0x83b0:['0x8628']
0x8628:['0x864c', '0x8658']
0x8658:['0x867c']
0x867c:['0x8628']
0x864c:['0x8694']
0x8694:['0x8794', '0x86b0']
0x86b0:['0x8788', '0x86d4']
0x86d4:['0x8788', '0x86f4']
0x86f4:['0x8714', '0x8788']
0x8788:['0x87ac']
0x87ac:['0x87c0']
0x87c0:[]
0x8714:['0x8788', '0x8734']
0x8734:['0x8770', '0x875c']
0x875c:['0x87c0']
0x8770:['0x8788']
0x8794:['0x87ac'] 

Patch二进制程序

首先把无用块都改成nop指令

for nop_node in nop_nodes:fill_nop(origin_data, nop_node.addr-base_addr,nop_node.size, project.arch) 

然后针对没有产生分支的真实块把最后一条指令改成jmp指令跳转到下一真实块

print("{} jmp {}".format(hex(parent),hex(childs[0])) )
# 把最后一条指令改成jmp指令跳转到下一真实块
parent_block = project.factory.block(parent)
last_instr = parent_block.capstone.insns[-1]
file_offset = last_instr.address - base_addr

patch_value = ins_b_jmp_hex_arm(last_instr.address, childs[0], 'b')
if project.arch.memory_endness == "Iend_BE": patch_value = patch_value[::-1]
patch_instruction(origin_data, file_offset, patch_value) 

针对产生分支的真实块把MOVX指令改成相应的条件跳转指令跳向符合条件的分支,例如moveq 改成beq ,再在这条之后添加b 指令跳向另一分支

instr = patch_instrs[parent]
# print("0x%x: %s \t%s\t%s" % (instr.insn.address,getByteStr(instr.insn.bytes), instr.insn.mnemonic, instr.insn.op_str))
file_offset = instr.insn.address - base_addr

parent_block = project.factory.block(parent)
fill_nop(origin_data, file_offset, parent_block.addr + parent_block.size - base_addr - file_offset, project.arch)

# patch the movx instruction to bx instruction
bx_cond = 'b' + instr.insn.mnemonic[len('mov'):]
patch_value = ins_b_jmp_hex_arm(instr.insn.address, childs[0], bx_cond)
if project.arch.memory_endness == 'Iend_BE':patch_value = patch_value[::-1]
patch_instruction(origin_data, file_offset, patch_value)

file_offset += 4
# patch the next instruction to b instrcution
patch_value = ins_b_jmp_hex_arm(instr.insn.address+4, childs[1], 'b')
if project.arch.memory_endness == 'Iend_BE':patch_value = patch_value[::-1]
patch_instruction(origin_data, file_offset, patch_value) 

最后用ida查看修复之后的cfg

1:cfgex

可以看到CFG跟原来的大致一样,然后反编译恢复出原始代码

bool __fastcall check_password(_BYTE *a1)
{int v2; // [sp+18h] [bp-10h]int i; // [sp+1Ch] [bp-Ch]v2 = 0;for ( i = 0; a1[i]; ++i )v2 += (unsigned __int8)a1[i];return i == 4&& v2 == 0x1A1&& (signed int)(unsigned __int8)a1[3] > 'c'&& (signed int)(unsigned __int8)a1[3] >= 'e'&& *a1 == 'b'&& ((unsigned __int8)a1[3] ^ 0xD) == (unsigned __int8)a1[1];
} 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/167128.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RT-Thread系列--启动过程

一、目的RT-Thread是一个小而美的RTOS&#xff0c;所有的RTOS的都有一个特点&#xff0c;那就是存在调度器&#xff0c;像RT-Thread的调度器是一个实时抢占式调度器&#xff08;同优先级任务使用时间片调度方式&#xff09;。本篇不讲调度器相关的内容&#xff0c;着重讲解调度…

【博客591】LVS的DR和NAT模式下要注意的缺陷

LVS的DR和NAT模式下要注意的缺陷 1、DR模式和NAT模式的转发原理 2、LVS的DR和NAT模式下要注意的缺陷 缺陷1&#xff1a;DR模式下的realserver和 lvs的vip提供服务的端口必须一致。 例如vip的端口对外端口为 80&#xff0c;但后端服务的真实端口为8080&#xff0c;通过lvs的D…

SpringBoot快速上手

SpringBoot 概述 Spring Boot 可以轻松创建独立的、生产级的基于 Spring 的应用程序&#xff0c;您可以“直接运行”。 特征 创建独立的 Spring 应用程序 直接嵌入Tomcat&#xff0c;Jetty或Undertow&#xff08;无需部署WAR文件&#xff09; 提供固执己见的“入门”依赖项…

初始C语言 - 函数(2)

目录 1.函数的嵌套调用和链式访问 1&#xff09;函数嵌套调用 2&#xff09;函数的链式访问 - 函数的返回值作为另一个函数的参数 2. 函数的声明和定义 1&#xff09;变量的声明和定义 2&#xff09;函数的声明和定义 //函数必须先声明后使用//函数的声明写在头文件里 3.…

集成学习、Bagging集成原理、随机森林构造过程、随机森林api与案例、boosting集成原理、梯度提升决策树(GBDT)、XGBoost与泰勒展开式

一、集成学习 集成学习&#xff1a;通过建立几个模型来解决单一预测问题&#xff0c;工作原理是生成多个分类器/模型&#xff0c;各自独立地学习和作出预测。这些预测最后结合成组合预测&#xff0c;因此优于任何一个单分类的做出预测 机器学习的两个核心任务 集成学习中boost…

【UE4 第一人称射击游戏】51-制作手榴弹

上一篇&#xff1a;【UE4 第一人称射击游戏】50-用另一种方法实现僵尸随机漫游 僵尸攻击玩家时造成伤害本篇效果&#xff1a;按G键投掷出手榴弹&#xff0c;产生爆炸效果步骤&#xff1a;新建一个蓝图类&#xff08;父类为Actor&#xff09;&#xff0c;命名为“GrenadeActor”…

水声功率放大器模块在圆柱壳结构声源辐射研究中的应用

客户需求&#xff1a;实验需要在消声水池中对圆柱壳声源振动和远场声压进行实验测量&#xff0c;圆柱壳内部尺寸为&#xff1a;高0.5m(不含盖板)&#xff0c;壳外径0.5&#xff0c;内径0.497m&#xff0c;上下盖板高0.014m&#xff0c;所以对圆柱壳内功率放大模块的尺寸以及供电…

Qt图表操作(QCustomPlot 与 QtCharts的介绍与使用)

一、QCustomPlot简介 QCustomPlot是QT下一个方便易用的绘图工具&#xff0c;该绘图库专注于制作美观&#xff0c;出版品质的2D图表&#xff0c;图表和图表&#xff0c;以及为实时可视化应用程序提供高性能。它可以导出为各种格式&#xff0c;如矢量化的PDF文件和光栅化图像&…

Task12 数据缘何而来数据格式

目录1 常见的格式1.1 Excel文件的格式1.2 Excel数据的格式1.3 基本知识2 Excel数据格式2.1 数据类型转换3 练习1 常见的格式 1.1 Excel文件的格式 Excel文件的常见格式&#xff1a;.xls和.xlsx 1.2 Excel数据的格式 Excel数据的存储不同格式&#xff1a;xlsx、csv、txt cs…

mysql快速生成100W条测试数据(8)全球各城市人口及经济增长速度并存入mysql数据库

这是之前的文章里面包含一些以前的一些操作流程可以进行参考学习 更加详细操作步骤在第一篇文章里面 mysql快速生成100W条测试数据&#xff08;1&#xff09;&#xff1a;游戏人物数据 mysql快速生成100W条测试数据&#xff08;2&#xff09;公司员工信息 mysql快速生成100W条测…

本地挂载网盘_Alist_RaiDrive_windows

目录 一、下载安装Alist 二、启动登录Alist 三、挂载网盘 四、挂载到本地 五、开机自启动 一、下载安装Alist 安装地址&#xff1a;https://github.com/alist-org/alist 二、启动登录Alist 1.打开alist.exe所在目录&#xff0c;输入cmd 2.利用cmd&#xff0c;输入alist …

[oeasy]python0051_ 转义_escape_字符_character_单引号_双引号_反引号_ 退格键

转义字符 回忆上次内容 上次研究的是进制转化10进制可以转化为其他形式 binocthex 其他进制也可以转化为10进制 int可以设置base来决定转为多少进制 回忆一下 我们为什么会有八进制&#xff1f;因为需要用八进制输出转义字符 \ooo 把(ooo)8进制对应的ascii字符输出 就如同 \…

QMAKE_POST_LINK QMAKE_PRE_LINK解释

命令解释 QMAKE_POST_LINK是在可执行程序链接后执行它的命令 QMAKE_PRE_LINK是在可执行程序链接前执行它的命令 注意C/C程序是先编译后链接 如果你需要一个编译前执行的命令可以使用 copy_files.files $$filelist copy_files.path $$OUT_PWD COPIES copy_files即使是编…

存储与数据库 | 字节青训营笔记

目录 一、存储系统 1、什么是存储系统 2、存储系统的特点 3、RAID技术 RAID出现的背景 RAID 0 RAID 1 RAID 01 二、数据库 1、难道数据库和存储系统不一样吗 2、数据库vs经典存储 三、主流产品剖析 1、单机存储 本地文件系统 key-value存储 2、分布式存储系统 …

【程序员陪你过大年】html+css+js 实现春节动态烟花特效及服务器部署

前言 不知不觉又到了年底&#xff0c;这一年是值得庆贺的一年&#xff0c;疫情过去&#xff0c;经济好转。我们急需在春节这个特殊的日志释放下自己的情绪。但是大部分地区都不让放炮&#xff0c;于是乎我为大家带来一套十分炫酷应景的春节烟花动画代码实现。效果如下图所示 :…

第10章 FreeRTOS

ESP32 FREERTOS 打印ESP32任务 menuconfig中&#xff0c;打开FreeRTOS的trace打印功能 menuconfig中&#xff0c;增加app_main主任务的栈大小 测试代码 ESP32最小工程 #include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h&q…

php sql注入

文章目录一、什么是sql注入二、sql注入处理1、使用内置函数2、使用pdo预处理语句三、安全注意事项一、什么是sql注入 在应用程序中&#xff0c;为了和用户交互&#xff0c;允许用户提交输入数据&#xff0c;假如应用程序并没有对用户输入数据进行处理&#xff0c;攻击者可以输…

linux开发工具

文章目录linux开发工具1.软件包管理器yum2.编辑器vim3.gcc4.make与makefile5.git6.gdb7.小程序进度条linux开发工具 1.软件包管理器yum yum等同于手机上的应用商店.yum自动解决软件之间的耦合问题 yum list 显示软件清单 yum install 下载 yum remove 删除 2.编辑器vim 其…

opencv开发之numpy使用

打开Spyder, 在IPython控制台中输入 import numpy as np 引入numpy库并使用numpy构造一个ndarray对象: np.zeros((2,4),np.uint8),该对象为一个二维数组 ,构造一个2行4列的二维数组(矩阵) ,并初始化所有元素为0,及指定数据类型为uint8 创建并初始化:数据可视化:取矩阵类型: typ…

基于YOLOv4的车辆检测 MATLAB实现

目录 摘要 研究背景 算法设计及实现过程 车辆目标数据集的构建 基于YOLOv4的目标检测 对YOLOv4模型进行改进 实验结果及分析 结论与展望 代码实现 摘要 针对车辆检测&#xff0c;本文提出了一种基于YOLOv4车辆检测算法。制作了一个多天侯、多时段、多场景的车辆目标数…