Ollvm对抗

https://bbs.kanxue.com/thread-272414-1.htm
https://security.tencent.com/index.php/blog/msg/112
https://bbs.kanxue.com/thread-271164-1.htm
https://qmeimei10086.github.io/2025/12/28/长城杯初赛-2025-vvvmmm-最失败的一集/?utm_source=chatgpt.com

ollvm

前置知识

分类&识别

分类 描述
指令替换(Instructions Substitution)(Sub) 将一条运算指令替换为多条等价的运算指令,例如: y = x + 1变为y = x + 1 - 1
虚假控制流(Bogus Control Flow)(bcf) 通过加入包含不透明谓词的条件跳转和不可达的基本块,来干扰IDA的控制流分析和F5反汇编
控制流平坦化(Control Flow Flattening)(Fla) 主要通过一个主分发器来控制程序基本块的执行流程,将所有基本代码放到控制流最底部,然后删除原理基本块之间跳转关系,添加次分发器来控制分发逻辑,然后过新的复杂分发逻辑还原原来程序块之间的逻辑关系
字符串加密 编写一个pass将其中的字符串信息使用一些加密算法进行加密,然后特定的时间进行还原

D-810工具

  • 启动快捷键:
    edit->D-810或者Ctrl+Shift+D
  • 使用
    image.png
名称 作用
default_instruction_only.json 每条伪代码基本对应一条或少量汇编
default_unflattening_ollvm.json 针对 OLLVM(Obfuscator-LLVM)混淆的反扁平化配置
试图把 while(1) + switch + state 结构:还原成正常的 if / while / for
(推荐)
default_unflattening_switch_case.json 通用的 switch-case 反扁平化(推荐)
example_anel.json 用来演示 如何写 d810 的规则
default.json 默认综合配置(推荐日常使用)
  • 可优先尝试先default_unflattening_ollvm.json和default_unflattening_switch_case.json的组合

控制流平坦化(FLA)

控制流平坦化原理概要

控制流平坦化(control flow flattening)的基本思想:通过一个主分发器来控制程序基本块的执行流程,例如下图是正常的执行流程:
image.png

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

image-20251219221842413
  • 序言:函数的入口块,主要作用是初始化“状态变量”,将其设置为第一个真实块的“ID”。
  • 主 (子) 分发器:负责控制程序跳转到下一个待执行的基本块。通常是一个巨大的while(true)循环。子分发器可能包含另一层的swithch判断。
  • 真实块:混淆前的基本块,程序真正执行工作的块。
  • 预处理器:跳转到主分发器,这个块的唯一目的就是修改“状态变量”的值,将其设置为下一个应该执行的“真实块”的ID。
  • retn 块:函数出口 ,其特征是没有往下执行,且函数最后是以retn结尾

OLLVM反混淆经典思路

对于经典的 ollvm 来说,各个块之间有如下规则

  1. 找到序言块,这是整个函数的入口
  2. 序言块的后继是主分发器
  3. 主分发器的前驱有两个,一个是序言块,一个是预处理器
  4. 预处理器的前驱是真实块
  5. 除此之外的其他块是子分发器

去除FLA需要以下三步:

  • 找到真实块:除真实块和序言块外,其余的块都是虚假块,需要 NOP 掉他们。

  • 得到真实块之间的联系:我们需要让代码运行起来,可以用模拟执行,也可以在真机调试打断点 trace, 核心都是找到真实块之间的调用关系

  • 跳转:得到真实块之间的调用关系后,我们只需要在每个真实块的末尾,用跳转汇编指令将每个真实串起来就好了。

获取块关系

获取程序块 -> 下断点(ida动态调试,一直F9即可)-> 获取块之间的顺序

import ida_funcs
import ida_gdl
import ida_kernwin
import ida_name
import ida_bytes
import idautils
import ida_dbg


# =========================
# Config
# =========================

# 方式1:手动指定函数地址
TARGET_FUNC_EA = 0x401C60

# 方式2:自动使用当前光标所在函数
USE_CURRENT_FUNC = False

# 是否先清除该函数 block_start 上已有断点
CLEAR_OLD_BREAKPOINTS = False

# 是否打印块摘要信息
PRINT_BLOCK_INFO = True

# 是否打印命中轨迹
PRINT_TRACE = True

# 是否保存轨迹到内存
SAVE_TRACE = True

# 是否在进程退出时自动打印/保存完整 trace
DUMP_TRACE_ON_EXIT = True

# 调试结束后是否自动清除本脚本添加的断点
CLEAR_BREAKPOINTS_ON_EXIT = True

# 调试结束后是否自动卸载 hook
UNHOOK_ON_EXIT = True

# trace 输出文件;None 表示不写文件
TRACE_OUT_PATH = None
# 例如:
# TRACE_OUT_PATH = r"C:\temp\bb_trace.txt"

# 输出时是否显示 preds/succs 列表
SHOW_PREDS_SUCCS_IN_TRACE = False

# 输出时是否复制最终 trace 到剪贴板
COPY_TRACE_TO_CLIPBOARD_ON_EXIT = False


# =========================
# Helpers
# =========================

def get_target_func():
    if USE_CURRENT_FUNC:
        ea = ida_kernwin.get_screen_ea()
        f = ida_funcs.get_func(ea)
        if not f:
            raise RuntimeError("Current cursor is not inside a function: 0x%X" % ea)
        return f

    f = ida_funcs.get_func(TARGET_FUNC_EA)
    if not f:
        raise RuntimeError("No function at 0x%X" % TARGET_FUNC_EA)
    return f


def get_func_name(func):
    return ida_name.get_ea_name(func.start_ea) or ("sub_%X" % func.start_ea)


def get_last_insn_ea(start_ea, end_ea):
    """
    IDA basic block 的 end_ea 是开区间 [start_ea, end_ea)
    返回块内最后一条指令地址
    """
    last = None
    for ea in idautils.Heads(start_ea, end_ea):
        if ida_bytes.is_code(ida_bytes.get_flags(ea)):
            last = ea
    return last if last is not None else start_ea


def count_instructions(start_ea, end_ea):
    cnt = 0
    for ea in idautils.Heads(start_ea, end_ea):
        if ida_bytes.is_code(ida_bytes.get_flags(ea)):
            cnt += 1
    return cnt


def guess_block_role(block_size, insn_count, pred_count, succ_count):
    """
    只是启发式提示,不是严格结论
    """
    if insn_count <= 3 and succ_count == 1:
        return "trampoline_like"
    if insn_count <= 4 and succ_count >= 2:
        return "dispatcher_like"
    if insn_count >= 8:
        return "real_block_like"
    return "uncertain"


def collect_basic_blocks(func):
    fc = ida_gdl.FlowChart(func, flags=ida_gdl.FC_PREDS)
    block_list = []

    for idx, b in enumerate(fc):
        preds = [x.start_ea for x in b.preds()]
        succs = [x.start_ea for x in b.succs()]

        block_start = b.start_ea
        block_end_exclusive = b.end_ea
        block_end = get_last_insn_ea(block_start, block_end_exclusive)
        block_size = block_end_exclusive - block_start
        insn_count = count_instructions(block_start, block_end_exclusive)
        pred_count = len(preds)
        succ_count = len(succs)
        role_hint = guess_block_role(block_size, insn_count, pred_count, succ_count)

        block_list.append({
            "id": idx,
            "block_start": block_start,
            "block_end": block_end,
            "block_end_exclusive": block_end_exclusive,
            "block_size": block_size,
            "insn_count": insn_count,
            "preds": preds,
            "succs": succs,
            "pred_count": pred_count,
            "succ_count": succ_count,
            "role_hint": role_hint,
        })

    block_list.sort(key=lambda x: x["block_start"])
    return block_list


def clear_breakpoints(block_list):
    for block in block_list:
        addr = block["block_start"]
        ok = ida_dbg.del_bpt(addr)
        if PRINT_BLOCK_INFO:
            print("[BPT-DEL] %s at 0x%X" % ("OK" if ok else "FAIL", addr))


def add_breakpoints(block_list):
    for block in block_list:
        addr = block["block_start"]
        ok = ida_dbg.add_bpt(addr)
        if PRINT_BLOCK_INFO:
            print("[BPT-ADD] %s at 0x%X" % ("OK" if ok else "FAIL", addr))


def format_trace_lines(trace):
    lines = []
    for item in trace:
        line = (
            "%d : 0x%X (block_id=%d, size=%d, insn=%d, preds=%d, succs=%d, hint=%s)"
            % (
                item["hit_index"],
                item["ea"],
                item["block_id"],
                item["block_size"],
                item["insn_count"],
                item["pred_count"],
                item["succ_count"],
                item["role_hint"],
            )
        )
        if SHOW_PREDS_SUCCS_IN_TRACE:
            preds_text = ", ".join("0x%X" % x for x in item["preds"])
            succs_text = ", ".join("0x%X" % x for x in item["succs"])
            line += " preds=[%s] succs=[%s]" % (preds_text, succs_text)
        lines.append(line)
    return lines


def save_trace(trace, out_path):
    if not out_path:
        return

    lines = format_trace_lines(trace)
    with open(out_path, "w", encoding="utf-8") as fp:
        fp.write("\n".join(lines) + "\n")

    print("[TRACE] written to %s" % out_path)


# =========================
# Hook
# =========================

class BlockTraceHook(ida_dbg.DBG_Hooks):
    def __init__(self, block_list, func_name, func_start):
        super().__init__()
        self.block_list = block_list
        self.func_name = func_name
        self.func_start = func_start
        self.hit_index = 0
        self.trace = []

        # ea -> block info
        self.block_map = {b["block_start"]: b for b in block_list}

    def dbg_bpt(self, tid, ea):
        block = self.block_map.get(ea)
        if block is None:
            return 0

        record = {
            "hit_index": self.hit_index,
            "ea": ea,
            "block_id": block["id"],
            "block_size": block["block_size"],
            "insn_count": block["insn_count"],
            "pred_count": block["pred_count"],
            "succ_count": block["succ_count"],
            "preds": block["preds"],
            "succs": block["succs"],
            "role_hint": block["role_hint"],
        }

        if PRINT_TRACE:
            line = format_trace_lines([record])[0]
            print(line)

        if SAVE_TRACE:
            self.trace.append(record)

        self.hit_index += 1
        return 0

    def dbg_process_exit(self, pid, tid, ea, code):
        print("[EXIT] process exited with code %d" % code)

        if DUMP_TRACE_ON_EXIT and self.trace:
            print("[TRACE] total hits: %d" % len(self.trace))
            lines = format_trace_lines(self.trace)
            for line in lines:
                print(line)

            if TRACE_OUT_PATH:
                save_trace(self.trace, TRACE_OUT_PATH)

            if COPY_TRACE_TO_CLIPBOARD_ON_EXIT:
                try:
                    ida_kernwin.set_clipboard_text("\n".join(lines))
                    print("[TRACE] copied to clipboard")
                except Exception as e:
                    print("[TRACE] clipboard failed:", e)

        if CLEAR_BREAKPOINTS_ON_EXIT:
            clear_breakpoints(self.block_list)
            print("[BPT] breakpoints cleared on exit")

        if UNHOOK_ON_EXIT:
            try:
                self.unhook()
                print("[HOOK] uninstalled on exit")
            except Exception as e:
                print("[HOOK] unhook failed:", e)

        return 0


# =========================
# Main
# =========================

my_hook = None

def install_hook(block_list, func_name, func_start):
    global my_hook

    try:
        if my_hook is not None:
            my_hook.unhook()
            print("[HOOK] removed old hook")
    except Exception:
        pass

    my_hook = BlockTraceHook(block_list, func_name, func_start)
    my_hook.hook()
    print("[HOOK] installed")


def cleanup():
    global my_hook
    try:
        if my_hook is not None:
            clear_breakpoints(my_hook.block_list)
            my_hook.unhook()
            print("[CLEANUP] done")
    except Exception as e:
        print("[CLEANUP] failed:", e)


def main():
    func = get_target_func()
    func_name = get_func_name(func)
    block_list = collect_basic_blocks(func)

    print("[FUNC] %s @ 0x%X" % (func_name, func.start_ea))
    print("[BLOCKS] %d" % len(block_list))

    if PRINT_BLOCK_INFO:
        for b in block_list:
            print(
                "  id=%d start=0x%X end=0x%X size=%d insn=%d preds=%d succs=%d hint=%s"
                % (
                    b["id"],
                    b["block_start"],
                    b["block_end"],
                    b["block_size"],
                    b["insn_count"],
                    b["pred_count"],
                    b["succ_count"],
                    b["role_hint"],
                )
            )

    if CLEAR_OLD_BREAKPOINTS:
        clear_breakpoints(block_list)
        print("[BPT] old breakpoints cleared")

    add_breakpoints(block_list)
    install_hook(block_list, func_name, func.start_ea)

    print("[READY] Breakpoints are set on all block_start addresses.")
    print("[READY] Run the program and keep pressing F9 to collect the execution order.")
    print("[READY] On process exit, breakpoints will be cleared automatically.")

    return block_list


if __name__ == "__main__":
    main()

字段含义:

  • hit_index:第几次命中
  • ea:当前 block 起始地址
  • block_id:块编号
  • size:块地址跨度
  • insn:块内指令数
  • preds:前驱数量
  • succs:后继数量
  • hint:粗略角色判断

D-810

标准 ollvm-fla 用 IDA 插件——D-810。

区分是否标准:

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

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

image.png

OLLVM-fla 分析还原(复杂的非标准 fla)

原版

架构:ARM64 OLLVM

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
import logging
import os

import ida_funcs
import idaapi
import idautils
import idc

try:
    import angr
except ImportError as exc:
    raise RuntimeError("angr is required inside IDAPython for this script") from exc

try:
    import keystone
except ImportError as exc:
    raise RuntimeError("keystone is required inside IDAPython for this script") from exc

try:
    from tqdm import tqdm
except ImportError:
    def tqdm(iterable, *args, **kwargs):
        return iterable


logging.getLogger("angr").setLevel(logging.ERROR)

KS = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)


def get_imagebase():
    return idaapi.get_imagebase()


def ea_to_off(ea):
    return ea - get_imagebase()


def off_to_ea(off):
    return get_imagebase() + off


def get_block_by_address(ea):
    func = idaapi.get_func(ea)
    if func is None:
        return None
    for block in idaapi.FlowChart(func):
        if block.start_ea <= ea < block.end_ea:
            return block
    return None


def get_basic_block_size(bb):
    return bb.end_ea - bb.start_ea


def get_last_insn_ea(block):
    return idc.prev_head(block.end_ea, block.start_ea)


def get_mnem(ea):
    mnem = idc.print_insn_mnem(ea)
    return mnem.lower() if mnem else ""


def is_plain_branch_mnem(mnem):
    return mnem in {"b", "br"} or mnem.startswith("b.")


def patch_bytes(ea, data):
    for i, byte in enumerate(data):
        idc.patch_byte(ea + i, byte)


def patch_ins_to_nop(ins_ea):
    encoding, _ = KS.asm("NOP", ins_ea)
    patch_bytes(ins_ea, encoding)


def add_block_color(ea):
    block = get_block_by_address(ea)
    if block is None:
        return
    curr_ea = block.start_ea
    while curr_ea != idc.BADADDR and curr_ea < block.end_ea:
        idc.set_color(curr_ea, idc.CIC_ITEM, 0xFFCC33)
        curr_ea = idc.next_head(curr_ea, block.end_ea)


def del_func_color(func_ea):
    func = idaapi.get_func(func_ea)
    if func is None:
        return
    curr_ea = func.start_ea
    while curr_ea != idc.BADADDR and curr_ea < func.end_ea:
        idc.set_color(curr_ea, idc.CIC_ITEM, 0xFFFFFFFF)
        curr_ea = idc.next_head(curr_ea, func.end_ea)


def find_loop_heads(func_ea):
    loop_heads = set()
    queue = deque()

    block = get_block_by_address(func_ea)
    if block is None:
        return []

    queue.append((block, []))

    while queue:
        cur_block, path = queue.popleft()
        if cur_block.start_ea in path:
            loop_heads.add(cur_block.start_ea)
            continue

        next_path = path + [cur_block.start_ea]
        queue.extend((succ, next_path) for succ in cur_block.succs())

    all_loop_heads = sorted(loop_heads)
    return all_loop_heads


def find_converge_addr(loop_head_addr):
    block = get_block_by_address(loop_head_addr)
    if block is None:
        return None

    pred_list = list(block.preds())
    if len(pred_list) == 2:
        for pred in pred_list:
            if len(list(pred.preds())) > 1:
                return pred.start_ea
        return None

    return loop_head_addr


def find_ret_block_addr(blocks):
    for block in blocks:
        succs_list = list(block.succs())
        if succs_list:
            continue

        last_ins_ea = get_last_insn_ea(block)
        if get_mnem(last_ins_ea) != "ret":
            continue

        ori_ret_block = block

        while True:
            pred_list = list(block.preds())
            if len(pred_list) == 1:
                block = pred_list[0]
                if get_basic_block_size(block) <= 4:
                    continue
                break
            break

        block2 = block
        sub_dispatcher_score = 0
        for _ in range(2):
            next_blocks = []
            for succ in block2.succs():
                succ_succs = list(succ.succs())
                if succ_succs:
                    next_blocks.append(succ)
                    sub_dispatcher_score += 1
            if sub_dispatcher_score > 2:
                block = ori_ret_block
                break
            if not next_blocks:
                break
            block2 = next_blocks[0]

        return block.start_ea

    return None


def find_all_real_block(func_ea):
    func = idaapi.get_func(func_ea)
    if func is None:
        raise RuntimeError("target address is not inside a function")

    blocks = list(idaapi.FlowChart(func))
    loop_heads = find_loop_heads(func_ea)
    print(f"loop head count: {len(loop_heads)} -> {[hex(loop_head) for loop_head in loop_heads]}")

    grouped_real_blocks = []

    for loop_head_addr in loop_heads:
        loop_head_block = get_block_by_address(loop_head_addr)
        if loop_head_block is None:
            continue

        loop_head_preds_addr = [pred.start_ea for pred in loop_head_block.preds()]
        converge_addr = find_converge_addr(loop_head_addr)

        real_blocks = []

        if converge_addr is not None and loop_head_addr != converge_addr:
            prologue_candidates = [ea for ea in loop_head_preds_addr if ea != converge_addr]
            real_blocks.extend(prologue_candidates)

        converge_block = get_block_by_address(converge_addr if converge_addr is not None else loop_head_addr)
        if converge_block is None:
            continue

        for pred_block in converge_block.preds():
            last_ins_ea = get_last_insn_ea(pred_block)
            last_mnem = get_mnem(last_ins_ea)
            size = get_basic_block_size(pred_block)

            if size <= 4 or is_plain_branch_mnem(last_mnem):
                continue

            start_ea = pred_block.start_ea
            first_mnem = get_mnem(start_ea)

            if first_mnem == "csel":
                for csel_pred in pred_block.preds():
                    real_blocks.append(csel_pred.start_ea)
            else:
                real_blocks.append(start_ea)

        real_blocks = sorted(set(real_blocks))
        if not real_blocks:
            print(f"warning: empty real block group for loop head {hex(loop_head_addr)}")
            continue

        grouped_real_blocks.append(real_blocks)
        print("real block group:", [hex(block_ea) for block_ea in real_blocks])

    ret_addr = find_ret_block_addr(blocks)
    if ret_addr is None:
        raise RuntimeError("failed to find return block")

    all_real_block_list = []
    for real_blocks in grouped_real_blocks:
        all_real_block_list.extend(real_blocks)
    all_real_block_list.append(ret_addr)

    for real_block_ea in all_real_block_list:
        add_block_color(real_block_ea)

    print("\nall real blocks:")
    print("INT:", all_real_block_list)
    print("HEX:", [hex(real_block_ea) for real_block_ea in all_real_block_list])

    all_child_prologue_addr = [group for group in grouped_real_blocks[1:] if group] if len(grouped_real_blocks) > 1 else []
    print("child prologue groups:", [[hex(ea) for ea in group] for group in all_child_prologue_addr])

    all_child_prologue_last_ins_ea = []
    for child_prologue_array in all_child_prologue_addr:
        if not child_prologue_array:
            continue
        child_prologue_addr = child_prologue_array[0]
        child_prologue_block = get_block_by_address(child_prologue_addr)
        if child_prologue_block is None:
            continue
        all_child_prologue_last_ins_ea.append(get_last_insn_ea(child_prologue_block))

    print("child prologue last insns:", [hex(ea) for ea in all_child_prologue_last_ins_ea])
    return all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea


def capstone_decode_csel(insn):
    operands = insn.op_str.replace(" ", "").split(",")
    if len(operands) != 4:
        raise ValueError(f"unexpected CSEL operand format: {insn.op_str}")
    dst_reg = operands[0]
    reg1 = operands[1]
    reg2 = operands[2]
    condition = operands[3]
    return dst_reg, reg1, reg2, condition


def find_state_succ(proj, base, local_state, choose_true, real_blocks, real_block_addr, path, max_steps=0x4000):
    insns = local_state.block().capstone.insns
    if not insns:
        return None

    ins = insns[0]
    if ins.mnemonic.lower() != "csel":
        return None

    dst_reg, reg1, reg2, _ = capstone_decode_csel(ins)
    val1 = local_state.regs.get(reg1)
    val2 = local_state.regs.get(reg2)

    sm = proj.factory.simgr(local_state)
    sm.step(num_inst=1)
    if not sm.active:
        return None

    tmp_state = sm.active[0]
    if choose_true:
        setattr(tmp_state.regs, dst_reg, val1)
    else:
        setattr(tmp_state.regs, dst_reg, val2)

    steps = 0
    while sm.active and steps < max_steps:
        for active_state in sm.active:
            ins_offset = active_state.addr - base
            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)
        steps += 1

    return None


def find_block_succ(proj, base, state, real_block_addr, real_blocks, path, max_steps=0x4000):
    ret_addr = real_blocks[-1]
    msm = proj.factory.simgr(state)

    search_steps = 0
    while msm.active and search_steps < max_steps:
        for active_state in msm.active:
            offset = active_state.addr - base
            if offset != real_block_addr:
                continue

            mstate = active_state.copy()
            msm2 = proj.factory.simgr(mstate)
            msm2.step(num_inst=1)

            walk_steps = 0
            while msm2.active and walk_steps < max_steps:
                hit_real_blocks = []

                for mactive_state in msm2.active:
                    ins_offset = mactive_state.addr - base

                    if ins_offset in real_blocks:
                        if ins_offset not in hit_real_blocks:
                            hit_real_blocks.append(ins_offset)
                        continue

                    insns = mactive_state.block().capstone.insns
                    if not insns:
                        continue

                    ins = insns[0]
                    if ins.mnemonic.lower() == "csel":
                        state_true = mactive_state.copy()
                        state_true_succ_addr = find_state_succ(
                            proj,
                            base,
                            state_true,
                            True,
                            real_blocks,
                            real_block_addr,
                            path,
                            max_steps=max_steps,
                        )

                        state_false = mactive_state.copy()
                        state_false_succ_addr = find_state_succ(
                            proj,
                            base,
                            state_false,
                            False,
                            real_blocks,
                            real_block_addr,
                            path,
                            max_steps=max_steps,
                        )

                        if state_true_succ_addr is None or state_false_succ_addr is None:
                            print("csel error at:", hex(ins_offset))
                            print(
                                f"bad csel succ: {hex(real_block_addr)} -> "
                                f"{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 branch: {hex(real_block_addr)} -> "
                            f"{hex(state_true_succ_addr)}, {hex(state_false_succ_addr)}"
                        )
                        return

                if hit_real_blocks:
                    unique_hits = []
                    for hit in hit_real_blocks:
                        if hit not in path[real_block_addr] and hit not in unique_hits:
                            unique_hits.append(hit)

                    if len(unique_hits) > 1 and ret_addr in unique_hits:
                        unique_hits.remove(ret_addr)

                    if unique_hits:
                        chosen = unique_hits[0]
                        path[real_block_addr].append(chosen)
                        print(f"direct branch: {hex(real_block_addr)} -> {hex(chosen)}")
                        return chosen

                msm2.step(num_inst=1)
                walk_steps += 1

            return None

        msm.step(num_inst=1)
        search_steps += 1

    return None


def make_jump_hook(target_addr, skipped_size):
    def _hook(state):
        state.regs.pc = target_addr - skipped_size

    return _hook


def angr_main(real_block_eas, all_child_prologue_eas, all_child_prologue_last_ins_eas, func_ea, file_path):
    if not os.path.exists(file_path):
        raise RuntimeError(f"input file not found: {file_path}")

    real_blocks = [ea_to_off(ea) for ea in real_block_eas]
    child_groups = [[ea_to_off(ea) for ea in group] for group in all_child_prologue_eas]
    child_last_offsets = [ea_to_off(ea) for ea in all_child_prologue_last_ins_eas]
    func_offset = ea_to_off(func_ea)

    proj = angr.Project(file_path, auto_load_libs=False)
    base = proj.loader.main_object.mapped_base
    func_addr = base + func_offset

    init_state = proj.factory.blank_state(addr=func_addr)
    init_state.options.add(angr.options.CALLLESS)

    path = {key: [] for key in real_blocks}
    ret_addr = real_blocks[-1]

    first_block = proj.factory.block(func_addr)
    first_block_insns = first_block.capstone.insns
    if not first_block_insns:
        raise RuntimeError("failed to decode function entry block")
    first_block_last_ins = first_block_insns[-1]

    for real_block_addr in tqdm(real_blocks):
        if real_block_addr == ret_addr:
            continue

        prologue_block_addr = None
        child_prologue_last_ins_addr = None
        for index, child_prologue_array in enumerate(child_groups):
            if real_block_addr in child_prologue_array:
                prologue_block_addr = base + child_prologue_array[0]
                child_prologue_last_ins_addr = base + child_last_offsets[index]
                break

        state = init_state.copy()
        installed_hooks = []
        print("finding succ for:", hex(real_block_addr))

        try:
            if prologue_block_addr is None:
                if real_block_addr != func_offset:
                    proj.hook(
                        first_block_last_ins.address,
                        make_jump_hook(base + real_block_addr, first_block_last_ins.size),
                        first_block_last_ins.size,
                    )
                    installed_hooks.append(first_block_last_ins.address)
            else:
                proj.hook(
                    first_block_last_ins.address,
                    make_jump_hook(prologue_block_addr, first_block_last_ins.size),
                    first_block_last_ins.size,
                )
                installed_hooks.append(first_block_last_ins.address)

                proj.hook(
                    child_prologue_last_ins_addr,
                    make_jump_hook(base + real_block_addr, 4),
                    4,
                )
                installed_hooks.append(child_prologue_last_ins_addr)

            ret = find_block_succ(proj, base, state, real_block_addr, real_blocks, path)
            if ret == "erro":
                return None
        finally:
            for hook_addr in installed_hooks:
                if proj.is_hooked(hook_addr):
                    proj.unhook(hook_addr)

    hex_dict = {hex(key): [hex(value) for value in values] for key, values in path.items()}
    print("real block cfg:\n", hex_dict)
    return path


def patch_branch(patch_list):
    for block_off, succ_offs in patch_list.items():
        if not succ_offs:
            continue

        block_ea = off_to_ea(block_off)
        block = get_block_by_address(block_ea)
        if block is None:
            continue

        start_ea = block.start_ea
        end_ea = block.end_ea
        last_ins_ea = get_last_insn_ea(block)
        succ_eas = [off_to_ea(off) for off in succ_offs]

        if len(succ_eas) == 2:
            patched_csel = False

            for ins_ea in idautils.Heads(start_ea, end_ea):
                if get_mnem(ins_ea) != "csel":
                    continue

                condition = idc.print_operand(ins_ea, 3).lower()
                encoding_true, _ = KS.asm(f"B.{condition} 0x{succ_eas[0]:x}", ins_ea)
                encoding_false, _ = KS.asm(f"B 0x{succ_eas[1]:x}", last_ins_ea)
                patch_bytes(ins_ea, encoding_true)
                patch_bytes(last_ins_ea, encoding_false)
                patched_csel = True
                break

            if patched_csel:
                continue

            succs_list = list(block.succs())
            if not succs_list:
                continue

            csel_ea = succs_list[0].start_ea
            if get_mnem(csel_ea) != "csel":
                print(f"warning: expected csel successor at {hex(csel_ea)}")
                continue

            cond_branch_ea = idc.prev_head(last_ins_ea, start_ea)
            condition = idc.print_operand(csel_ea, 3).lower()
            encoding_true, _ = KS.asm(f"B.{condition} 0x{succ_eas[0]:x}", cond_branch_ea)
            encoding_false, _ = KS.asm(f"B 0x{succ_eas[1]:x}", last_ins_ea)
            patch_bytes(cond_branch_ea, encoding_true)
            patch_bytes(last_ins_ea, encoding_false)
        else:
            encoding, _ = KS.asm(f"B 0x{succ_eas[0]:x}", last_ins_ea)
            patch_bytes(last_ins_ea, encoding)

    print("patch over")


def find_all_useless_block(func_ea, real_blocks):
    func = idaapi.get_func(func_ea)
    if func is None:
        return []

    blocks = list(idaapi.FlowChart(func))
    local_real_blocks = list(real_blocks)
    useless_blocks = []

    ret_block_addr = local_real_blocks[-1]
    ret_block = get_block_by_address(ret_block_addr)
    if ret_block is None:
        return []

    queue = deque([ret_block])
    visited = set()

    while queue:
        cur_block = queue.popleft()
        if cur_block.start_ea in visited:
            continue
        visited.add(cur_block.start_ea)

        ret_flag = False
        for succ in cur_block.succs():
            queue.append(succ)
            if succ.start_ea not in local_real_blocks:
                local_real_blocks.append(succ.start_ea)

            last_ins_ea = get_last_insn_ea(succ)
            if get_mnem(last_ins_ea) == "ret":
                ret_flag = True

        if ret_flag:
            break

    for block in blocks:
        if block.start_ea not in local_real_blocks:
            useless_blocks.append(block.start_ea)

    print("useless blocks:", [hex(b) for b in useless_blocks])
    return useless_blocks


def patch_useless_blocks(func_ea, real_blocks):
    useless_blocks = find_all_useless_block(func_ea, real_blocks)
    for useless_block_addr in useless_blocks:
        block = get_block_by_address(useless_block_addr)
        if block is None:
            continue

        for ins_ea in idautils.Heads(block.start_ea, block.end_ea):
            patch_ins_to_nop(ins_ea)

    print("useless blocks patched to nop")


def main(func_ea):
    file_path = idc.get_input_file_path()

    all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea = find_all_real_block(func_ea)
    patch_list = angr_main(
        all_real_block_list,
        all_child_prologue_addr,
        all_child_prologue_last_ins_ea,
        func_ea,
        file_path,
    )
    if patch_list is None:
        print("angr failed to rebuild cfg")
        return None

    patch_branch(patch_list)

    # Optional cleanup:
    # patch_useless_blocks(func_ea, all_real_block_list)
    # ida_funcs.reanalyze_function(ida_funcs.get_func(func_ea))

    return patch_list


def run_at_current_function():
    screen_ea = idaapi.get_screen_ea() if hasattr(idaapi, "get_screen_ea") else idc.here()
    func = idaapi.get_func(screen_ea)
    if func is None:
        raise RuntimeError("place the cursor inside the target function and rerun")
    return main(func.start_ea)


if __name__ == "__main__":
    run_at_current_function()
    # Or use a fixed function start:
    # main(0x41D08)


虚假控制流

原理/特征

会在代码中添加两种判断条件,一种是恒为FALSE,另一种是恒为TRUE。在下面这张图中,其中的x和y恒实际上恒为0,我们可以发现上面的就是恒为FALSE,下面的是恒为TRUE。

图片描述

不会到达并被执行的代码块称为不可达的基本块,如上图中的LABEL_10和它下面的几行代码。
这里的x和y由于IDA不知道其真实值,会导致伪代码复杂,这里的x和y称作不透明谓词。

而那些不可达的基本块,需要nop掉。

利用angr符号执行去除不可达的基本块

利用符号执行去混淆的基本思路是:先找到目标函数的所有基本块,再通过符号执行遍历目标函数所有可达的基本块,剩下的就是不可达的基本块。把不可达的基本块全部nop掉,就能使IDA的F5反汇编正常分析。

示例脚本

python deflat.py -f 目标文件名 -s 起始地址 -e 结束地址
import argparse
import angr
import claripy

# 1. 参数解析 (保持不变)
parser = argparse.ArgumentParser()
parser.add_argument('-f','--file', help='The path of binary file')
parser.add_argument('-s','--start', help='Start address (hex)')
parser.add_argument('-e','--end', help='End address (hex)')
args = parser.parse_args()
if not all([args.file, args.start, args.end]):
    parser.print_help()
    exit(0)

filename = args.file
start_addr = int(args.start, 16)
end_addr = int(args.end, 16)

# 2. 加载与CFG提取
proj = angr.Project(filename, load_options={'auto_load_libs': False})
cfg = proj.analyses.CFGFast()
target_func = cfg.functions.get(start_addr)

if not target_func:
    print(f"Error: Function at {hex(start_addr)} not found.")
    exit(1)

# 获取函数内所有的基本块地址
target_blocks = set()
for block in target_func.blocks:
    if start_addr <= block.addr <= end_addr:
        target_blocks.add(block.addr)

# 3. 符号执行 (修复版)
# 建议: 使用 call_state 或给寄存器赋具体值,防止立刻崩溃
state = proj.factory.blank_state(addr=start_addr)
# 示例:如果函数需要参数,可以手动给rdi, rsi赋具体值
# state.regs.rdi = 1  
# state.regs.rsi = 2

simgr = proj.factory.simulation_manager(state)
control_flow = set()
control_flow.add(start_addr)

max_steps = 5000 # 必须加步数限制,防止路径爆炸死循环
step_count = 0

while len(simgr.active) > 0 and step_count < max_steps:
    # 正确的 step 方式
    simgr.step()
    step_count += 1
    
    for active_state in simgr.active:
        # 记录当前执行到的地址
        control_flow.add(active_state.addr)
        # 如果跑飞到函数外面了,就丢弃这条路径
        if active_state.addr < start_addr or active_state.addr > end_addr:
            simgr.move(active_state, simgr.deadended)

# 4. 找出未被执行的块并进行Patch
base_addr = proj.loader.main_object.mapped_base
patched_addrs = []

# 读取原始文件数据
with open(filename, 'rb') as f:
    data = bytearray(f.read())

# 简单的NOP填充函数 (以x86/x64为例)
def write_nops(data, offset, size):
    for i in range(size):
        if offset + i < len(data):
            data[offset + i] = 0x90 # x86 NOP

# 正确的Patch逻辑:不在执行路径中的块,直接NOP
for block_addr in target_blocks:
    if block_addr not in control_flow:
        # 获取该块的大小以计算需要NOP多少字节
        block_size = proj.factory.block(block_addr).size
        file_offset = block_addr - base_addr
        
        patched_addrs.append(hex(block_addr))
        write_nops(data, file_offset, block_size)

# 5. 保存文件
outpath = filename + '_recovered'
with open(outpath, 'wb') as f:
    f.write(data)

print(f'[+] Symbolic execution explored {len(control_flow)} blocks.')
print(f'[+] Patched {len(patched_addrs)} unreachable blocks: {patched_addrs}')
print(f'[+] Saved to: {outpath}')

示例脚本二

python deobfu.py -f 目标程序 -s 函数地址
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 is None:
        # 尝试自动在二进制文件中查找 'main' 函数的符号
        main = proj.loader.find_symbol('main')

        # 如果 'main' 函数也找不到
        if main is 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!')

patch不透明谓词

在汇编中将不透明谓词直接 patch 掉,例如对于该不透明谓词 x_9 , y_10 , 它的 c 表达式为 y_10 >= 10 && ((((_BYTE)x_9 - 1) * (_BYTE)x_9) & 1) != 0。
image.png
我们要做的就是让 mov eax, ds:x_9 改成 mov eax, 0 , 这样就可以做到消除 BCF 的目的
image.png

所以我们可以用 ida python, 通过找到不透明谓词的所有交叉引用的方式来批量修改。

# 去除虚假控制流 idapython脚本
import ida_xref
import ida_idaapi
from ida_bytes import get_bytes, patch_bytes
 
# 将 mov 寄存器, 不透明谓词 修改为 mov 寄存器, 0
def do_patch(ea):
    if get_bytes(ea, 1) == b"\x8B": # mov eax-edi, dword
        reg = (ord(get_bytes(ea + 1, 1)) & 0b00111000) >> 3
        patch_bytes(ea, (0xB8 + reg).to_bytes(1,'little') + b'\x00\x00\x00\x00\x90')
    else:
        print('error')
 
# 不透明谓词在.bss段的范围
start = 0x00428298
end = 0x00428384
 
for addr in range(start,end,4):
    ref = ida_xref.get_first_dref_to(addr)
    print(hex(addr).center(20,'-'))
    # 获取所有交叉引用
    while(ref != ida_idaapi.BADADDR):
        do_patch(ref)
        print('patch at ' + hex(ref))
        ref = ida_xref.get_next_dref_to(addr, ref)
    print('-' * 20)
  • 划定 .bss 一片可疑地址范围;
  • 遍历每个 4 字节全局变量;
  • 找所有数据交叉引用(谁读了这个变量);
  • 被 mov reg, [bss_var] 读取,后面紧跟 test/cmp/jcc 条件跳转;
  • 直接认定:这就是不透明谓词读取指令,直接 patch 成 mov reg,0 废掉分支。

最大风险:

  • 上面脚本假设 所有 对 .bss 变量的 读取 都是为了混淆。如果一段合法代码读取 g_critical_data(mov eax, [g_critical_data]),这个脚本也会(错误地)将其 替换为 mov eax, 0,导致 IDA 错误地优化掉 合法的 代码分支。

将全局变量赋值并将 segment 设为只读

核心原理:

  • 将 .bss 段中的所有变量(包括 y_10, x_9 等)的值都强制 Patch (修补) 成了 2。
  • 将 .bss 段的权限设为 只读。
  • 这就“告诉”了 IDA:“这些变量的值是 常量 2,并且它们永远不会变。”
  • 因此,当 IDA 分析 if (y_10 >= 10 ...) 这样的不透明谓词时,它会执行常量计算:if (2 >= 10 ...) -> if (False ...)。
  • 最终,IDA 的“死代码消除”(DCE)功能会启动,自动移除这个 if (False ...) 对应的虚假分支。

最大风险:

  • 下面脚本假设 .bss 段中 所有变量 都是用于混淆的。如果 .bss 中存在一个合法的全局变量 g_critical_data,IDA 也会(错误地)认为它的值 永远是 2,并可能因此错误地优化掉依赖该变量的 合法代码。

脚本:

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 字节
    print("Patching all dwords in .bss to 2...")
    patched_count = 0
    for ea in range(seg.start_ea, seg.end_ea, 4):
        # 使用 patch_dword 来更新 dword 值为 2
        ida_bytes.patch_dword(ea, 2)
        patched_count += 1
    
    print(f"Patched {patched_count} dwords.")
    
    # 3. 设置段权限为只读
    print("Setting segment permissions to Read-Only (R--)...")
    seg.perm = 0b100  # R-- (Read=4, Write=2, Exec=1)
    
    # 4. 将权限更改写回 IDA 数据库
    ida_segment.update_segm(seg)
    print("Segment permissions updated in the database.")
    
    # 5. 强制 IDA 重新分析 .bss 段的数据
    print("Forcing re-analysis of all items in .bss...")
    for ea in range(seg.start_ea, seg.end_ea, 4):
        # 删除此地址上任何旧的数据定义 (相当于按 U 键)
        ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, 4)
        # 强制 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.")

IDA插件-D-810

image.png|90

当如果发现一些不透明谓词 d810 没有识别到无法去除的话,我们也可以手动添加规则让 D810 进行匹配来消除 BCF。

posted @ 2026-05-17 01:54  MillionMind  阅读(52)  评论(0)    收藏  举报