首页
社区
课程
招聘
[原创]Ollvm混淆还原学习
发表于: 2025-11-21 21:45 6486

[原创]Ollvm混淆还原学习

2025-11-21 21:45
6486

本文的目标是分析 OLLVM 核心混淆技术——虚假控制流(BCF)和控制流平坦化(FLA/CFF),并提供多种可复现的去混淆实战方法,这些方法并非万能的,有些如随机控制流、指令替换、常量替换等混淆还是无法去除,后续需要继续研究分析。

感谢 oacia九天 666乐子人 等作者的文章。

只针对的 Android 端的混淆特征以及混淆原理的学习,如有错误欢迎指正。

如侵权可联系修正删除。

在深入实战之前,我们必须彻底理解这两种混淆的“魔术”究竟是什么。

BCF 的核心思想是 “制造干扰”。它通过插入永远不会被执行(或功能上无意义)的代码块,并使用复杂的、静态分析难以解析的“不透明谓词”(Opaque Predicate)来引导跳转,从而污染(Pollute)程序的控制流图(CFG)。

基本形态:
一个原始的基本块 A 会被分裂成至少两个块(例如 A_preA_post)。

干扰引入:
A_preA_post 之间,BCF 会插入一个或多个“虚假”的基本块(B_bogus)。

不透明谓词 (Opaque Predicate):
A_pre 的结尾会有一个条件跳转。这个跳转的条件是一个“不透明谓词”——它是一个在运行时 结果恒定(始终为真或始终为假),但静态分析器(如 IDA Pro)难以或无法 在不运行代码的情况下确定其结果的表达式。

混淆效果:

原理图

图片描述

BCF 的关键: 识别并“拆除”那个恒为真/假的“不透明谓词”,nop 恒为假的虚假块,让 CFG“塌陷”回真实路径。

如果说 BCF 是“制造干扰”,那么 FLA(或 CFF)就是一种 “摧毁结构” 的混淆。它彻底颠覆了函数正常的执行流图(CFG),将其“拍扁”(Flatten),所有真实的代码块都变成由一个中央调度器(Dispatcher)来“发牌”和“调度”的“子程序”。

基本思想:
FLA 会摧毁函数内所有的“直接跳转”(如 BB_1 结束时直接跳到 BB_2)。取而代之的是,它引入一个“状态变量”(State Variable),并建立一个巨大的、循环的“分发器”(Dispatcher)。

平坦化流程 476K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Z5k6i4S2Q4x3X3c8J5j5i4W2K6i4K6u0W2j5$3!0E0i4K6u0r3j5X3I4G2k6#2)9J5c8X3S2W2P5q4)9J5k6s2u0S2P5i4y4Q4x3X3c8E0K9h3y4J5L8$3y4G2k6r3g2Q4x3X3c8S2M7r3W2Q4x3X3c8$3M7#2)9J5k6r3!0T1k6Y4g2K6j5$3q4@1K9h3&6Y4i4K6u0V1j5$3!0E0M7r3W2D9k6i4t1`.

图片描述

图片描述

图片描述

图片描述

正常的执行流程

图片描述

经过控制流平坦化后的执行流程

图片描述

控制流平坦化后的 CFG:

图片描述

核心组件:

序言 (Prologue):
这是函数的入口块。它的主要作用是初始化“状态变量”,将其设置为第一个“真实块”的 ID。

主分发器 (Main Dispatcher):
这是 FLA 的心脏,通常是一个巨大的 while(true) 循环。这个循环的主体是一个 switch 语句(或等效的 if-else 链),它根据“状态变量”的当前值来决定下一步要跳向何处。这就是图中的“Hub-and-Spoke”(集线器-辐条)模型的“集线器”。

真实块 (Relevant Blocks):
这些是程序原始的基本块(BB),它们包含了函数真正的业务逻辑。在图中,它们被“拍扁”并排列在最底层。它们现在是完全“隔离”的“岛屿”,互相之间没有任何直接的 CFG 连接。

预处理块 (Predispatcher) / 状态更新:
这是 FLA 的关键所在。 当一个“真实块”执行完毕后,它 不会 跳转到下一个“真实块”。相反,它会跳转到一个(或多个)“预处理块”。这个块的 唯一目的 就是 修改“状态变量”的值,将其设置为下一个 应该 执行的“真实块”的 ID。

如图所示,所有“真实块”的执行(蓝线)都将汇聚到“预处理块”,该块完成状态更新后,再 无条件地跳回“主分发器”,形成一个闭环。

子分发器 (Sub-Dispatcher) (高级变种):
正如下图所示,FLA 可以被嵌套和复杂化。“主分发器”可能不会直接跳转到“真实块”,而是跳转到一个“子分发器”。这个“子分发器”可能包含另一层 switch 或复杂的条件判断,进一步混淆状态和目标块之间的关系,然后再跳转到“真实块”。

Return:

返回块

执行流程(控制流平坦化后的 CFG):

混淆效果:

FLA 的关键: 找到那个“状态变量”,并静态或动态地分析出“状态转移图”(State Transition Graph),即 case 1 之后 state 会变成几?case 2 之后呢?找到所有的“真实块”和执行流程,从而重建原始的 CFG。

分析平台:

脚本与自动化:

符号执行/约束求解(可选高级方法):

angr: 强大的二进制分析和符号执行框架。(python 3.10 版本 pip install angr-management

z3-solver: 微软的 SMT 约束求解器。

建议安装 Z3 以便使用 D-810 的几个功能:

目标样本:

示例为:1f5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6G2j5h3y4A6j5g2)9J5k6h3c8W2N6W2)9J5c8X3!0D9L8s2k6E0i4K6u0V1M7%4c8#2k6s2W2Q4x3V1j5`. 中的 ollvm_bcf-fla-sub.ziptest-bcf 二进制文件

参考:https://bbs.kanxue.com/thread-266005.htm (内包含示例)

注意:需要安装python 3.10 版本

工具与思路:

angr 之所以能“去除”这种虚假控制流,是因为它使用了 符号执行 (Symbolic Execution),而这种混淆技术的核心——不透明谓词 (Opaque Predicate)——在强大的符号执行引擎面前是无效的。

angr 的核心是 符号执行引擎 和一个 约束求解器 (SMT Solver, 如 Z3)

angr 遇到这个混淆代码时,它会这样做:

angr 通过其强大的求解器,在数学上证明了 哪些路径是“虚假”的(永远不会执行),哪些是“真实”的(永远会执行)。

当您要求 angr 生成控制流图(CFG)时,它只会显示它找到的 唯一真实执行路径。这个过程就等同于“去除”了所有由不透明谓词制造的虚假 goto 和虚假 while 循环,将所有被拆分的真实代码块重新按正确的顺序拼接了起来。

Angr 脚本实现:

重建 CFG:

根据 angr 的“可达”路径(sat states),重建一个干净的 CFG。

对比图(示例为:ollvm_bcf-fla-sub.zip 中的 test-bcf)

图片描述

参考:621K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6U0M7e0j5%4y4o6x3#2x3o6f1J5z5g2)9J5c8X3c8W2k6X3I4S2N6l9`.`.

备注:运行比较慢,需要等待一段时间,但效果和之前一样

参考:4b6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6G2j5h3y4A6j5g2)9J5k6h3c8W2N6W2)9J5c8X3!0D9L8s2k6E0i4K6u0V1M7%4c8#2k6s2W2Q4x3V1j5`.

核心原理:

最大风险:

编写 IDAPython 脚本:

它先用 patch_dword 修改了底层的原始字节

然后用 update_segm 保存了段权限

最后用 del_itemscreate_dword 摧毁了所有旧的分析缓存,迫使 IDA 基于新的字节和新的权限从头开始分析。

Patch 与效果演示:

.bss 段手动修改流程

图片描述

图片描述

图片描述

CFG 恢复原状,以及 F5 伪代码变得简洁可读。

图片描述

参考:14cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6G2j5h3y4A6j5g2)9J5k6h3c8W2N6W2)9J5c8X3!0D9L8s2k6E0i4K6u0V1M7%4c8#2k6s2W2Q4x3V1j5`.

原理:

我们之前的脚本(方法三)是“修改数据”:它把 .bss 段的数据改成常量 2,然后设为只读,依赖 IDA 的分析引擎 去自动“算”出 if(2 >= 10) 为假。

这个脚本是“修改代码”:它不修改 .bss 段的数据,而是 直接修改所有“读取”这些数据的代码

最大风险:

ida python 脚本:

[FLA 的恢复通常比 BCF 更复杂,核心是找到状态转移关系。]

示例:https://bbs.kanxue.com/thread-286151.htm 附件作为样本

标准和非标准 fla 区分:

非标准 fla 的循环头地址和汇聚块的地址是相等的

图片描述

标准 fla 的循环头地址和汇聚块的地址是不相等的,其循环头的前驱只有两个基本块, 一个是序言块, 一个是汇聚块。

图片描述

插件功能展示

图片描述

示例:https://bbs.kanxue.com/thread-286151.htm 附件的 init_proc() 函数作为样本

思路:

CFG 示例

idapython 脚本:

对样本中的 init_proc() 去混淆

图片描述

示例:https://bbs.kanxue.com/thread-286151.htm 附件中的 sub_41D08() 函数作为样本

思路:

CFG 示例

idapython 脚本:

对样本中的 sub_41D08() 去混淆(TODO :但是测试发现,对一些样本还原效果不理想,后续需研究)

示例 1:https://bbs.kanxue.com/thread-277428-1.htm 评论区中经过去除 br x9 后的 JNI_OnLoad() 函数作为样本

示例 2:https://bbs.kanxue.com/thread-286151.htm 附件的 init_proc() 函数,

附件示例:作为 本文 unidbg 的去混淆复现的样本

首先下载 unidbg-0.9.8,并进行测试,测试通过如下

先看一下 main 函数

后续逐个分析 main 函数中的子函数作用

(1)然后通过 Unidbg 模板加载 so,一定先跑起来!!!!

(2)主动调用目标函数,目的是触发 hook

(3)跑起来后,通过 unidbg hook 目标函数,打印指令执行流,试验是否有指令流产生

(4)同上 hook(此处可与之前的 hook 合并在一个函数中):通过 unidbg hook 主动调用的函数,将其以对象 InsAndCtx 存储在栈容器 instructions 中,内部利用 do_processflt 函数进行处理

(5)关键处理 do_processflt 函数

所以我们可以按照以下算法对控制流平坦化进行还原:

同时,我们在主分发器处 hook 指令,记录每次经过主分发器时的索引寄存器的值为索引值顺序。

图片描述

(6)do_processflt 处理之后,先打印和处理各个索引值和真实块信息

问:有的一分支,trace时发现,程序原有逻辑没有经过,为什么要处理?

答:一个 app 的不同动作对 if 的不同分支都可能触发,那么我们根据程序运行的主要逻辑,找到另一个分支真实块,这样代码还原时,才不会缺省。

如果不处理会出现类似如下报错:

图片描述

那么如何寻找呢?

情况一(B.NE):0x76B76AA2-> 0x1BB14,B.NE寻找下一层跳到非主分发器上的分支地址0x1BB14,可得 tbs.add(new TrueBlock(0x76b76aa2L,0x1bb14));

图片描述

情况二(B.EQ):0xA618D6AF-> 0x1BE3C,B.EQ寻找紧跟后面的地址0x1BE3C(大多数代码会自动处理好), 可得 true block index a618d6af,addr 1be3c
图片描述

除了 CSEL(if-else)分支要处理,如果后续有报错,那么必须要根据 trace 流,手动加入【欠缺的索引值,对应的后续真实块地址】如下第二部分

图片描述

(7)patch 连接真实块

手动处理:对真实块最后为【B 主分发器地址 dispatcher】 的指令处进行 patch

图片描述

特殊处理部分:即对【b.lt dispatcher】的指令进行 patch

图片描述

需要手动分析

有一部分没有还原成功,导致最外层还有一个 while 循环,以及部分真实块没有反编译出来(dclose 函数没有反编译出来),还需要细致化分析处理

此作为分析示例,但是没有完全达到 D810 的效果,但是大多数已经满足,可以以此了解去混淆思路,从而预防 D810 等插件去混淆

样本较简单,只作为学习

基于 ida pro 分析的伪代码,交给大模型分析,效果还不错!

BCF: 侧重于制造“噪音”,恢复关键是“去噪”(识别不透明谓词)。

FLA: 侧重于“重构”,恢复关键是“重建”(找到状态转移)。

D810 插件需要仔细研究,了解其去混淆原理

2025-11-24 ——针对 fla 中的方法四的TODO(附件已更新)

最外层还有一个 while 循环原因找到了:序言块最后一个 b 指令应该直接跳转到第一个真实块,而不是主分发器

部分真实块没有反编译出来原因(dclose 函数没有反编译出来):在打印索引对应的真实块中发现,在寻找 b.eq 对应的跳转地址时,有些索引对应的下一个真实块地址是主分发器,这肯定是不对的,因此需要修改其对应的下一跳真正的真实块地址

​ 如果存在下面的情况时,就需要手动修复(每个样本此处可能有所变化)

图片描述

手动修复后日志信息如下:

图片描述

将大多数手动 patch 转成自动 patch,减少出错几率

图片描述

python -m pip install z3-solver
python -m pip install z3-solver
python-m pip install angr-management
python-m pip install angr-management
python debogus.py [-h] [-f FILE] [-s START]
# python debogus.py -f example/example-1 -s 0x404350
python debogus.py [-h] [-f FILE] [-s START]
# python debogus.py -f example/example-1 -s 0x404350
# 导入angr主库,用于进行二进制分析和符号执行
import angr
 
# 从angr-management(angr的GUI工具)中导入一个图工具,用于简化CFG(控制流图)
from angrmanagement.utils.graph import to_supergraph
 
# 导入argparse库,用于解析命令行传入的参数(如-f和-s)
import argparse
 
# 导入logging库,用于控制日志输出级别(后面用来关闭angr的冗余日志)
import logging
 
# 导入os库,用于处理文件路径和文件名
import os
 
# def patch_jmp(block, jmp_addr):
# #     # (获取基本块中最后一条指令)
# #     insn = block.capstone.insns[-1]
# #     # (计算指令在文件中的偏移量)
# #     offset = insn.address - proj.loader.main_object.mapped_base
# #     # (将原始的 jx/jnx 指令替换为 NOP)
# #     binfile[offset : offset + insn.size] = b'\x90' * insn.size    
# #     # (修补一个新的 JMP 指令,使其跳转到真实的后继块)
# #     binfile[offset : offset + 5] = b'\xE9' + (jmp_addr - (insn.address + 5)).to_bytes(4, 'little', signed=True)
# #     # (打印修补日志)
# #     print('Patch [%s\t%s] at %#x' % (insn.mnemonic, insn.op_str, insn.address))
#
     
# 定义一个函数,用于将一个基本块的所有指令替换为 NOP(空操作)
def patch_nops(block):
    # 计算基本块的内存地址(block.addr)相对于文件基址的偏移量,得到文件偏移
    offset = block.addr - proj.loader.main_object.mapped_base
     
    # binfile 是一个全局的 bytearray。这行代码将文件中该基本块对应的所有字节,全部替换为 \x90 (NOP)
    binfile[offset : offset + block.size] = b'\x90' * block.size
     
    # 打印日志,告知用户哪个地址的基本块被 NOP 了
    print('Patch nop at block %#x' % block.addr)
 
#
# 定义一个函数,用于获取指定函数地址的控制流图 (CFG)
def get_cfg(func_addr):
    # 运行angr的CFGFast分析,normalize=True表示标准化图,force_complete_scan=False表示不进行强制完全扫描
    cfg = proj.analyses.CFGFast(normalize=True, force_complete_scan=False)
     
    # 从完整的CFG中,根据函数地址(func_addr)获取特定函数的CFG(转换图)
    function_cfg = cfg.functions.get(func_addr).transition_graph
     
    # 使用 to_supergraph 将CFG图进行简化,合并节点
    super_cfg = to_supergraph(function_cfg)
     
    # 返回简化后的CFG
    return super_cfg
 
#
# 定义核心的去混淆函数
def deobfu_func(func_addr):
    # 创建一个空的集合(set),用于存放所有基本块的地址
    blocks = set()
     
    # 调用 get_cfg 获取目标函数的CFG
    cfg = get_cfg(func_addr)
     
    # 遍历CFG中的所有节点(基本块)
    for node in cfg.nodes:
        # 将每个基本块的地址添加到集合中
        blocks.add(node.addr)
         
    # (用于调试) 打印出此函数中所有基本块的地址(此时包含真实块和虚假块)
    print([hex(b) for b in blocks])
     
    # 注释:下面开始进行符号执行
    # Symbolic execution
     
    # 创建一个angr的“空白状态”,指定符号执行的起始地址为目标函数地址
    state = proj.factory.blank_state(addr=func_addr)
     
    # 创建一个“模拟管理器”(Simulation Manager),用于管理和驱动符号执行
    simgr = proj.factory.simgr(state)
     
    # 当模拟管理器中还有“活跃”的(即未探索完的)路径时,循环继续
    while len(simgr.active):
        # 遍历当前所有的活跃路径
        for active in simgr.active:
            # !!! 核心逻辑 !!!
            # active.addr 是当前符号执行“真实”到达的地址
            # .discard() 会从集合中移除这个地址。
            # 这样,所有真实能到达的块都会被移除
            blocks.discard(active.addr)
             
            # --- 下面是为 call 指令添加 Hook,以加速符号执行 ---
             
            # 获取当前活跃状态地址对应的基本块对象
            block = proj.factory.block(active.addr)
             
            # 遍历基本块中的每一条指令
            for insn in block.capstone.insns:
                # 如果指令是一个 call 指令
                if insn.mnemonic == 'call':
                    # 解析 call 指令的操作数(即目标地址),将其转为整数
                    next_func_addr = int(insn.op_str, 16)
                     
                    # 在这个 call 的地址上“挂钩”(hook)
                    # "ReturnUnconstrained" 是一个angr的“桩”(stub)
                    # 它告诉angr:不要深入分析这个call,假装它执行了并返回一个“任意值”,然后继续
                    proj.hook(next_func_addr, angr.SIM_PROCEDURES["stubs"]["ReturnUnconstrained"](), replace=True)
                     
                    # 打印日志,告知用户哪个 call 被 hook 了
                    print('Hook [%s\t%s] at %#x' % (insn.mnemonic, insn.op_str, insn.address))
                     
        # 让模拟管理器“走一步”,即探索所有活跃路径的下一个基本块
        simgr.step()
         
    # 当 while 循环结束(所有真实路径都探索完毕),`blocks` 集合中“剩下”的地址就是虚假块
    # 遍历所有这些“剩下”的(虚假的)基本块地址
    for block_addr in blocks:
        # 调用 patch_nops 函数,将这些虚假块在文件中 NOP 掉
        patch_nops(proj.factory.block(block_addr))
 
#
# 程序的标准入口点
if __name__ == '__main__':
    # 注释:关闭警告日志
    # Disable warning
     
    # 将cle(angr的加载器)的日志级别设为ERROR,屏蔽无关信息
    logging.getLogger('cle').setLevel(logging.ERROR)
     
    # 将angr主库的日志级别设为ERROR,屏蔽无关信息
    logging.getLogger('angr').setLevel(logging.ERROR)
     
    # 创建一个命令行参数解析器
    parser = argparse.ArgumentParser()
     
    # 添加 '-f' 或 '--file' 参数,必需(required=True),用于指定要分析的文件
    parser.add_argument('-f', '--file', required=True, help='File to deobfuscate')
     
    # 添加 '-s' 或 '--start' 参数,用于指定目标函数地址。
    # type=lambda x : int(x, 0) 允许输入十进制或十六进制(0x...)
    parser.add_argument('-s', '--start', type=lambda x : int(x, 0), help='Starting address of target function')
     
    # 解析命令行传入的参数
    args = parser.parse_args()
     
    # 注释:将二进制文件加载到angr
    # Load binary file ${file} into angr
     
    # 创建angr项目。args.file 是文件名。auto_load_libs=False 表示不自动加载libc等依赖库,加快分析
    proj = angr.Project(args.file, load_options={"auto_load_libs": False})
     
    # 从解析的参数中获取起始地址
    start = args.start
     
    # 如果用户没有提供 -s/--start 参数
    if start == None:
        # 尝试自动在二进制文件中查找 'main' 函数的符号
        main = proj.loader.find_symbol('main')
         
        # 如果 'main' 函数也找不到
        if main == None:
            # 报告错误并退出,提示用户必须提供 -s 参数
            parser.error('Can\'t find the main function, please provide argument -s/--start')
             
        # 如果找到了 main,使用 main 函数的重基址地址作为起始地址
        start = main.rebased_addr
         
    # 注释:将二进制文件读入内存
    # Load binary file ${file} into memory
     
    # 以二进制只读('rb')模式打开目标文件
    with open(args.file, 'rb') as file:
        # 读取文件的全部内容,并存入一个“可变的” bytearray 中,命名为 binfile
        # (必须是 bytearray 才能在内存中进行修补)
        binfile = bytearray(file.read())
         
    # 注释:对目标函数执行去混淆操作
    # Do deobfuscation on target function
     
    # 调用核心去混淆函数,传入目标函数地址
    deobfu_func(func_addr=start)
     
    # 注释:将被修复的二进制文件写入 ${file}_recovered
    # Write the recovered binary file to ${file}_recovered
     
    # 将原始文件名(如 "example/example-1")分割为文件名("example/example-1")和扩展名("")
    fname, ext = os.path.splitext(args.file)
     
    # 以二进制写入('wb')模式打开一个新文件(文件名后缀为 _recovered)
    with open(fname + '_recovered' + ext, 'wb') as file:
        # 将我们修补过的(已 NOP 掉虚假块)binfile 的内容写入新文件
        file.write(binfile)
         
    # 打印成功信息
    print('Deobfuscation success!')
# 导入angr主库,用于进行二进制分析和符号执行
import angr
 
# 从angr-management(angr的GUI工具)中导入一个图工具,用于简化CFG(控制流图)
from angrmanagement.utils.graph import to_supergraph
 
# 导入argparse库,用于解析命令行传入的参数(如-f和-s)
import argparse
 
# 导入logging库,用于控制日志输出级别(后面用来关闭angr的冗余日志)
import logging
 
# 导入os库,用于处理文件路径和文件名
import os
 
# def patch_jmp(block, jmp_addr):
# #     # (获取基本块中最后一条指令)
# #     insn = block.capstone.insns[-1]
# #     # (计算指令在文件中的偏移量)
# #     offset = insn.address - proj.loader.main_object.mapped_base
# #     # (将原始的 jx/jnx 指令替换为 NOP)
# #     binfile[offset : offset + insn.size] = b'\x90' * insn.size    
# #     # (修补一个新的 JMP 指令,使其跳转到真实的后继块)
# #     binfile[offset : offset + 5] = b'\xE9' + (jmp_addr - (insn.address + 5)).to_bytes(4, 'little', signed=True)
# #     # (打印修补日志)
# #     print('Patch [%s\t%s] at %#x' % (insn.mnemonic, insn.op_str, insn.address))
#
     
# 定义一个函数,用于将一个基本块的所有指令替换为 NOP(空操作)
def patch_nops(block):
    # 计算基本块的内存地址(block.addr)相对于文件基址的偏移量,得到文件偏移
    offset = block.addr - proj.loader.main_object.mapped_base
     
    # binfile 是一个全局的 bytearray。这行代码将文件中该基本块对应的所有字节,全部替换为 \x90 (NOP)
    binfile[offset : offset + block.size] = b'\x90' * block.size
     
    # 打印日志,告知用户哪个地址的基本块被 NOP 了
    print('Patch nop at block %#x' % block.addr)
 
#
# 定义一个函数,用于获取指定函数地址的控制流图 (CFG)
def get_cfg(func_addr):
    # 运行angr的CFGFast分析,normalize=True表示标准化图,force_complete_scan=False表示不进行强制完全扫描
    cfg = proj.analyses.CFGFast(normalize=True, force_complete_scan=False)
     
    # 从完整的CFG中,根据函数地址(func_addr)获取特定函数的CFG(转换图)
    function_cfg = cfg.functions.get(func_addr).transition_graph
     
    # 使用 to_supergraph 将CFG图进行简化,合并节点
    super_cfg = to_supergraph(function_cfg)
     
    # 返回简化后的CFG
    return super_cfg
 
#
# 定义核心的去混淆函数
def deobfu_func(func_addr):
    # 创建一个空的集合(set),用于存放所有基本块的地址
    blocks = set()
     
    # 调用 get_cfg 获取目标函数的CFG
    cfg = get_cfg(func_addr)
     
    # 遍历CFG中的所有节点(基本块)
    for node in cfg.nodes:
        # 将每个基本块的地址添加到集合中
        blocks.add(node.addr)
         
    # (用于调试) 打印出此函数中所有基本块的地址(此时包含真实块和虚假块)
    print([hex(b) for b in blocks])
     
    # 注释:下面开始进行符号执行
    # Symbolic execution
     
    # 创建一个angr的“空白状态”,指定符号执行的起始地址为目标函数地址
    state = proj.factory.blank_state(addr=func_addr)
     
    # 创建一个“模拟管理器”(Simulation Manager),用于管理和驱动符号执行
    simgr = proj.factory.simgr(state)
     
    # 当模拟管理器中还有“活跃”的(即未探索完的)路径时,循环继续
    while len(simgr.active):
        # 遍历当前所有的活跃路径
        for active in simgr.active:
            # !!! 核心逻辑 !!!
            # active.addr 是当前符号执行“真实”到达的地址
            # .discard() 会从集合中移除这个地址。
            # 这样,所有真实能到达的块都会被移除
            blocks.discard(active.addr)
             
            # --- 下面是为 call 指令添加 Hook,以加速符号执行 ---
             
            # 获取当前活跃状态地址对应的基本块对象
            block = proj.factory.block(active.addr)
             
            # 遍历基本块中的每一条指令
            for insn in block.capstone.insns:
                # 如果指令是一个 call 指令
                if insn.mnemonic == 'call':
                    # 解析 call 指令的操作数(即目标地址),将其转为整数
                    next_func_addr = int(insn.op_str, 16)
                     
                    # 在这个 call 的地址上“挂钩”(hook)
                    # "ReturnUnconstrained" 是一个angr的“桩”(stub)
                    # 它告诉angr:不要深入分析这个call,假装它执行了并返回一个“任意值”,然后继续
                    proj.hook(next_func_addr, angr.SIM_PROCEDURES["stubs"]["ReturnUnconstrained"](), replace=True)
                     
                    # 打印日志,告知用户哪个 call 被 hook 了
                    print('Hook [%s\t%s] at %#x' % (insn.mnemonic, insn.op_str, insn.address))
                     
        # 让模拟管理器“走一步”,即探索所有活跃路径的下一个基本块
        simgr.step()
         
    # 当 while 循环结束(所有真实路径都探索完毕),`blocks` 集合中“剩下”的地址就是虚假块
    # 遍历所有这些“剩下”的(虚假的)基本块地址
    for block_addr in blocks:
        # 调用 patch_nops 函数,将这些虚假块在文件中 NOP 掉
        patch_nops(proj.factory.block(block_addr))
 
#
# 程序的标准入口点
if __name__ == '__main__':
    # 注释:关闭警告日志
    # Disable warning
     
    # 将cle(angr的加载器)的日志级别设为ERROR,屏蔽无关信息
    logging.getLogger('cle').setLevel(logging.ERROR)
     
    # 将angr主库的日志级别设为ERROR,屏蔽无关信息
    logging.getLogger('angr').setLevel(logging.ERROR)
     
    # 创建一个命令行参数解析器
    parser = argparse.ArgumentParser()
     
    # 添加 '-f' 或 '--file' 参数,必需(required=True),用于指定要分析的文件
    parser.add_argument('-f', '--file', required=True, help='File to deobfuscate')
     
    # 添加 '-s' 或 '--start' 参数,用于指定目标函数地址。
    # type=lambda x : int(x, 0) 允许输入十进制或十六进制(0x...)
    parser.add_argument('-s', '--start', type=lambda x : int(x, 0), help='Starting address of target function')
     
    # 解析命令行传入的参数
    args = parser.parse_args()
     
    # 注释:将二进制文件加载到angr
    # Load binary file ${file} into angr
     
    # 创建angr项目。args.file 是文件名。auto_load_libs=False 表示不自动加载libc等依赖库,加快分析
    proj = angr.Project(args.file, load_options={"auto_load_libs": False})
     
    # 从解析的参数中获取起始地址
    start = args.start
     
    # 如果用户没有提供 -s/--start 参数
    if start == None:
        # 尝试自动在二进制文件中查找 'main' 函数的符号
        main = proj.loader.find_symbol('main')
         
        # 如果 'main' 函数也找不到
        if main == None:
            # 报告错误并退出,提示用户必须提供 -s 参数
            parser.error('Can\'t find the main function, please provide argument -s/--start')
             
        # 如果找到了 main,使用 main 函数的重基址地址作为起始地址
        start = main.rebased_addr
         
    # 注释:将二进制文件读入内存
    # Load binary file ${file} into memory
     
    # 以二进制只读('rb')模式打开目标文件
    with open(args.file, 'rb') as file:
        # 读取文件的全部内容,并存入一个“可变的” bytearray 中,命名为 binfile
        # (必须是 bytearray 才能在内存中进行修补)
        binfile = bytearray(file.read())
         
    # 注释:对目标函数执行去混淆操作
    # Do deobfuscation on target function
     
    # 调用核心去混淆函数,传入目标函数地址
    deobfu_func(func_addr=start)
     
    # 注释:将被修复的二进制文件写入 ${file}_recovered
    # Write the recovered binary file to ${file}_recovered
     
    # 将原始文件名(如 "example/example-1")分割为文件名("example/example-1")和扩展名("")
    fname, ext = os.path.splitext(args.file)
     
    # 以二进制写入('wb')模式打开一个新文件(文件名后缀为 _recovered)
    with open(fname + '_recovered' + ext, 'wb') as file:
        # 将我们修补过的(已 NOP 掉虚假块)binfile 的内容写入新文件
        file.write(binfile)
         
    # 打印成功信息
    print('Deobfuscation success!')
#IDAPython
 
import ida_segment
import ida_bytes
import ida_auto
 
print("--- Starting BSS segment patch script ---")
 
# 1. 获取 .bss segment
seg = ida_segment.get_segm_by_name('.bss')
 
if not seg:
    print("Error: .bss segment not found.")
else:
    print(f"Found .bss at [0x{seg.start_ea:X} - 0x{seg.end_ea:X}]")
     
    # 2. 遍历并 Patch 字节 (使用 patch_dword 更符合语义)
    print("Patching all dwords in .bss to 2...")
    patched_count = 0
    for ea in range(seg.start_ea, seg.end_ea, 4):
        # 使用 patch_dword 而不是 patch_bytes 来更新 dword 值
        ida_bytes.patch_dword(ea, 2)
        patched_count += 1
     
    print(f"Patched {patched_count} dwords.")
 
    # 3. 设置段权限为只读 (R--)
    print("Setting segment permissions to Read-Only (R--)...")
    seg.perm = 0b100  # R-- (Read=4, Write=2, Exec=1)
     
    # 4. 【关键修复 A】: 将权限更改写回数据库!
    # 如果没有这一步, 你的 seg.perm 修改会丢失
    ida_segment.update_segm(seg)
    print("Segment permissions updated in the database.")
 
    # 5. 【关键修复 B】: 强制 IDA 重新分析 .bss 段的数据
    # 这会删除所有旧的 'dd 2'、'dd 0' 等注解
    # 并强制 IDA 重新读取你刚刚 patch 的字节
    print("Forcing re-analysis of all items in .bss...")
    for ea in range(seg.start_ea, seg.end_ea, 4):
        # (1) 删除此地址上任何旧的数据定义 (U键)
        ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, 4)
        # (2) 强制 IDA 重新将它定义为 dword (D键)
        ida_bytes.create_dword(ea, 4)
 
    print("--- Script finished successfully! ---")
    print(">>> ACTION REQUIRED: Go to your function's pseudocode and press F5 to refresh!")
    print(">>> If F5 is not enough, go to assembly, press 'U' on the function, then 'P', then F5.")
#IDAPython
 
import ida_segment
import ida_bytes
import ida_auto
 
print("--- Starting BSS segment patch script ---")
 
# 1. 获取 .bss segment
seg = ida_segment.get_segm_by_name('.bss')
 
if not seg:
    print("Error: .bss segment not found.")
else:
    print(f"Found .bss at [0x{seg.start_ea:X} - 0x{seg.end_ea:X}]")
     
    # 2. 遍历并 Patch 字节 (使用 patch_dword 更符合语义)
    print("Patching all dwords in .bss to 2...")
    patched_count = 0
    for ea in range(seg.start_ea, seg.end_ea, 4):
        # 使用 patch_dword 而不是 patch_bytes 来更新 dword 值
        ida_bytes.patch_dword(ea, 2)
        patched_count += 1
     
    print(f"Patched {patched_count} dwords.")
 
    # 3. 设置段权限为只读 (R--)
    print("Setting segment permissions to Read-Only (R--)...")
    seg.perm = 0b100  # R-- (Read=4, Write=2, Exec=1)
     
    # 4. 【关键修复 A】: 将权限更改写回数据库!
    # 如果没有这一步, 你的 seg.perm 修改会丢失
    ida_segment.update_segm(seg)
    print("Segment permissions updated in the database.")
 
    # 5. 【关键修复 B】: 强制 IDA 重新分析 .bss 段的数据
    # 这会删除所有旧的 'dd 2'、'dd 0' 等注解
    # 并强制 IDA 重新读取你刚刚 patch 的字节
    print("Forcing re-analysis of all items in .bss...")
    for ea in range(seg.start_ea, seg.end_ea, 4):
        # (1) 删除此地址上任何旧的数据定义 (U键)
        ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, 4)
        # (2) 强制 IDA 重新将它定义为 dword (D键)
        ida_bytes.create_dword(ea, 4)
 
    print("--- Script finished successfully! ---")
    print(">>> ACTION REQUIRED: Go to your function's pseudocode and press F5 to refresh!")
    print(">>> If F5 is not enough, go to assembly, press 'U' on the function, then 'P', then F5.")
# 导入 IDAPython 库
import ida_xref  # 用于处理交叉引用 (XREF)
import ida_idaapi  # 包含 IDA 的核心 API,例如 BADADDR (无效地址)
import ida_segment # 用于操作内存段 (Segment)
from ida_bytes import get_bytes, patch_bytes # 用于读取和修补 (Patch) 字节
 
# --- 定义 Patch 函数 ---
# 这个函数的目标是把 "mov 寄存器, [内存地址]" 替换为 "mov 寄存器, 0"
# ea: 传入的指令地址 (例如 "mov eax, [y_10]" 这条指令的地址)
def do_patch(ea):
    # 检查指令的第一个字节是否是 0x8B
    # 0x8B 是 x86 汇编中 "mov reg32, r/m32" (即 mov 寄存器, [内存]) 的操作码
    if get_bytes(ea, 1) == b"\x8B":
         
        # --- 解码 ModR/M 字节 ---
        # 0x8B 指令的第二个字节是 ModR/M 字节,它编码了目标寄存器和源操作数
        # 我们需要知道目标寄存器是 eax, ecx, dss ... 还是哪个
         
        # 1. 获取 ModR/M 字节 (ea + 1)
        # 2. ord(...) 将字节转换为整数
        # 3. (& 0b00111000) >> 3 是一个位操作技巧,
        #    用于从 ModR/M 字节中提取 3 位的寄存器编码 (000=eax, 001=ecx, ...)
        reg_code = (ord(get_bytes(ea + 1, 1)) & 0b00111000) >> 3
         
        # --- 准备新的指令字节 ---
        # 0xB8 是 "mov eax, 立即数32" 的操作码
        # 0xB9 是 "mov ecx, 立即数32" 的操作码
        # ... 以此类推,它们是连续的。
        # (0xB8 + reg_code) 就能巧妙地构造出正确的目标寄存器
        new_opcode = (0xB8 + reg_code).to_bytes(1, 'little')
         
        # 立即数 0 (dword, 4 字节, 小端)
        new_immediate = b'\x00\x00\x00\x00'
         
        # NOP (空操作) 指令,用于填充
        # "mov reg, [mem]" (如 mov eax, [0x407050]) 通常是 6 字节: 8B 05 50 70 40 00
        # "mov reg, 0" (如 mov eax, 0) 是 5 字节: B8 00 00 00 00
        # 我们需要一个 NOP (0x90) 来填满 6 字节,防止破坏后续指令
        # 注意: 原始脚本用了2个NOP (\x90\x90),如果原指令是7字节(带SIB)这可能是对的,
        # 但对于简单寻址(如此处),1个NOP(总共6字节)通常更正确。我们先用1个。
        padding = b'\x90'
         
        # 组合成新的 6 字节指令
        new_instruction_bytes = new_opcode + new_immediate + padding
         
        # --- 执行 Patch ---
        # 在原地址 ea 处,写入我们构造的新指令
        # "mov eax, [y_10]"  (8B 05 50...)  就被替换成了
        # "mov eax, 0; nop"  (B8 00 00 00 00 90)
        patch_bytes(ea, new_instruction_bytes)
         
    else:
        # 如果指令不是 0x8B,打印错误(可能分析出错了)
        print(f'Error: Instruction at 0x{ea:X} is not 0x8B, skipping.')
 
# --- 主逻辑开始 ---
 
print("--- Starting BCF Patch Script (Code Modification) ---")
 
# 1. 找到 .bss 段的范围 (假设不透明谓词变量都在这里)
seg = ida_segment.get_segm_by_name('.bss')
start = seg.start_ea  # .bss 起始地址
end = seg.end_ea    # .bss 结束地址
 
if not seg:
    print("Error: .bss segment not found. Exiting script.")
else:
    print(f"Scanning .bss segment [0x{start:X} - 0x{end:X}]...")
     
    # 2. 遍历 .bss 段中的每一个 dword (步长为 4)
    # addr 将依次代表 y_10, x_9 等变量的地址
    for addr in range(start, end, 4):
         
        # 3. 查找所有“引用”了这个地址(addr)的“代码”
        # ida_xref.get_first_dref_to(addr)
        # 获取第一个“数据交叉引用” (Data Reference) 到 addr 的地址
        # 简单说:找到代码中第一个 "mov ..., [addr]" 的地方
        ref = ida_xref.get_first_dref_to(addr)
         
        # 打印当前正在处理的 .bss 变量地址
        print(f"--- Processing refs to 0x{addr:X} ---")
 
        # 4. 循环处理所有找到的交叉引用
        # ida_idaapi.BADADDR 是一个常量,表示“无效地址”或“未找到”
        while(ref != ida_idaapi.BADADDR):
             
            # 5. 对找到的交叉引用地址 (ref),执行 patch
            print(f'   Found reference at 0x{ref:X}. Patching...')
            do_patch(ref)
             
            # 6. 查找下一个引用了 addr 的代码地址
            # 从上一个 ref 的位置继续向后搜索
            ref = ida_xref.get_next_dref_to(addr, ref)
             
        print(f"--- Finished processing 0x{addr:X} ---")
 
    print("--- Script finished successfully! ---")
    print(">>> ACTION REQUIRED: Go to your function's assembly view.")
    print(">>> Press 'U' (Undefine), then 'P' (Create Function).")
    print(">>> Finally, press F5 to see the deobfuscated pseudocode.")
# 导入 IDAPython 库
import ida_xref  # 用于处理交叉引用 (XREF)
import ida_idaapi  # 包含 IDA 的核心 API,例如 BADADDR (无效地址)
import ida_segment # 用于操作内存段 (Segment)
from ida_bytes import get_bytes, patch_bytes # 用于读取和修补 (Patch) 字节
 
# --- 定义 Patch 函数 ---
# 这个函数的目标是把 "mov 寄存器, [内存地址]" 替换为 "mov 寄存器, 0"
# ea: 传入的指令地址 (例如 "mov eax, [y_10]" 这条指令的地址)
def do_patch(ea):
    # 检查指令的第一个字节是否是 0x8B
    # 0x8B 是 x86 汇编中 "mov reg32, r/m32" (即 mov 寄存器, [内存]) 的操作码
    if get_bytes(ea, 1) == b"\x8B":
         
        # --- 解码 ModR/M 字节 ---
        # 0x8B 指令的第二个字节是 ModR/M 字节,它编码了目标寄存器和源操作数
        # 我们需要知道目标寄存器是 eax, ecx, dss ... 还是哪个
         
        # 1. 获取 ModR/M 字节 (ea + 1)
        # 2. ord(...) 将字节转换为整数
        # 3. (& 0b00111000) >> 3 是一个位操作技巧,
        #    用于从 ModR/M 字节中提取 3 位的寄存器编码 (000=eax, 001=ecx, ...)
        reg_code = (ord(get_bytes(ea + 1, 1)) & 0b00111000) >> 3
         
        # --- 准备新的指令字节 ---
        # 0xB8 是 "mov eax, 立即数32" 的操作码
        # 0xB9 是 "mov ecx, 立即数32" 的操作码
        # ... 以此类推,它们是连续的。
        # (0xB8 + reg_code) 就能巧妙地构造出正确的目标寄存器
        new_opcode = (0xB8 + reg_code).to_bytes(1, 'little')
         
        # 立即数 0 (dword, 4 字节, 小端)
        new_immediate = b'\x00\x00\x00\x00'
         
        # NOP (空操作) 指令,用于填充
        # "mov reg, [mem]" (如 mov eax, [0x407050]) 通常是 6 字节: 8B 05 50 70 40 00
        # "mov reg, 0" (如 mov eax, 0) 是 5 字节: B8 00 00 00 00
        # 我们需要一个 NOP (0x90) 来填满 6 字节,防止破坏后续指令
        # 注意: 原始脚本用了2个NOP (\x90\x90),如果原指令是7字节(带SIB)这可能是对的,
        # 但对于简单寻址(如此处),1个NOP(总共6字节)通常更正确。我们先用1个。
        padding = b'\x90'
         
        # 组合成新的 6 字节指令
        new_instruction_bytes = new_opcode + new_immediate + padding
         
        # --- 执行 Patch ---
        # 在原地址 ea 处,写入我们构造的新指令
        # "mov eax, [y_10]"  (8B 05 50...)  就被替换成了
        # "mov eax, 0; nop"  (B8 00 00 00 00 90)
        patch_bytes(ea, new_instruction_bytes)
         
    else:
        # 如果指令不是 0x8B,打印错误(可能分析出错了)
        print(f'Error: Instruction at 0x{ea:X} is not 0x8B, skipping.')
 
# --- 主逻辑开始 ---
 
print("--- Starting BCF Patch Script (Code Modification) ---")
 
# 1. 找到 .bss 段的范围 (假设不透明谓词变量都在这里)
seg = ida_segment.get_segm_by_name('.bss')
start = seg.start_ea  # .bss 起始地址
end = seg.end_ea    # .bss 结束地址
 
if not seg:
    print("Error: .bss segment not found. Exiting script.")
else:
    print(f"Scanning .bss segment [0x{start:X} - 0x{end:X}]...")
     
    # 2. 遍历 .bss 段中的每一个 dword (步长为 4)
    # addr 将依次代表 y_10, x_9 等变量的地址
    for addr in range(start, end, 4):
         
        # 3. 查找所有“引用”了这个地址(addr)的“代码”
        # ida_xref.get_first_dref_to(addr)
        # 获取第一个“数据交叉引用” (Data Reference) 到 addr 的地址
        # 简单说:找到代码中第一个 "mov ..., [addr]" 的地方
        ref = ida_xref.get_first_dref_to(addr)
         
        # 打印当前正在处理的 .bss 变量地址
        print(f"--- Processing refs to 0x{addr:X} ---")
 
        # 4. 循环处理所有找到的交叉引用
        # ida_idaapi.BADADDR 是一个常量,表示“无效地址”或“未找到”
        while(ref != ida_idaapi.BADADDR):
             
            # 5. 对找到的交叉引用地址 (ref),执行 patch
            print(f'   Found reference at 0x{ref:X}. Patching...')
            do_patch(ref)
             
            # 6. 查找下一个引用了 addr 的代码地址
            # 从上一个 ref 的位置继续向后搜索
            ref = ida_xref.get_next_dref_to(addr, ref)
             
        print(f"--- Finished processing 0x{addr:X} ---")
 
    print("--- Script finished successfully! ---")
    print(">>> ACTION REQUIRED: Go to your function's assembly view.")
    print(">>> Press 'U' (Undefine), then 'P' (Create Function).")
    print(">>> Finally, press F5 to see the deobfuscated pseudocode.")
#python版本 3.8.7
 
#ida pro 7.5
#angr版本
angr                      9.2.102
#python版本 3.8.7
 
#ida pro 7.5
#angr版本
angr                      9.2.102
import idaapi
import idautils
import idc
import keystone
 
#初始化Ks
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
 
 
# 打印基本块的所有指令
def print_block_insts(block):
    ea = block.start_ea
    while ea < block.end_ea:
        print(hex(ea), ":", idc.GetDisasm(ea))
        ea = idc.next_head(ea)
 
 
 
#TODO 返回前驱块的索引
 
    # 前驱块指令:
    # 0x43290 : MOV             W9, #0xEC74B33D
    # 0x43298 : CMP             W8, W9
    # 0x4329c : B.EQ            loc_43458
    #      提取状态 ID: 0xec74b33d
 
    # 前驱块指令:
    # 0x43250 : CMP             W8, W26                                   特殊,是寄存器W26不是立即数,在序言块中,所以需要手动计算出来
    # 0x43254 : B.EQ            loc_43474
 
    # 前驱块指令:
    # 0x431a8 : MOV             W9, #0x3455F111
    # 0x431b0 : CMP             W8, W9
    # 0x431b4 : B.EQ            loc_43490
    #      提取状态 ID: 0x3455f111
def getPredBlockIndex(block):
    # 获取所有前驱基本块
    preds_list = list(block.preds())
    preds_len = len(preds_list)
    if preds_len > 1:
        # TODO  原文章作者认为理论上,OLLVM 的真实块只有一个前驱(即主分发器派生出的预处理块)  这里如果有两个前驱块怎么处理呢?
        print(hex(block.start_ea), " 前驱数量-->", preds_len)
        return None
    elif preds_len == 0:
        print(hex(block.start_ea)," 没有前驱块")
        return None
    # 获取这唯一的前驱块
    pred = preds_list[0]
 
    # 打印前区块所有指令
    print("前驱块指令:")
    print_block_insts(pred)  # 打印前区块指令
 
 
    end_addr = pred.end_ea
    # 前驱块的最后一条指令
    last_ins_ea = idc.prev_head(end_addr)
    mnem = idc.print_insn_mnem(last_ins_ea)
    # print(mnem,"-->",hex(pred.start_ea))
 
 
    # 查找 OLLVM 主分发器中的比较跳转模式
    #
    #   MOV  W9, #STATE_ID_FOR_BLOCK
    #   CMP  W8, W9
    #   B.EQ <block.start_ea>
    #
    if mnem == 'B.EQ' or mnem == "B.NE":
        # B.EQ/B.NE 上方的 CMP
        CMP_ea = idc.prev_head(last_ins_ea)
        mnem = idc.print_insn_mnem(CMP_ea)
        # 获取 CMP 的第二个操作数
        CMP_1 = idc.print_operand(CMP_ea,1)
 
        if mnem == 'CMP':
            # CMP 上方的 MOV
            MOV_ea = idc.prev_head(CMP_ea)
            mnem = idc.print_insn_mnem(MOV_ea)
            if mnem == 'MOV':
                # 成功匹配模式,提取该基本块的前驱存储的索引:  例如:0x431c8 : MOV             W9, #0x39649A15  提取 0x39649A15
                index = idc.get_operand_value(MOV_ea, 1)
                # print("     提取该基本块的前驱存储的索引:", hex(index))
                return hex(index)
 
            # TODO 硬编码的后备方案,如果 MOV 不在 CMP 正上方
            else:
                # todo  看序言块
                # 通过规律可以发现 CMP W8, xxx   xxx认定为是前驱块的索引,可以为立即数/寄存器,此处下面两个案例为W24、W26,发现其在序言块中,手动计算出来即可
                # 比如:
                # 前驱块指令:
                # 0x43288: CMP W8, W24
                # 0x4328c: B.EQ loc_4340C
 
                # 或者
                # 前驱块指令:
                # 0x43250: CMP W8, W26
                # 0x43254: B.EQ loc_43474
                if CMP_1 == "W26":
                    return hex(0xA9D4543B)
                elif CMP_1 == "W24":
                    return hex(0xE4DBC33F)
    return None
 
# 获取汇聚到主分发器的所有块
def findLoopEntryBlockAllPreds(loop_end_ea):
    block = getBlockByAddress(loop_end_ea)
    for pred in block.preds():
        ea = idc.prev_head(pred.end_ea)
        print("主分发器前驱基本块:", hex(ea), idc.GetDisasm(ea))
 
def getBlockLink(func_ea,loop_end_ea):
    state_map = {}  #用于记录真实块
    func = idaapi.get_func(func_ea)
    blocks = idaapi.FlowChart(func) #获取方法中所有的基本块
 
    # findLoopEntryBlockAllPreds(loop_end_ea)#获取主分发器的所有前驱块
 
    for block in blocks:
        block_start_ea = block.start_ea  #基本块起始地址
        block_end_ea = block.end_ea #基本块结束地址
        next_states = []    #记录后继真实块的索引
 
        if block_start_ea == 0x43058: #添加序言块
            # TODO 因为序言块后面直接跟后续块,所以此直接给后继索引,通过   MOVK            W8, #0x6657,LSL#16   计算出W8的值
            next_states.append(hex(0x665797A5)) #true分支
            next_states.append(None)            #false分支
            state_map[hex(block_start_ea)] = next_states
            continue
 
        last_ins_addr = idc.prev_head(block_end_ea)#获取最后一条指令地址
        mnem = idc.print_insn_mnem(last_ins_addr)  #获取指令的操作符
        op_0 = idc.get_operand_value(last_ins_addr, 0) #获取指令的操作数的第0位
 
 
        # todo 以下是遍历特征向state_map里添加所有后继块和前驱块——判断标准:基本块最后一个指令是 【B               loc_43120】
        if  mnem == "B" and op_0 == loop_end_ea:#loop_end_ea为主分发器地址
            ins = idc.prev_head(last_ins_addr)#取每个真实块B指令的上一条指令,
 
            # 根据MOV和MOVK的不同特征去匹配后继索引。
            mnem = idc.print_insn_mnem(ins)
 
            # 示例
            # loc_433C8
            #     BL              sub_62CEC
            #     MOV             W8, #0xBEE4A4C9
            #     B               loc_43120   #跳转到主分发器的
            if mnem == "MOV":
                # todo 因为没有CSEL所以只有一个后继索引,即一个后续分支
                mov_1 = idc.get_operand_value(ins,1)
                next_states.append(hex(mov_1))
 
                # todo 寻找该基本块的前驱块索引并添加到next_states
                pred_index = getPredBlockIndex(block)
                next_states.append(pred_index)
 
            # 示例
            # loc_433F8
            # BL              sub_1C4F4
            # MOV             W8, #0xF5EA
            # STR             W0, [X19,#0x3C]
            # MOVK            W8, #0x89EF,LSL#16
            # B               loc_43120
            if mnem == "MOVK":
                #MOVK            W9, #0x4E30,LSL#16
                MOVK_0 = idc.print_operand(ins, 0)
                MOVK_1 = idc.get_operand_value(ins,1)
                mov_0 = ""
                mov_1 = 0
 
                # todo 获取指定地址段的汇编指令,在该指令块下从前往后找 mov指令,获取W8的值,目的是为了后续的MOVK的计算
                for ea in idautils.Heads(block_start_ea, block_end_ea):#获取指定地址段的汇编指令
                    mnem = idc.print_insn_mnem(ea)
                    if mnem == "MOV":
                        # 此处拿到  MOV             W8, #0xF5EA  中的  W8
                        mov_0 = idc.print_operand(ea,0)
                        if MOVK_0 == mov_0:
                            # 此处拿到  MOV             W8, #0xF5EA  中的  0xF5EA
                            mov_1 = idc.get_operand_value(ea,1)
                            break
 
                if MOVK_0 == mov_0:
                    #MOV W8,  # 0xF5EA
                    #MOVK            W8, #0x89EF,LSL#16
                    #结果为 w8 = 0x89EFF5EA
                    if  idc.GetDisasm(ins).find("LSL#16") != -1:
                       index = (MOVK_1 << 16) | mov_1
                       # todo 因为没有CSEL所以只有一个后继索引,即一个后续分支
                       next_states.append(hex(index))
 
                       # todo 寻找该基本块的前驱块索引并添加到next_states
                       pred_index = getPredBlockIndex(block)
                       next_states.append(pred_index)
 
                    else:
                        print("未匹配算术移位:",hex(block_start_ea))
 
            # todo if-else分支,此处原文作者是通过人工寻找CSEL指令,然后添加后续分支索引(true/false两个分支)
            if mnem == "CSEL":
                print("CSEL:",hex(block_start_ea))
                if block_start_ea == 0x43168:
                    next_states.append(hex(0x4E30550D)) #if(true) 后继块索引
                    next_states.append(hex(0xBEE4A4C9))#else(false) 后继块索引
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x431d8:
                    next_states.append(hex(0xA9D4543B))
                    next_states.append(hex(0xC7AC1F5F))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x433d8:
                    next_states.append(hex(0xF5C370CA))
                    next_states.append(hex(0x667521E4))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x43420:
                    next_states.append(hex(0xE4DBC33F))
                    next_states.append(hex(0x667521E4))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x434ac:
                    next_states.append(hex(0xBD9FBBA))
                    next_states.append(hex(0x5338AB80))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x434cc:
                    next_states.append(hex(0x146E0C87))
                    next_states.append(hex(0x1B166FED))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
        # todo 以下是遍历特征向state_map里添加所有后继块和前驱块——基本块最后一个指令是 【MOV             W8, #0xD23AD535】
        # loc_430E0
        # LDR             X8, [X19,#0x10]
        # MOV             W2, #0x7D0 ; n
        # MOV             W1, WZR ; c
        # STR             X8, [X19,#0x40]
        # LDR             X8, [X19,#0x40]
        # LDR             X0, [X19,#0x40] ; s
        # BL              memset
        # LDR             X8, [X19,#0x10]
        # ADRL            X1, (aCouldNotMapSeg+0x1A) ; format
        # STR             X8, [X19,#0x48]
        # LDR             X0, [X19,#0x30] ; stream
        # LDR             X2, [X19,#0x48]
        # BL              fscanf
        # MOV             W8, #0xD23AD535
        if  mnem == "MOV" and block_end_ea == loop_end_ea:
            mov_1 = idc.get_operand_value(last_ins_addr,1)
            next_states.append(hex(mov_1))
 
            pred_index = getPredBlockIndex(block)
            next_states.append(pred_index)
 
        # todo 目的是从ret块开始一直往前找前驱块,找到的标志为:MOV CMP B.NE 指令按顺序排序为止:
        # ret块
        # SUB             SP, X29, #0x50 ; 'P'
        # LDP             X29, X30, [SP,#0x50+var_s0]
        # LDP             X20, X19, [SP,#0x50+var_10]
        # LDP             X22, X21, [SP,#0x50+var_20]
        # LDP             X24, X23, [SP,#0x50+var_30]
        # LDP             X26, X25, [SP,#0x50+var_40]
        # LDP             X28, X27, [SP+0x50+var_50],#0x60
        if mnem == "RET": #添加ret块
            while (1):
                # MOV             W9, #0x146E0C87
                # CMP             W8, W9
                # B.NE            loc_43120
 
                preds = block.preds()
                preds_list = list(preds)
                block = preds_list[0]
                pred_ea = block.start_ea
                mnem = idc.print_insn_mnem(pred_ea)
                if mnem == "MOV":
                    MOV_ea = pred_ea
                    pred_ea = idc.next_head(pred_ea)
                    mnem = idc.print_insn_mnem(pred_ea)
                    if mnem == "CMP":
                        pred_ea = idc.next_head(pred_ea)
                        mnem = idc.print_insn_mnem(pred_ea)
                        if mnem == "B.NE":
                            mov_1 = idc.get_operand_value(MOV_ea, 1)
                            next_states.append(None)
                            next_states.append(hex(mov_1))
                            break
 
        if next_states:
            state_map[hex(block_start_ea)] = next_states
 
    print(state_map)
    return state_map
 
def getSuccBlockAddrFromMap(state_map,index):
    for key in state_map:
        block_ea = int(key,16)
        targets = state_map[key]
 
        if len(targets) == 2:
            pred = targets[1]
            if index == pred:#如果A真实块要跳转的索引和B真实块的前驱模块所具备的索引相等,那么直接返回B真实块地址
                return hex(block_ea)
        if len(targets) == 3:
            pred = targets[2]
            if index == pred:
                return hex(block_ea)
    return None
 
def verifyBlockLink(state_map,fun_start,ret_block_ea,next_states):
    value = state_map[fun_start]
    next_states.append(fun_start)
 
    if len(value) == 3:
        #进入这里 fun_start即是支配节点
        for i in range(2):
            tmp = next_states.copy()    #获取到支配节点数组
            index = value[i]
            addr = getSuccBlockAddrFromMap(state_map, index)
            # print("支配节点:", fun_start,"-->",addr)
            if addr == None:    #如果获取的地址为空,需要对应补上需要的后继块
                print("array3 无法找到后继块:",tmp,index)
                return None
            if addr == ret_block_ea:
                tmp.append(addr)
                print(tmp)
            else:
                verifyBlockLink(state_map,addr,ret_block_ea,tmp)
 
    elif len(value) == 2:
        index = value[0]
        addr = getSuccBlockAddrFromMap(state_map, index)
 
        if addr == None:
            print("array2 无法找到后继块:", next_states, hex(index))
            return None
        if addr == ret_block_ea:
            next_states.append(addr)
            print(next_states)
        else:
            verifyBlockLink(state_map, addr, ret_block_ea, next_states)
 
def findRETBlock(func_ea):
    func = idaapi.get_func(func_ea)
    blocks = idaapi.FlowChart(func)  # 获取方法中所有的基本块
    for block in blocks:
        block_end_ea = block.end_ea
        last_ins_ea = idc.prev_head(block_end_ea)
        mnem = idc.print_insn_mnem(last_ins_ea)
        if mnem == "RET":
            return block
 
def verifyLinkMain(state_map,fun_start):
    next_states = []
    ret_block = findRETBlock(fun_start)
    ret_block_ea = ret_block.start_ea#获取ret块地址
    verifyBlockLink(state_map, hex(fun_start), hex(ret_block_ea), next_states)#开始执行验证程序
 
 
 
def getBlockByAddress(ea):
    # 获取地址所在的函数
    func = idaapi.get_func(ea)
    if not func:
        print(f"地址 {hex(ea)} 不在任何函数中")
        return None
    # 创建控制流图
    blocks = idaapi.FlowChart(func)
 
    # 遍历所有块
    for block in blocks:
        # 检查地址是否在当前块中
        if block.start_ea <= ea < block.end_ea:
            # print(f"地址 {hex(ea)} 在块 {hex(block.start_ea)} - {hex(block.end_ea)} 中")
            return block
 
    print(f"地址 {hex(ea)} 未找到对应的块")
    return None
 
def patchBranch(src_addr, dest_addr,op_value = 0):
    # print("src_addr:",hex(src_addr),"dest_addr:",dest_addr)
 
    # CSEL W8, W9, W8, EQ
    CSEL_ea = idc.prev_head(src_addr)
    CSEL_3 = idc.print_operand(CSEL_ea,3)
    if op_value == 1:
        if CSEL_3 == "EQ":
                encoding, count = ks.asm(f'b.eq {dest_addr}', CSEL_ea)
        if CSEL_3 == "NE":
                encoding, count = ks.asm(f'b.ne {dest_addr}', CSEL_ea)
        if CSEL_3 == "GT":
                encoding, count = ks.asm(f'b.gt {dest_addr}', CSEL_ea)
        src_addr = CSEL_ea
    else:
        encoding, count = ks.asm(f'b {dest_addr}', src_addr)
 
    if not count:
        print('ks.asm err')
    else:
        for i in range(4):
            idc.patch_byte(src_addr + i, encoding[i])
            # print("patch success:",hex(src_addr),dest_addr)
 
def rebuildControlFlow(state_map):
    for block in state_map:
        block_ea = int(block,16)#需要把字符串转成int
        # 获取真实块保存的前驱、后继链接块索引
        value = state_map[block]
        # 查找块尾的跳转指令
        endEa = getBlockByAddress(block_ea).end_ea
 
        last_insn_ea = idc.prev_head(endEa)
        if idc.print_insn_mnem(last_insn_ea) == "B":
            # 如果是无条件跳转(B)
            if len(value) == 2:
                succ_index = value[0] #当前真实块的后继块索引
                if succ_index == None: #return块没有后继,过滤掉它
                    continue
                jmp_addr = getSuccBlockAddrFromMap(state_map,succ_index) #获取后继索引对应的真实块地址
                patchBranch(last_insn_ea, jmp_addr)
 
            # 如果是条件跳转(CSEL)
            elif len(value) == 3:
                succ_0 = value[0] #后继块的索引值
                jmp_addr_0 = getSuccBlockAddrFromMap(state_map, succ_0) #后继块的地址
                patchBranch(last_insn_ea, jmp_addr_0,1)
 
                succ_1 = value[1]   #后继块的索引值
                jmp_addr_1 = getSuccBlockAddrFromMap(state_map, succ_1)
                patchBranch(last_insn_ea, jmp_addr_1)
        if idc.print_insn_mnem(last_insn_ea) == "MOV":
            succ_index = value[0# 当前真实块的后继块索引
            # if succ_index == None:  # return块没有后继,过滤掉它
            #     continue
            jmp_addr = getSuccBlockAddrFromMap(state_map, succ_index)  # 获取后继索引对应的真实块地址
            patchBranch(last_insn_ea, jmp_addr)
 
# 获取主分发器
def findDispatchers(func_start,num = 10):
    func = idaapi.get_func(func_start)
    blocks = idaapi.FlowChart(func)
    pachers = []
    for block in blocks:
        preds = block.preds()
        preds_list = list(preds)
        if len(preds_list) > num:
            pachers.append(block)
    return pachers
 
def deObfuscatorFla():
    print("===============START===================")
    fn = 0x43058 #函数的起始地址
 
    patchers = findDispatchers(fn) #获取方法中所有的主分发器块 默认块被引用10次的为主分发器
    print("patchers:",len(patchers))
    if len(patchers) == 0:
        print("未找到主分发器")
        return
    # 这里只对一个主分发器操作,多个主分发器需要额外处理
    for disPatcherBlock in patchers:
        print("主分发器地址:", hex(disPatcherBlock.start_ea))
        stamp = getBlockLink(fn, disPatcherBlock.start_ea)  # 记录真实块链接关系
        verifyLinkMain(stamp,fn)#验证块连接关系是否正确
        rebuildControlFlow(stamp)
    print("===============END===================")
 
deObfuscatorFla()
import idaapi
import idautils
import idc
import keystone
 
#初始化Ks
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
 
 
# 打印基本块的所有指令
def print_block_insts(block):
    ea = block.start_ea
    while ea < block.end_ea:
        print(hex(ea), ":", idc.GetDisasm(ea))
        ea = idc.next_head(ea)
 
 
 
#TODO 返回前驱块的索引
 
    # 前驱块指令:
    # 0x43290 : MOV             W9, #0xEC74B33D
    # 0x43298 : CMP             W8, W9
    # 0x4329c : B.EQ            loc_43458
    #      提取状态 ID: 0xec74b33d
 
    # 前驱块指令:
    # 0x43250 : CMP             W8, W26                                   特殊,是寄存器W26不是立即数,在序言块中,所以需要手动计算出来
    # 0x43254 : B.EQ            loc_43474
 
    # 前驱块指令:
    # 0x431a8 : MOV             W9, #0x3455F111
    # 0x431b0 : CMP             W8, W9
    # 0x431b4 : B.EQ            loc_43490
    #      提取状态 ID: 0x3455f111
def getPredBlockIndex(block):
    # 获取所有前驱基本块
    preds_list = list(block.preds())
    preds_len = len(preds_list)
    if preds_len > 1:
        # TODO  原文章作者认为理论上,OLLVM 的真实块只有一个前驱(即主分发器派生出的预处理块)  这里如果有两个前驱块怎么处理呢?
        print(hex(block.start_ea), " 前驱数量-->", preds_len)
        return None
    elif preds_len == 0:
        print(hex(block.start_ea)," 没有前驱块")
        return None
    # 获取这唯一的前驱块
    pred = preds_list[0]
 
    # 打印前区块所有指令
    print("前驱块指令:")
    print_block_insts(pred)  # 打印前区块指令
 
 
    end_addr = pred.end_ea
    # 前驱块的最后一条指令
    last_ins_ea = idc.prev_head(end_addr)
    mnem = idc.print_insn_mnem(last_ins_ea)
    # print(mnem,"-->",hex(pred.start_ea))
 
 
    # 查找 OLLVM 主分发器中的比较跳转模式
    #
    #   MOV  W9, #STATE_ID_FOR_BLOCK
    #   CMP  W8, W9
    #   B.EQ <block.start_ea>
    #
    if mnem == 'B.EQ' or mnem == "B.NE":
        # B.EQ/B.NE 上方的 CMP
        CMP_ea = idc.prev_head(last_ins_ea)
        mnem = idc.print_insn_mnem(CMP_ea)
        # 获取 CMP 的第二个操作数
        CMP_1 = idc.print_operand(CMP_ea,1)
 
        if mnem == 'CMP':
            # CMP 上方的 MOV
            MOV_ea = idc.prev_head(CMP_ea)
            mnem = idc.print_insn_mnem(MOV_ea)
            if mnem == 'MOV':
                # 成功匹配模式,提取该基本块的前驱存储的索引:  例如:0x431c8 : MOV             W9, #0x39649A15  提取 0x39649A15
                index = idc.get_operand_value(MOV_ea, 1)
                # print("     提取该基本块的前驱存储的索引:", hex(index))
                return hex(index)
 
            # TODO 硬编码的后备方案,如果 MOV 不在 CMP 正上方
            else:
                # todo  看序言块
                # 通过规律可以发现 CMP W8, xxx   xxx认定为是前驱块的索引,可以为立即数/寄存器,此处下面两个案例为W24、W26,发现其在序言块中,手动计算出来即可
                # 比如:
                # 前驱块指令:
                # 0x43288: CMP W8, W24
                # 0x4328c: B.EQ loc_4340C
 
                # 或者
                # 前驱块指令:
                # 0x43250: CMP W8, W26
                # 0x43254: B.EQ loc_43474
                if CMP_1 == "W26":
                    return hex(0xA9D4543B)
                elif CMP_1 == "W24":
                    return hex(0xE4DBC33F)
    return None
 
# 获取汇聚到主分发器的所有块
def findLoopEntryBlockAllPreds(loop_end_ea):
    block = getBlockByAddress(loop_end_ea)
    for pred in block.preds():
        ea = idc.prev_head(pred.end_ea)
        print("主分发器前驱基本块:", hex(ea), idc.GetDisasm(ea))
 
def getBlockLink(func_ea,loop_end_ea):
    state_map = {}  #用于记录真实块
    func = idaapi.get_func(func_ea)
    blocks = idaapi.FlowChart(func) #获取方法中所有的基本块
 
    # findLoopEntryBlockAllPreds(loop_end_ea)#获取主分发器的所有前驱块
 
    for block in blocks:
        block_start_ea = block.start_ea  #基本块起始地址
        block_end_ea = block.end_ea #基本块结束地址
        next_states = []    #记录后继真实块的索引
 
        if block_start_ea == 0x43058: #添加序言块
            # TODO 因为序言块后面直接跟后续块,所以此直接给后继索引,通过   MOVK            W8, #0x6657,LSL#16   计算出W8的值
            next_states.append(hex(0x665797A5)) #true分支
            next_states.append(None)            #false分支
            state_map[hex(block_start_ea)] = next_states
            continue
 
        last_ins_addr = idc.prev_head(block_end_ea)#获取最后一条指令地址
        mnem = idc.print_insn_mnem(last_ins_addr)  #获取指令的操作符
        op_0 = idc.get_operand_value(last_ins_addr, 0) #获取指令的操作数的第0位
 
 
        # todo 以下是遍历特征向state_map里添加所有后继块和前驱块——判断标准:基本块最后一个指令是 【B               loc_43120】
        if  mnem == "B" and op_0 == loop_end_ea:#loop_end_ea为主分发器地址
            ins = idc.prev_head(last_ins_addr)#取每个真实块B指令的上一条指令,
 
            # 根据MOV和MOVK的不同特征去匹配后继索引。
            mnem = idc.print_insn_mnem(ins)
 
            # 示例
            # loc_433C8
            #     BL              sub_62CEC
            #     MOV             W8, #0xBEE4A4C9
            #     B               loc_43120   #跳转到主分发器的
            if mnem == "MOV":
                # todo 因为没有CSEL所以只有一个后继索引,即一个后续分支
                mov_1 = idc.get_operand_value(ins,1)
                next_states.append(hex(mov_1))
 
                # todo 寻找该基本块的前驱块索引并添加到next_states
                pred_index = getPredBlockIndex(block)
                next_states.append(pred_index)
 
            # 示例
            # loc_433F8
            # BL              sub_1C4F4
            # MOV             W8, #0xF5EA
            # STR             W0, [X19,#0x3C]
            # MOVK            W8, #0x89EF,LSL#16
            # B               loc_43120
            if mnem == "MOVK":
                #MOVK            W9, #0x4E30,LSL#16
                MOVK_0 = idc.print_operand(ins, 0)
                MOVK_1 = idc.get_operand_value(ins,1)
                mov_0 = ""
                mov_1 = 0
 
                # todo 获取指定地址段的汇编指令,在该指令块下从前往后找 mov指令,获取W8的值,目的是为了后续的MOVK的计算
                for ea in idautils.Heads(block_start_ea, block_end_ea):#获取指定地址段的汇编指令
                    mnem = idc.print_insn_mnem(ea)
                    if mnem == "MOV":
                        # 此处拿到  MOV             W8, #0xF5EA  中的  W8
                        mov_0 = idc.print_operand(ea,0)
                        if MOVK_0 == mov_0:
                            # 此处拿到  MOV             W8, #0xF5EA  中的  0xF5EA
                            mov_1 = idc.get_operand_value(ea,1)
                            break
 
                if MOVK_0 == mov_0:
                    #MOV W8,  # 0xF5EA
                    #MOVK            W8, #0x89EF,LSL#16
                    #结果为 w8 = 0x89EFF5EA
                    if  idc.GetDisasm(ins).find("LSL#16") != -1:
                       index = (MOVK_1 << 16) | mov_1
                       # todo 因为没有CSEL所以只有一个后继索引,即一个后续分支
                       next_states.append(hex(index))
 
                       # todo 寻找该基本块的前驱块索引并添加到next_states
                       pred_index = getPredBlockIndex(block)
                       next_states.append(pred_index)
 
                    else:
                        print("未匹配算术移位:",hex(block_start_ea))
 
            # todo if-else分支,此处原文作者是通过人工寻找CSEL指令,然后添加后续分支索引(true/false两个分支)
            if mnem == "CSEL":
                print("CSEL:",hex(block_start_ea))
                if block_start_ea == 0x43168:
                    next_states.append(hex(0x4E30550D)) #if(true) 后继块索引
                    next_states.append(hex(0xBEE4A4C9))#else(false) 后继块索引
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x431d8:
                    next_states.append(hex(0xA9D4543B))
                    next_states.append(hex(0xC7AC1F5F))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x433d8:
                    next_states.append(hex(0xF5C370CA))
                    next_states.append(hex(0x667521E4))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x43420:
                    next_states.append(hex(0xE4DBC33F))
                    next_states.append(hex(0x667521E4))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x434ac:
                    next_states.append(hex(0xBD9FBBA))
                    next_states.append(hex(0x5338AB80))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
                elif block_start_ea == 0x434cc:
                    next_states.append(hex(0x146E0C87))
                    next_states.append(hex(0x1B166FED))
 
                    # todo 寻找该基本块的前驱块索引并添加到next_states
                    pred_index = getPredBlockIndex(block)
                    next_states.append(pred_index)
 
        # todo 以下是遍历特征向state_map里添加所有后继块和前驱块——基本块最后一个指令是 【MOV             W8, #0xD23AD535】
        # loc_430E0
        # LDR             X8, [X19,#0x10]
        # MOV             W2, #0x7D0 ; n
        # MOV             W1, WZR ; c
        # STR             X8, [X19,#0x40]
        # LDR             X8, [X19,#0x40]
        # LDR             X0, [X19,#0x40] ; s
        # BL              memset
        # LDR             X8, [X19,#0x10]
        # ADRL            X1, (aCouldNotMapSeg+0x1A) ; format
        # STR             X8, [X19,#0x48]
        # LDR             X0, [X19,#0x30] ; stream
        # LDR             X2, [X19,#0x48]
        # BL              fscanf
        # MOV             W8, #0xD23AD535
        if  mnem == "MOV" and block_end_ea == loop_end_ea:
            mov_1 = idc.get_operand_value(last_ins_addr,1)
            next_states.append(hex(mov_1))
 
            pred_index = getPredBlockIndex(block)
            next_states.append(pred_index)
 
        # todo 目的是从ret块开始一直往前找前驱块,找到的标志为:MOV CMP B.NE 指令按顺序排序为止:
        # ret块
        # SUB             SP, X29, #0x50 ; 'P'
        # LDP             X29, X30, [SP,#0x50+var_s0]
        # LDP             X20, X19, [SP,#0x50+var_10]
        # LDP             X22, X21, [SP,#0x50+var_20]
        # LDP             X24, X23, [SP,#0x50+var_30]
        # LDP             X26, X25, [SP,#0x50+var_40]
        # LDP             X28, X27, [SP+0x50+var_50],#0x60
        if mnem == "RET": #添加ret块
            while (1):
                # MOV             W9, #0x146E0C87
                # CMP             W8, W9
                # B.NE            loc_43120
 
                preds = block.preds()
                preds_list = list(preds)
                block = preds_list[0]
                pred_ea = block.start_ea
                mnem = idc.print_insn_mnem(pred_ea)
                if mnem == "MOV":
                    MOV_ea = pred_ea
                    pred_ea = idc.next_head(pred_ea)
                    mnem = idc.print_insn_mnem(pred_ea)
                    if mnem == "CMP":
                        pred_ea = idc.next_head(pred_ea)
                        mnem = idc.print_insn_mnem(pred_ea)
                        if mnem == "B.NE":
                            mov_1 = idc.get_operand_value(MOV_ea, 1)
                            next_states.append(None)
                            next_states.append(hex(mov_1))
                            break
 
        if next_states:
            state_map[hex(block_start_ea)] = next_states
 
    print(state_map)
    return state_map
 
def getSuccBlockAddrFromMap(state_map,index):
    for key in state_map:
        block_ea = int(key,16)
        targets = state_map[key]
 
        if len(targets) == 2:
            pred = targets[1]
            if index == pred:#如果A真实块要跳转的索引和B真实块的前驱模块所具备的索引相等,那么直接返回B真实块地址
                return hex(block_ea)
        if len(targets) == 3:
            pred = targets[2]
            if index == pred:
                return hex(block_ea)
    return None
 
def verifyBlockLink(state_map,fun_start,ret_block_ea,next_states):
    value = state_map[fun_start]
    next_states.append(fun_start)
 
    if len(value) == 3:
        #进入这里 fun_start即是支配节点
        for i in range(2):
            tmp = next_states.copy()    #获取到支配节点数组
            index = value[i]
            addr = getSuccBlockAddrFromMap(state_map, index)
            # print("支配节点:", fun_start,"-->",addr)
            if addr == None:    #如果获取的地址为空,需要对应补上需要的后继块
                print("array3 无法找到后继块:",tmp,index)
                return None
            if addr == ret_block_ea:
                tmp.append(addr)
                print(tmp)
            else:
                verifyBlockLink(state_map,addr,ret_block_ea,tmp)
 
    elif len(value) == 2:
        index = value[0]
        addr = getSuccBlockAddrFromMap(state_map, index)
 
        if addr == None:
            print("array2 无法找到后继块:", next_states, hex(index))
            return None
        if addr == ret_block_ea:
            next_states.append(addr)
            print(next_states)
        else:
            verifyBlockLink(state_map, addr, ret_block_ea, next_states)
 
def findRETBlock(func_ea):
    func = idaapi.get_func(func_ea)
    blocks = idaapi.FlowChart(func)  # 获取方法中所有的基本块
    for block in blocks:
        block_end_ea = block.end_ea
        last_ins_ea = idc.prev_head(block_end_ea)
        mnem = idc.print_insn_mnem(last_ins_ea)
        if mnem == "RET":
            return block
 
def verifyLinkMain(state_map,fun_start):
    next_states = []
    ret_block = findRETBlock(fun_start)
    ret_block_ea = ret_block.start_ea#获取ret块地址
    verifyBlockLink(state_map, hex(fun_start), hex(ret_block_ea), next_states)#开始执行验证程序
 
 
 
def getBlockByAddress(ea):
    # 获取地址所在的函数
    func = idaapi.get_func(ea)
    if not func:
        print(f"地址 {hex(ea)} 不在任何函数中")
        return None
    # 创建控制流图
    blocks = idaapi.FlowChart(func)
 
    # 遍历所有块
    for block in blocks:
        # 检查地址是否在当前块中
        if block.start_ea <= ea < block.end_ea:
            # print(f"地址 {hex(ea)} 在块 {hex(block.start_ea)} - {hex(block.end_ea)} 中")
            return block
 
    print(f"地址 {hex(ea)} 未找到对应的块")
    return None
 
def patchBranch(src_addr, dest_addr,op_value = 0):
    # print("src_addr:",hex(src_addr),"dest_addr:",dest_addr)
 
    # CSEL W8, W9, W8, EQ
    CSEL_ea = idc.prev_head(src_addr)
    CSEL_3 = idc.print_operand(CSEL_ea,3)
    if op_value == 1:
        if CSEL_3 == "EQ":
                encoding, count = ks.asm(f'b.eq {dest_addr}', CSEL_ea)
        if CSEL_3 == "NE":
                encoding, count = ks.asm(f'b.ne {dest_addr}', CSEL_ea)
        if CSEL_3 == "GT":
                encoding, count = ks.asm(f'b.gt {dest_addr}', CSEL_ea)
        src_addr = CSEL_ea
    else:
        encoding, count = ks.asm(f'b {dest_addr}', src_addr)
 
    if not count:
        print('ks.asm err')
    else:
        for i in range(4):
            idc.patch_byte(src_addr + i, encoding[i])
            # print("patch success:",hex(src_addr),dest_addr)
 
def rebuildControlFlow(state_map):
    for block in state_map:
        block_ea = int(block,16)#需要把字符串转成int
        # 获取真实块保存的前驱、后继链接块索引
        value = state_map[block]
        # 查找块尾的跳转指令
        endEa = getBlockByAddress(block_ea).end_ea
 
        last_insn_ea = idc.prev_head(endEa)
        if idc.print_insn_mnem(last_insn_ea) == "B":
            # 如果是无条件跳转(B)
            if len(value) == 2:
                succ_index = value[0] #当前真实块的后继块索引
                if succ_index == None: #return块没有后继,过滤掉它
                    continue
                jmp_addr = getSuccBlockAddrFromMap(state_map,succ_index) #获取后继索引对应的真实块地址
                patchBranch(last_insn_ea, jmp_addr)
 
            # 如果是条件跳转(CSEL)
            elif len(value) == 3:
                succ_0 = value[0] #后继块的索引值
                jmp_addr_0 = getSuccBlockAddrFromMap(state_map, succ_0) #后继块的地址
                patchBranch(last_insn_ea, jmp_addr_0,1)
 
                succ_1 = value[1]   #后继块的索引值
                jmp_addr_1 = getSuccBlockAddrFromMap(state_map, succ_1)
                patchBranch(last_insn_ea, jmp_addr_1)
        if idc.print_insn_mnem(last_insn_ea) == "MOV":
            succ_index = value[0# 当前真实块的后继块索引
            # if succ_index == None:  # return块没有后继,过滤掉它
            #     continue
            jmp_addr = getSuccBlockAddrFromMap(state_map, succ_index)  # 获取后继索引对应的真实块地址
            patchBranch(last_insn_ea, jmp_addr)
 
# 获取主分发器
def findDispatchers(func_start,num = 10):
    func = idaapi.get_func(func_start)
    blocks = idaapi.FlowChart(func)
    pachers = []
    for block in blocks:
        preds = block.preds()
        preds_list = list(preds)
        if len(preds_list) > num:
            pachers.append(block)
    return pachers
 
def deObfuscatorFla():
    print("===============START===================")
    fn = 0x43058 #函数的起始地址
 
    patchers = findDispatchers(fn) #获取方法中所有的主分发器块 默认块被引用10次的为主分发器
    print("patchers:",len(patchers))
    if len(patchers) == 0:
        print("未找到主分发器")
        return
    # 这里只对一个主分发器操作,多个主分发器需要额外处理
    for disPatcherBlock in patchers:
        print("主分发器地址:", hex(disPatcherBlock.start_ea))
        stamp = getBlockLink(fn, disPatcherBlock.start_ea)  # 记录真实块链接关系
        verifyLinkMain(stamp,fn)#验证块连接关系是否正确
        rebuildControlFlow(stamp)
    print("===============END===================")
 
deObfuscatorFla()
from collections import deque # 导入双端队列,用于实现广度优先搜索 (BFS)
 
import ida_funcs # 导入 IDA Pro 函数相关 API
import idaapi # 导入 IDA Pro 核心 API
import idc # 导入 IDA Pro 经典 API (IDC)
 
def get_block_by_address(ea):
    # 获取地址所在的函数
    func = idaapi.get_func(ea)
    # 获取函数的流程图(FlowChart),包含了所有基本块
    blocks = idaapi.FlowChart(func)
    # 遍历函数中的所有基本块
    for block in blocks:
        # 判断地址 ea 是否在该基本块的范围内
        if block.start_ea <= ea < block.end_ea:
            # 如果是,则返回这个基本块对象
            return block
    # 如果没有找到,返回 None
    return None
 
def find_loop_heads(func):
    # 查找循环头。在OLLVM-fla中,这通常是主分发器(main dispatcher)
    loop_heads = set() # 使用集合来存储循环头地址,自动去重
    queue = deque() # 初始化一个队列用于 BFS
     
    # 获取函数入口地址的基本块
    block = get_block_by_address(func)
    # 将入口块和空路径(表示访问过的节点)加入队列
    queue.append((block, []))
     
    # 开始 BFS
    while len(queue) > 0:
        # 取出当前块和到达该块的路径
        cur_block, path = queue.popleft()
         
        # 检查当前块是否已经在路径中
        if cur_block.start_ea in path:
            # 如果在,说明找到了一条回边(back-edge),当前块是一个循环头
            loop_heads.add(cur_block.start_ea)
            # 停止在当前路径上继续搜索,避免无限循环
            continue
             
        # 将当前块的起始地址添加到路径中
        path = path + [cur_block.start_ea]
        # 将当前块的所有后继块(succs)及当前路径加入队列,继续搜索
        queue.extend((succ, path) for succ in cur_block.succs())
         
    # 将集合转换为列表
    all_loop_heads = list(loop_heads)
    # 升序排序, 保证函数开始的主循环头在第一个(非标准OLLVM可能有多个循环头)
    all_loop_heads.sort()
    return all_loop_heads
 
def find_converge_addr(loop_head_addr):
    # 寻找汇聚块(converge block)。真实块执行完毕后会跳转到汇聚块,汇聚块再跳回循环头
    converge_addr = None
    # 获取循环头基本块的对象
    block = get_block_by_address(loop_head_addr)
    # 获取循环头的所有前驱块(preds)
    preds = block.preds()
    pred_list = list(preds)
     
    # 标准OLLVM中,循环头(主分发器)有两个前驱:序言块 和 汇聚块
    if len(pred_list) == 2:
        for pred in pred_list:
            # 获取前驱块的“前驱们”
            tmp_list = list(pred.preds())
            # 汇聚块的特点是它有多个前驱(来自所有真实块),而序言块通常只有一个
            if len(tmp_list) > 1:
                # 这个就是汇聚块
                converge_addr = pred.start_ea
    else: # 非标准ollvm
        # 在某些非标准OLLVM变种中,循环头和汇聚块可能是同一个块
        converge_addr = loop_head_addr
    return converge_addr
 
def get_basic_block_size(bb):
    # 辅助函数:计算一个基本块的大小(结束地址 - 开始地址)
    return bb.end_ea - bb.start_ea
 
def add_block_color(ea):
    # 辅助函数:给指定地址所在的基本块在IDA中染色,便于分析
    block = get_block_by_address(ea)
    curr_addr = block.start_ea
    # 遍历块内的每一条指令
    while curr_addr < block.end_ea:
        # 设置指令颜色为 0xffcc33 (一种黄色)
        idc.set_color(curr_addr, idc.CIC_ITEM, 0xffcc33)
        # 移动到下一条指令的头部
        curr_addr = idc.next_head(curr_addr)
 
# 清除函数中的颜色渲染
def del_func_color(curr_addr):
    # 找到函数的结束地址
    end_ea = idc.find_func_end(curr_addr)
    # 遍历函数内的所有指令
    while curr_addr < end_ea:
        # 恢复默认颜色 (0xffffffff)
        idc.set_color(curr_addr, idc.CIC_ITEM, 0xffffffff)
        curr_addr = idc.next_head(curr_addr)
 
def find_ret_block_addr(blocks):
    # 查找函数的返回块(或逻辑上的返回块)
    for block in blocks:
        succs = block.succs()  # 获取后继块
        succs_list = list(succs)  # 转为list结构
 
        end_ea = block.end_ea
        # 获取块的最后一条指令地址
        last_ins_ea = idc.prev_head(end_ea)
        # 获取最后一条指令的助记符
        mnem = idc.print_insn_mnem(last_ins_ea)
 
        # 如果一个块没有后继
        if len(succs_list) == 0:
            # 并且最后一条指令是 "RET"
            if mnem == "RET":
                # 注释说明:不直接用RET块,而是往上找前驱的分支块
                # 如果直接去把RET指令所在的块作为返回块的表示 最后可能会出现反混淆代码赋值错误
                # 所以这里取RET指令的前驱块并且前驱块的大小不能只有一条指令,一般这个块都是有分支的
                ori_ret_block = block # 保存原始的RET块
                 
                # 循环向上回溯
                while True:
                    tmp_block = block.preds() # 获取前驱
                    pred_list = list(tmp_block)
                    if len(pred_list) == 1: # 如果只有一个前驱
                        block = pred_list[0]
                        # 如果块大小为4(可能只是个跳板),则继续向上
                        if get_basic_block_size(block) == 4:
                            continue
                        else:
                            # 找到了一个非跳板块
                            break
                    else:
                        # 找到了一个有多个前驱的分支块
                        break
 
                # 特例处理:如果找到的这个“返回块”本身又是一个子分发器(有多个后继)
                # 此时应该使用原始的RET块
                # 此处while循环是为了解决当上述的ret块作为子分发器时,需要重新更改ret块为带ret指令的块
                block2 = block
                num = 0
                i = 0
                while True:
                    i += 1
                    succs_block = block2.succs()
                    for succ in succs_block:
                        child_succs = succ.succs()
                        succ_list = list(child_succs)
                        if len(succ_list) != 0:
                            block2 = succ
                            num += 1
                    if num > 2: # 如果后继太多,判定为子分发器
                        block = ori_ret_block # 恢复为原始RET块
                        break
                    if i > 2: # 限制搜索深度
                        break
                return block.start_ea # 返回最终确定的返回块地址
 
def find_all_real_block(func_ea):
    # 主函数:找到函数(func_ea)中所有的真实块(Real Blocks)
     
    # 获取函数的所有基本块
    blocks = idaapi.FlowChart(idaapi.get_func(func_ea))
 
    # 获取所有循环头(非标准ollvm可能有多个主分发器)
    loop_heads = find_loop_heads(func_ea)
    print(f"循环头数量:{len(loop_heads)}----{[hex(loop_head) for loop_head in loop_heads]}")
 
    all_real_block = [] # 用于存储所有真实块(按循环头分组)
     
    # 遍历每个循环头(主分发器)
    for loop_head_addr in loop_heads:
        loop_head_block = get_block_by_address(loop_head_addr) # 获取循环头
        loop_head_preds = list(loop_head_block.preds()) # 获取循环头的所有前驱块
        loop_head_preds_addr = [loop_head_pred.start_ea for loop_head_pred in loop_head_preds] # 把所有前驱块转为地址数组
 
        # 获取汇聚块地址
        converge_addr = find_converge_addr(loop_head_addr)
 
        real_blocks = [] # 存储当前这个循环头对应的真实块
 
        # 如果循环头和汇聚块不是同一个块(标准OLLVM)
        if loop_head_addr != converge_addr:
            loop_head_preds_addr.remove(converge_addr) # 移除汇聚块, 剩下的一个是序言块
            real_blocks.extend(loop_head_preds_addr) # 序言块是第一个真实块
 
        # 获取汇聚块,并遍历它的所有前驱(这些就是真实块或其跳板)
        converge_block = get_block_by_address(converge_addr)
        list_preds = list(converge_block.preds())
        for pred_block in list_preds:
            end_ea = pred_block.end_ea
            last_ins_ea = idc.prev_head(end_ea)
            mnem = idc.print_insn_mnem(last_ins_ea)  # 获取基本块最后一条指令的操作符
 
            size = get_basic_block_size(pred_block)
            # 过滤掉小的(可能是跳板)或无条件跳转的块
            if size > 4 and "B." not in mnem:
                start_ea = pred_block.start_ea
                mnem = idc.print_insn_mnem(start_ea)
                 
                # 文章中提到,CSEL块是OLLVM-fla实现分支的关键
                if mnem == "CSEL":
                    # 如果这个块以CSEL开头,它是一个共享块,真正的真实块是它的前驱
                    csel_preds = pred_block.preds()
                    for csel_pred in csel_preds:
                        real_blocks.append(csel_pred.start_ea)
                else:
                    # 否则,这个块本身就是真实块
                    real_blocks.append(pred_block.start_ea)
 
        real_blocks.sort() # 排序后第一个元素始终是序言块
        all_real_block.append(real_blocks)
        print("子循环头:", [hex(child_block_ea) for child_block_ea in real_blocks])
 
    # 获取return块
    ret_addr = find_ret_block_addr(blocks)
    all_real_block.append(ret_addr) # 将返回块也加入列表
    print("all_real_block:", all_real_block)
 
    # 将所有分组的真实块合并到一个扁平的列表 all_real_block_list
    all_real_block_list = []
    for real_blocks in all_real_block:
        if isinstance(real_blocks, list):  # 如果是列表,用 extend
            all_real_block_list.extend(real_blocks)
        else# 如果不是列表(比如ret_addr),用 append
            all_real_block_list.append(real_blocks)
 
    # 为所有找到的真实块在IDA中上色
    for real_block_ea in all_real_block_list:
        # idc.add_bpt(real_block_ea)#断点
        add_block_color(real_block_ea) # 渲染颜色
 
    print("\n所有真实块获取完成")
    print("===========INT===============")
    print(all_real_block_list)
    print("===========HEX===============")
    print(f"数量:{len(all_real_block_list)}")
    print([hex(real_block_ea) for real_block_ea in all_real_block_list], "\n")
 
    # 准备angr分析所需的参数:分离出子序言块(非标准OLLVM)
    # 移除ret地址和主序言块相关真实块, 保留子序言块相关的真实块
    all_child_prologue_addr = all_real_block.copy()
    all_child_prologue_addr.remove(ret_addr)
    all_child_prologue_addr.remove(all_child_prologue_addr[0]) # 移除主序言块
    print("所有子序言块相关的真实块地址:", all_child_prologue_addr)
 
    # 获取所有子序言块的最后一条指令地址(用于angr hook)
    all_child_prologue_last_ins_ea = []
    for child_prologue_array in all_child_prologue_addr:
        child_prologue_addr = child_prologue_array[0] # 子序言块的起始地址
        child_prologue_block = get_block_by_address(child_prologue_addr)
        child_prologue_end_ea = child_prologue_block.end_ea
        child_prologue_last_ins_ea = idc.prev_head(child_prologue_end_ea) # 最后一条指令地址
        all_child_prologue_last_ins_ea.append(child_prologue_last_ins_ea)
     
    print("所有子序言块的最后一条指令的地址:", all_child_prologue_last_ins_ea)
 
    # 返回 真实块列表, 子序言块列表, 子序言块最后指令地址列表
    return all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea
 
'''
========================angr执行=============================
'''
import logging # 导入日志模块
import time # 导入时间模块
 
import angr # 导入 angr 框架
from tqdm import tqdm # 导入 tqdm,用于显示进度条
 
# 过滤angr日志,只显示ERROR日志,里面许多的WARNING输出影响日志分析
logging.getLogger('angr').setLevel(logging.ERROR)
 
def capstone_decode_csel(insn):
    # 辅助函数:解析 CSEL 指令,提取操作数
    operands = insn.op_str.replace(' ', '').split(',')
    dst_reg = operands[0] # 目标寄存器
    condition = operands[3] # 条件码
    reg1 = operands[1] # 源寄存器1 (True)
    reg2 = operands[2] # 源寄存器2 (False)
    return dst_reg, reg1, reg2, condition
 
def print_reg(state, reg_name):
    # 辅助调试函数:打印寄存器值
    value = state.regs.get(reg_name)
    print(f"地址:{hex(state.addr)},寄存器:{reg_name},value:{value}")
 
def find_state_succ(proj, base, local_state, flag, real_blocks, real_block_addr, path):
    # 辅助函数:用于探索 CSEL 指令的一个分支(真或假)
     
    # 获取当前块的第一条指令(即CSEL指令)
    ins = local_state.block().capstone.insns[0]
    # 解析 CSEL
    dst_reg, reg1, reg2, condition = capstone_decode_csel(ins)
    # 获取两个源寄存器的 *符号* 值(在angr中可能是 <BV64 ...>)
    val1 = local_state.regs.get(reg1)
    val2 = local_state.regs.get(reg2)
    # print(f"寄存器值 {reg1}:{val1},{reg2}:{val2}")
 
    # 创建模拟管理器
    sm = proj.factory.simgr(local_state)
    # 执行 CSEL 指令
    sm.step(num_inst=1)
    # 获取执行后的状态
    tmp_state = sm.active[0]
     
    # 关键:根据 flag(True/False)强制设置CSEL的目标寄存器
    if flag:
        setattr(tmp_state.regs, dst_reg, val1)  # 给寄存器的条件判断结果设为真
    else:
        setattr(tmp_state.regs, dst_reg, val2)  # 给寄存器的条件判断结果设为假
 
    # print(f"开始运行的寄存器:{sm.active[0].regs.get(dst_reg)}")
     
    # 继续模拟执行,直到碰到下一个真实块
    while len(sm.active):
        # print(sm.active)
        for active_state in sm.active:
            ins_offset = active_state.addr - base # 计算当前指令的偏移
            # if ins_offset == 0x41DC0:
            #     print_reg("x8")
             
            # 如果当前地址是已知的真实块之一
            if ins_offset in real_blocks:
                value = path[real_block_addr] # 获取当前分析块的后继列表
                if ins_offset not in value: # 如果这个后继块还没被记录
                    value.append(ins_offset) # 添加到后继列表
                    return ins_offset # 返回找到的后继块地址
        sm.step(num_inst=1) # 继续执行下一条指令
 
def find_block_succ(proj, base, func_offset, state, real_block_addr, real_blocks, path):
    # 主分析函数:找到一个真实块(real_block_addr)的所有后继真实块
     
    msm = proj.factory.simgr(state)  # 构造模拟器
 
    # 第一个while的作用: 寻找到传入的真实块地址作为主块, 再复制一份当前state, 准备后继块获取的操作
    while len(msm.active):
        # print(f"路径{msm.active}")
        for active_state in msm.active:
            offset = active_state.addr - base
            if offset == real_block_addr:  # 找到真实块
                mstate = active_state.copy()  # 复制state, 为后继块的获取做准备
                msm2 = proj.factory.simgr(mstate)
                msm2.step(num_inst=1# 防止下个while里获取后继块的时候key和value重复
                 
                # 第二个while的作用: 寻找真实块的所有后继块
                while len(msm2.active):
                    # print(msm2.active)
                    for mactive_state in msm2.active:
                        ins_offset = mactive_state.addr - base
                         
                        # 情况一:无分支块(后继是真实块)
                        if ins_offset in real_blocks:
                            # 在无条件跳转中,并且有至少两条路径同时执行到真实块时,取非ret块的真实块
                            msm2_len = len(msm2.active)
                            if msm2_len > 1: # 如果angr分析出多条路径
                                tmp_addrs = []
                                for s in msm2.active:
                                    moffset = s.addr - base
                                    tmp_value = path[real_block_addr]
                                    if moffset in real_blocks and moffset not in tmp_value:
                                        tmp_addrs.append(moffset)
                                if len(tmp_addrs) > 1: # 如果多条路径指向不同的真实块
                                    print("当前至少有两个路径同时执行到真实块:", [hex(tmp_addr) for tmp_addr in tmp_addrs])
                                    ret_addr = real_blocks[len(real_blocks) - 1] # 获取返回块地址
                                    if ret_addr in tmp_addrs:
                                        tmp_addrs.remove(ret_addr) # 优先排除返回块
                                    ins_offset = tmp_addrs[0] # 选择剩下的那个
                                    print("两个路径同时执行到真实块最后取得:", hex(ins_offset))
 
                            value = path[real_block_addr]
                            if ins_offset not in value: # 记录后继
                                value.append(ins_offset)
                            print(f"无条件跳转块关系:{hex(real_block_addr)}-->{hex(ins_offset)}")
                            return # 找到后继,结束当前块的分析
 
                        # 情况二:有分支块(CSEL)
                        ins = mactive_state.block().capstone.insns[0]
                        if ins.mnemonic == 'csel':
                            # 复制状态,分别探索 True 和 False 分支
                            state_true = mactive_state.copy()
                            state_true_succ_addr = find_state_succ(proj, base, state_true, True, real_blocks, real_block_addr, path)
 
                            state_false = mactive_state.copy()
                            state_false_succ_addr = find_state_succ(proj, base, state_false, False, real_blocks, real_block_addr, path)
 
                            if state_true_succ_addr is None or state_false_succ_addr is None:
                                print("csel错误指令地址:", hex(ins_offset))
                                print(f"csel后继有误:{hex(real_block_addr)}-->{hex(state_true_succ_addr) if state_true_succ_addr is not None else state_true_succ_addr},"
                                      f"{hex(state_false_succ_addr) if state_false_succ_addr is not None else state_false_succ_addr}")
                                return "erro"
 
                            print(f"csel分支跳转块关系:{hex(real_block_addr)}-->{hex(state_true_succ_addr)},{hex(state_false_succ_addr)}")
                            return # 找到两个分支的后继,结束当前块的分析
                    msm2.step(num_inst=1) # 继续单步执行
                return  # 真实块集合中的最后一个基本块如果最后没找到后继,说明是return块,直接返回
            msm.step(num_inst=1) # 继续单步执行(在第一个while中,直到找到real_block_addr)
 
def angr_main(real_blocks, all_child_prologue_addr, all_child_prologue_last_ins_ea, func_offset, file_path):
    # angr 分析的主入口
     
    # 加载二进制文件
    proj = angr.Project(file_path, auto_load_libs=False)
    # 获取基地址
    base = proj.loader.min_addr
    # 计算函数在内存中的实际地址
    func_addr = base + func_offset
    # 创建一个空状态,从函数入口开始
    init_state = proj.factory.blank_state(addr=func_addr)
    # 设置选项:不进入子函数调用
    init_state.options.add(angr.options.CALLLESS)
 
    # 初始化用于存储CFG的字典,键为真实块地址,值为空列表(用于存后继)
    path = {key: [] for key in real_blocks}
    # 找到返回块的地址
    ret_addr = real_blocks[len(real_blocks) - 1]
 
    # 获取主序言块(函数入口块)
    first_block = proj.factory.block(func_addr)
    first_block_insns = first_block.capstone.insns
    # 获取主序言块的最后一条指令(用于下hook)
    first_block_last_ins = first_block_insns[len(first_block_insns) - 1]
 
    # 遍历所有真实块,为每一个块寻找它的后继
    for real_block_addr in tqdm(real_blocks):
        if ret_addr == real_block_addr: # 如果是返回块,则跳过(它没有后继)
            continue
 
        prologue_block_addr = 0 # 子序言块地址
        child_prologue_last_ins_ea = 0 # 子序言块最后一条指令的地址
         
        # 检查当前真实块是否属于某个子序言块组(非标准OLLVM)
        if len(all_child_prologue_addr) > 0:
            for index, child_prologue_array in enumerate(all_child_prologue_addr):
                if real_block_addr in child_prologue_array:
                    prologue_block_addr = child_prologue_array[0] + base # 子序言块的内存地址
                    child_prologue_last_ins_ea = all_child_prologue_last_ins_ea[index] # 最后一条指令的地址
 
        # 拷贝初始化state, 确保每个真实块的分析都是从干净的函数入口状态开始
        state = init_state.copy()
        print("正在寻找:", hex(real_block_addr))
 
        # hook函数:用于劫持PC并跳转到目标真实块
        def jump_to_address(state):
            # -4 是因为ARM指令长度为4,hook发生在指令执行前,需要指向指令本身
            state.regs.pc = base + real_block_addr - 4
 
        # hook函数:用于跳转到子序言块
        def jump_to_child_prologue_address(state):
            state.regs.pc = prologue_block_addr - 4
 
        # 核心逻辑:设置 hook
        if prologue_block_addr == 0: # 标准OLLVM
            # 当序言块执行完后(初始化后续条件判断的寄存器),将最后一条指令的pc寄存器指向真实块地址
            if real_block_addr != func_offset: # 如果不是序言块本身
                # 在主序言块的最后一条指令处下hook,执行完后直接跳转到 real_block_addr
                proj.hook(first_block_last_ins.address, jump_to_address, first_block_last_ins.size)
        else: # 非标准OLLVM
            #  hook主序言块,让它跳转到子序言块
            proj.hook(first_block_last_ins.address, jump_to_child_prologue_address, first_block_last_ins.size)
            # hook子序言块,让它再跳转到 real_block_addr
            proj.hook(child_prologue_last_ins_ea, jump_to_address, 4)
 
        # 开始模拟执行,寻找后继
        ret = find_block_succ(proj, base, func_offset, state, real_block_addr, real_blocks, path)
        if ret == "erro": # 如果出错
            return
 
    # 将CFG中的地址转换为十六进制字符串
    hex_dict = {
        hex(key): [hex(value) for value in values]
        for key, values in path.items()
    }
 
    print("真实块控制流:\n", hex_dict)
    # 返回重建的控制流
    return hex_dict
 
'''
重建控制流
'''
from collections import deque # 再次导入 deque
import idaapi # 再次导入 idaapi
import idautils # 导入 ida utils
import idc # 再次导入 idc
import keystone # 导入 keystone 汇编引擎
 
# 初始化Ks arm64架构的so, 模式: 小端序
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
 
def patch_ins_to_nop(ins):
    # 辅助函数:将一条指令 patch 为 NOP
    size = idc.get_item_size(ins) # 获取指令大小
    for i in range(size):
        idc.patch_byte(ins + i, 0x90) # 逐字节替换为 0x90 (NOP)
 
def get_block_by_address(ea):
    # 辅助函数:获取地址所在的基本块(重复定义)
    func = idaapi.get_func(ea)
    blocks = idaapi.FlowChart(func)
    for block in blocks:
        if block.start_ea <= ea < block.end_ea:
            return block
    return None
 
def patch_branch(patch_list):
    # 主函数:根据 angr 生成的CFG (patch_list) 来 patch IDB
     
    # 遍历CFG中所有的真实块
    for ea in patch_list:
        values = patch_list[ea] # 获取后继块列表
        if len(values) == 0: # 如果后继块为0, 基本都是return块, 不需要patch, 直接跳过
            continue
             
        # 获取当前真实块的基本块对象
        block = get_block_by_address(int(ea, 16))
        start_ea = block.start_ea
        end_ea = block.end_ea
        # 获取块的最后一条指令地址(因为block.end_ea是下一块的起始)
        last_ins_ea = idc.prev_head(end_ea)
         
        # 情况一:有2个后继,说明是分支块
        if len(values) == 2:
            flag = False
            # 遍历块内的所有指令,寻找 CSEL
            for ins in idautils.Heads(start_ea, end_ea):
                if idc.print_insn_mnem(ins) == "CSEL":
                    # 提取 CSEL 的条件码
                    condition = idc.print_operand(ins, 3)
                    # 使用 keystone 将 CSEL 替换为 B.cond(跳转到第一个后继, 即True分支)
                    encoding, count = ks.asm(f'B.{condition} {values[0]}', ins)
                    # 使用 keystone 将块的最后一条指令替换为 B(跳转到第二个后继, 即False分支)
                    encoding2, count2 = ks.asm(f'B {values[1]}', last_ins_ea)
                    # 执行 patch
                    for i in range(4):
                        idc.patch_byte(ins + i, encoding[i])
                    for i in range(4):
                        idc.patch_byte(last_ins_ea + i, encoding2[i])
                    flag = True
             
            # 特例处理:如果在有分支跳转的情况下没有找到CSEL指令
            # (说明CSEL在下一个块,当前块是做比较和条件跳转的)
            if not flag:
                ins = idc.prev_head(last_ins_ea) # 定位到倒数第二条指令
                succs_list = list(block.succs())
                csel_ea = succs_list[0].start_ea # CSEL 指令在后继块
                condition = idc.print_operand(csel_ea, 3) # 获取csel指令的条件判断
                 
                # 在当前块的倒数第二条指令 patch B.cond
                encoding, count = ks.asm(f'B.{condition} {values[0]}', ins)
                # 在当前块的最后一条指令 patch B
                encoding2, count2 = ks.asm(f'B {values[1]}', last_ins_ea)
                try:
                    for i in range(4):
                        idc.patch_byte(ins + i, encoding[i])
                    for i in range(4):
                        idc.patch_byte(last_ins_ea + i, encoding2[i])
                except:
                    print("except")
 
        # 情况二:只有1个后继,无分支块
        else:
            # 将块的最后一条指令替换为 B (无条件跳转) 到唯一的后继
            encoding, count = ks.asm(f'B {values[0]}', last_ins_ea)
            for i in range(4):
                idc.patch_byte(last_ins_ea + i, encoding[i])
                 
    print("pach over!!!")
 
def find_all_useless_block(func_ea, real_blocks):
    # 查找所有的“无用块”(即混淆块)
    blocks = idaapi.FlowChart(idaapi.get_func(func_ea))
    local_real_blocks = real_blocks.copy() # 复制一份真实块列表
    useless_blocks = [] # 存储无用块
     
    # 找到返回块
    ret_block_addr = local_real_blocks[len(local_real_blocks) - 1]
    queue = deque()
    ret_block = get_block_by_address(ret_block_addr)
    queue.append(ret_block)
     
    # 处理ret块相关的后继块(这些块也不是无用块,比如异常处理)
    while len(queue) > 0:
        cur_block = queue.popleft()
        queue.extend(succ for succ in cur_block.succs())
        ret_flag = False
        for succ in cur_block.succs():
            local_real_blocks.append(succ.start_ea) # 将RET的后继也视为“真实”
            end_ea = succ.end_ea
            last_ins_ea = idc.prev_head(end_ea)
            mnem = idc.print_insn_mnem(last_ins_ea)
            if mnem == "RET":
                ret_flag = True
        if ret_flag: # 找到真正的RET就停止
            break
        # local_real_blocks.extend(succ.start_ea for succ in cur_block.succs())
         
    # 遍历函数的所有块
    for block in blocks:
        start_ea = block.start_ea
        # 如果一个块不在“真实块”列表中,它就是无用块
        if start_ea not in local_real_blocks:
            useless_blocks.append(start_ea)
             
    print("所有的无用块:", [hex(b) for b in useless_blocks])
    return useless_blocks
 
def patch_useless_blocks(func_ea, real_blocks):
    # 将所有找到的无用块用 NOP 填充
    useless_blocks = find_all_useless_block(func_ea, real_blocks)
    # print(useless_blocks)
    for useless_block_addr in useless_blocks:
        block = get_block_by_address(useless_block_addr)
        start_ea = block.start_ea
        end_ea = block.end_ea
 
        insns = idautils.Heads(start_ea, end_ea) # 获取块内所有指令
        for ins in insns:
            patch_ins_to_nop(ins) # 替换为NOP
    print("无用块nop完成")
 
def main(func_ea):
    # 脚本主入口
    file_path = idc.get_input_file_path() # 获取当前so路径
     
    # 步骤一:使用IDA API找到所有真实块
    all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea = find_all_real_block(func_ea)
     
    # 步骤二:使用 angr 执行符号分析,获取真实块之间的控制流
    patch_list = angr_main(all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea, func_ea, file_path)
     
    # 步骤三:使用 keystone 重建控制流(Patch)
    patch_branch(patch_list)
 
    # 步骤四(可选,已注释):用NOP填充所有无用的混淆块
    # patch_useless_blocks(func_ea,all_real_blocks)
     
    # 步骤五(可选,已注释):刷新IDA的函数控制流图
    # ida_funcs.reanalyze_function(ida_funcs.get_func(func_ea))
    # print("控制流图已刷新")
 
# 脚本执行入口,分析地址为 0x41D08 的函数
main(0x41D08)
from collections import deque # 导入双端队列,用于实现广度优先搜索 (BFS)
 
import ida_funcs # 导入 IDA Pro 函数相关 API
import idaapi # 导入 IDA Pro 核心 API
import idc # 导入 IDA Pro 经典 API (IDC)
 
def get_block_by_address(ea):
    # 获取地址所在的函数
    func = idaapi.get_func(ea)
    # 获取函数的流程图(FlowChart),包含了所有基本块
    blocks = idaapi.FlowChart(func)
    # 遍历函数中的所有基本块
    for block in blocks:
        # 判断地址 ea 是否在该基本块的范围内
        if block.start_ea <= ea < block.end_ea:
            # 如果是,则返回这个基本块对象
            return block
    # 如果没有找到,返回 None
    return None
 
def find_loop_heads(func):
    # 查找循环头。在OLLVM-fla中,这通常是主分发器(main dispatcher)
    loop_heads = set() # 使用集合来存储循环头地址,自动去重
    queue = deque() # 初始化一个队列用于 BFS
     
    # 获取函数入口地址的基本块
    block = get_block_by_address(func)
    # 将入口块和空路径(表示访问过的节点)加入队列
    queue.append((block, []))
     
    # 开始 BFS
    while len(queue) > 0:
        # 取出当前块和到达该块的路径
        cur_block, path = queue.popleft()
         
        # 检查当前块是否已经在路径中
        if cur_block.start_ea in path:
            # 如果在,说明找到了一条回边(back-edge),当前块是一个循环头
            loop_heads.add(cur_block.start_ea)
            # 停止在当前路径上继续搜索,避免无限循环
            continue
             
        # 将当前块的起始地址添加到路径中
        path = path + [cur_block.start_ea]
        # 将当前块的所有后继块(succs)及当前路径加入队列,继续搜索
        queue.extend((succ, path) for succ in cur_block.succs())
         
    # 将集合转换为列表
    all_loop_heads = list(loop_heads)
    # 升序排序, 保证函数开始的主循环头在第一个(非标准OLLVM可能有多个循环头)
    all_loop_heads.sort()
    return all_loop_heads
 
def find_converge_addr(loop_head_addr):
    # 寻找汇聚块(converge block)。真实块执行完毕后会跳转到汇聚块,汇聚块再跳回循环头
    converge_addr = None
    # 获取循环头基本块的对象
    block = get_block_by_address(loop_head_addr)
    # 获取循环头的所有前驱块(preds)
    preds = block.preds()
    pred_list = list(preds)
     

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 2025-11-24 17:44 被教教我吧~编辑 ,原因: 更新附件,以及补充todo
上传的附件:
收藏
免费 105
支持
分享
最新回复 (54)
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2025-11-21 22:14
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
感谢分享
2025-11-21 22:14
0
雪    币: 350
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
佬 牛逼
2025-11-22 09:36
0
雪    币: 204
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
大佬 牛逼
2025-11-22 18:17
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
666
2025-11-23 01:20
0
雪    币: 6094
活跃值: (5855)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
66
2025-11-23 21:15
0
雪    币: 387
活跃值: (1536)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
8
666
2025-11-23 22:35
0
雪    币: 244
活跃值: (2988)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
学习
2025-11-24 09:32
0
雪    币: 138
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
111
2025-11-24 10:03
0
雪    币: 31
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
期待更多优质内容的分享,论坛有你更精彩!
2025-11-24 10:30
0
雪    币: 713
活跃值: (1140)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
6666
2025-11-24 10:34
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
6666
2025-11-24 11:43
0
雪    币: 7223
活跃值: (5752)
能力值: ( LV12,RANK:280 )
在线值:
发帖
回帖
粉丝
14
感谢分享
2025-11-24 12:01
0
雪    币: 23
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
6
2025-11-24 12:11
0
雪    币: 3569
活跃值: (3046)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
mark
2025-11-24 12:53
0
雪    币: 209
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
666
2025-11-24 13:24
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18
谢谢你的细致分析,受益匪浅!
2025-11-24 13:47
0
雪    币: 806
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
19
牛掰,分析的很细致
2025-11-24 15:07
0
雪    币: 293
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20
6666666666666
2025-11-24 15:51
0
雪    币: 214
活跃值: (1762)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
6666
2025-11-24 18:24
0
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
22
111
2025-11-24 20:08
0
雪    币: 300
活跃值: (210)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
2025-11-25 02:48
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24
666
2025-11-25 09:47
0
雪    币: 268
活跃值: (1313)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
666
2025-11-25 09:48
0
游客
登录 | 注册 方可回帖
返回