吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2212|回复: 18
上一主题 下一主题
收起左侧

[Android 原创] 控制流平坦化反混淆(春节红包活动Android高级题)

  [复制链接]
跳转到指定楼层
楼主
circle2 发表于 2024-6-24 13:29 回帖奖励
本帖最后由 circle2 于 2024-6-29 18:46 编辑

前言

春节期间就已经看到红包活动了,但是感觉自己完成时间比较长,当时就没做。后面断断续续做了 Android 这边的题目,还是学习到很多东西的。论坛上有许多红包活动解题的文章,Android的高级题只有一个,所以解的过程还是很有记录和分享的价值的。

这篇文章主要是记录Android高级题控制流平坦化反混淆的思路和一些实现,另外目标并不是高级题flag的获取,因为还未分析完成,所以后续可能会重新编辑本文,进行更新

2024-06-23 首次发布
2024-06-29 补充了一下使用 rizin patch 的代码;以及修正0x1e924部分反混淆失败的问题,主要修改点是下面两个

  1. 第二次探索补全的信息中,可能存在commonBlock,那么跑出来的一些信息就不正确,所以需要三次进行探索
  2. 部分控制块地址上是和真实块直接相连的,但是allocBlock的时候把这些块也分配了,导致后续的 patch 信息不正确
    目前似乎都能正常反混淆了,后面考虑将反混淆的代码放到 github 上,到时候大家就可以本地看效果了

使用的是unidbg,目前可以已经验证可以反混淆的(2024-06-29):

  • checkSn函数 -- 0x1befc
  • base64的函数 -- 0x24290
  • 0x1e924

简单看一下效果(这里是最外层函数0x1befc),左边是进行一些简单patch之后的效果,可以看出是控制流平坦化的特征,右边是进行反混淆之后的结果

样本混淆特征 & 为什么反混淆

样本中控制流平坦化控制块是这样的,前面部分和以前了解过的控制流平坦化差不多,但是跳转地址是动态计算的,所以我们无法看清楚方法的全貌。在 爱破解 2024 春节红包活动WP(全,含 Android 高级题) 文章中对 Android 高级题的处理主要是 1c110 处修改 nzcv 寄存器,1c11c 获取 x10 中的地址,这样就能获取符合 lt 以及不符合 lt 的跳转地址,再进行 patch 即可。

我也这样处理试了一下,我认为有一个缺点,由于还是按照原本执行逻辑进行处理的,只是去获取了另一个分支的情况,所以修改完成后会出现一些未被 patch 的逻辑,经常需要再补。举例:样本中 adb check 如果是 running ,就会直接返回,如果在 adb running 的状态下获取的地址进行 patch,adb 检测通过后的很多逻辑都是无法覆盖的

另外一个原因是这只是进行了简单的 patch,修复完成后,它只是回归到了正常的控制流平坦化的样子了,反编译得到的代码还需要进一步处理,为此我还写了一个小工具获得当前块的下一个块,但是块之间的连接还是需要再进行处理,非常耗时间的

这里我用的是 ghidra, 它的 image base 是 0x100000,所以这里实际偏移是 0x1c0fc, 0x1c100, ...

  0011c0fc 6a 52 82 52     mov        w10, #0x1293
  0011c100 ea 0b b7 72     movk       w10, #0xb85f, LSL #16
  0011c104 1f 01 0a 6b     cmp        w8, w10
  0011c108 0a 0a 80 52     mov        w10, #0x50
  0011c10c 0b 23 80 52     mov        w11, #0x118
  0011c110 6a b1 8a 9a     csel       x10, x11, x10, lt
  0011c114 2a 69 6a f8     ldr        x10, [x9, x10, LSL #0x0]
  0011c118 4a 01 0e 8b     add        x10, x10, x14
  0011c11c 40 01 1f d6     br         x10

反混淆的思路

下面介绍反混淆的思路,反混淆的方法是在 爱破解 2024 春节红包活动WP(全,含 Andro高级题)利用angr符号执行去除控制流平坦化 基础上改进的

探索

这里需要首先介绍一个思路:探索。

将一个方法简化之后,其实就两部分,一是顺序执行,二是分支。如果将分支看作节点,以及节点的 true 和 false 后的执行部分看作两个分支,那么整个方法就是一个二叉树的结构,那么通过强制修改节点位置的 nzcv 寄存器,遍历这个二叉树其实是可能的。正常程序是不需要这样做的,但是这个样本中,由于控制流平坦化的跳转动态计算,无法直接看清全貌,那么就可以通过这样的遍历,建立起方法的全貌,而这样的遍历,我称为探索。

方法看作二叉树其实是简化的,实际可能是个图,因为它是可能存在循环的。另外强制修改 nzcv 寄存器来改变它的分支,可能存在执行错误,比如 read memory failed。

控制流平坦化的反混淆思路

另外这里简单介绍一下控制流平坦化的反混淆思路

  1. 区分出入口块,返回块,预分发器,主分发器,子分发器,真实块
  2. 获取真实块之间的前后关系
  3. 进行patch

反混淆的过程

下面是反混淆的具体步骤,相对重要的是1和7,代码本身只有两个文件:

  1. Test.java:主要是unidbg模拟执行的逻辑,1和5才会涉及到,hook时会调用 InstructionGraph.java 记录指令
  2. InstructionGraph.java: 包含指令合并,控制流平坦化块的区分,以及最终的 patch 输出

1. 初步探索

探索其实就是遍历二叉树的一个过程,每探索一个分支就重新执行一遍程序。之前实现过一次执行,以及仅重复执行方法,均有问题。所以每次探索一个分支就跑一遍 unidbg 的 emulator build,load library,这样处理是最方便的。此外就是探索过程要实际做的事情:获取指令之间的前后顺序

1.1 遍历的整体逻辑

path是一个Node List,node中包含的信息是 address 和 cond,表示涉及判断,即获取 NZCV 寄存器指令的地址,以及当时应选择的指令条件满足的状态,true 还是 false

  1. 状态重置。这里是探索的代码以及 unidbg 的一些代码混在一块了,所以每次探索前需要将状态重置。这是因为之前尝试仅重复执行方法遗留的代码框架,就没做大的改动,理论上分开是更好的
  2. 进行当前 path 的探索。path最初是空的,我们也没有提前知道相应指令的地址,走指定的 path,那么这个 path 应该有多长,这些问题都是未知的。所以我这里采取的方法是,执行方法时,尽可能的往前探索未知的节点,未知的节点条件都走 true
  3. 其他分支的探索。第二步探索之后,会新增一些为true的节点,将末尾转化为false,就可以探索令一条分支了,下面演示以下

入参path:node1-true, node2-false, node3-false

单次探索结果,也相当与这个新增全 true 的分支走过去了:node1-true, node2-false, node3-false, node4-true, node5-true, node6-true

那么需要探索的其他分支是:

explorer(node1-true, node2-false, node3-false, node4-true, node5-true, node6-false)

explorer(node1-true, node2-false, node3-false, node4-true, node5-false)

explorer(node1-true, node2-false, node3-false, node4-false)

再往前就归属于不属于传入 path 的范畴了,所以当前 path 的探索就结束了

private void explorer(List<Node> path) {
    // 每次探索前的准备
    String origin = path.toString();
    int originSize = path.size();
    this.currentPath = path;
    this.curNodeIndex = 0;
    this.accessedNodeInCommonBlock = new HashSet<>();
    this.explorerStatus = Status.RUNNING;
    this.accessedInstructions = new ArrayList<>();
    this.accessedRealInstructions = new ArrayList<>();
    this.accessedStartInstructions = new ArrayList<>();

    // 进行单次探索
    try {
        this.init();
        this.checkSn();
    } catch (Exception ignored) {}

    try {
        this.destory();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

    System.out.println("origin path: " + origin);
    System.out.println("result path: " + this.currentPath);
    System.out.println("---------------------");

    // 进行其他分支的探索
    List<Node> tmpPath = this.currentPath;
    for (int i = tmpPath.size() - 1; i >= originSize; i--) {
        if (tmpPath.get(i).cond) {
            List<Node> newPath = new ArrayList<>();
            for (int j = 0; j < i; j++) {
                newPath.add(new Node(tmpPath.get(j).address, tmpPath.get(j).cond));
            }
            newPath.add(new Node(tmpPath.get(i).address, false));
            explorer(newPath);
        }
    }
}

class Node {
    long address;
    boolean cond;

    public Node(long address, boolean cond) {
        this.address = address;
        this.cond = cond;
    }

    @Override
    public String toString() {
        return String.format("%x %s", address, cond);
    }
}

1.2 新分支的探索

这一块是 emulator.getBackend().hook_add_newCodeHook() 中的内容,每次碰到 nzcv 节点进行如下处理:

  • 获取 cpsr,指令中的cond string,然后得到当前是符合条件还是不符合条件
  • 如果 index 还在 path 内,就按照 path 中的 cond 修改
  • 如果不再 path 内,则修改为 true,同时创建新的节点添加到 path 中
String condStr = getCond(ins);
Cpsr cpsr = Cpsr.getArm64(backend);
boolean cond = isCondSuc(condStr, cpsr);
if (curNodeIndex < currentPath.size() && currentPath.get(curNodeIndex).address != offset) {
    System.err.println(String.format("Unexpected address: %X %s %s", address, ins, currentPath));
}
if (curNodeIndex < currentPath.size() && currentPath.get(curNodeIndex).cond != cond) {
    toggleCpsrStatus(condStr, cpsr);
}
if (curNodeIndex >= currentPath.size()) {
    if (!cond) {
        toggleCpsrStatus(condStr, cpsr);
    }
    // 新节点处理true的情况
    currentPath.add(new Node(offset, true));
}

1.3 记录指令之间的连接

在hook方法中:

if (!accessedInstructions.isEmpty()) {
    Instruction last = accessedInstructions.get(accessedInstructions.size() - 1);
    instructionGraph.addLink(last, ins);
}
accessedInstructions.add(ins);

在 InstructionGraph 中,这是创建用于处理指令图的类,同时在里面做了后续的控制流平坦化的相关处理

这部分应该比较简单,就是将 node 和 link 储存起来。这里通过 address 来判断是否重复,因为同一 address 的 Instruction 判断好像是不等的,所以 Set 处理不了

private Set<Instruction> nodes = new HashSet<>();
private Map<Long, List<Instruction>> toMap = new HashMap<>();
private Map<Long, List<Instruction>> fromMap = new HashMap<>();

public void addLink(Instruction ins, Instruction to) {
    addNode(ins);
    addNode(to);
    putLinkToMap(toMap, ins.getAddress(), to);
    putLinkToMap(fromMap, to.getAddress(), ins);
}

private void addNode(Instruction ins) {
    boolean include = false;
    for (Instruction i : nodes)
        if (i.getAddress() == ins.getAddress()) {
            include = true;
            break;
        }
    if (!include)
        nodes.add(ins);
}

private void putLinkToMap(Map<Long, List<Instruction>> map, long address, Instruction ins) {
    if (!map.containsKey(address)) {
        map.put(address, new ArrayList<>());
    }

    boolean include = false;
    for (Instruction i : map.get(address))
        if (i.getAddress() == ins.getAddress()) {
            include = true;
            break;
        }
    if (!include)
        map.get(address).add(ins);
}

1.4 避免死循环

需要在两个层面避免

  1. 避免 path 中已经出现过的节点再次出现,重复出现直接退出
  2. 避免节点之间的 instruction 出现死循环,重复出现直接退出

将PC改成一个不正确的数字,就会退出,我这里是改成了-1

下面是 CodeHook 中的代码

// 将PC随便设置个数字,就会异常退出
if (explorerStatus != Status.RUNNING) {
    emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_PC, -1);
    return;
}

// 判断之前的指令是否出现过
for (Instruction i : accessedInstructions)
    if (i.getAddress() == address) {
        // 死循环的需要正确串起来,所以这里需要加ins
        accessedInstructions.add(ins);
        explorerStatus = Status.INFINITE_LOOP;
        return;
    }
// 添加新遍历过的指令
accessedInstructions.add(ins);

// 判断节点是否重复
short[] read = ins.regsAccess().getRegsRead();
if (hasNZCV(ins, read)) {
    for (int i = 0; i < currentPath.size(); i++)
        if (i != curNodeIndex && currentPath.get(i).address == offset) {
            explorerStatus = Status.INFINITE_LOOP;
            return;
        }
}

1.5 避免重复探索

如果一个节点 true 和 false 均探索完成,那么就没有必要再继续探索了,下次碰到直接退出即可

这个可以极大的加快探索的速度,但是可能会引起探索不够充分的问题,代码比较乱,暂时不贴

2. 合并块

合并指令为块,其实相对简单,通过遍历之前存下的指令先后顺序即可创建出来

  1. 首先找到entry,即没有from的指令
  2. 设置一个栈,储存待处理的指令(这些指令会是每个块的首个指令)
  3. 从栈中取指令,判断是否可以 merge,可以 merge 的标准是,指令之间地址是相连的,而且指令1的 to 只有指令2,指令2的 from 只有指令1
  4. 直到找到不可 merge 的,将刚刚遍历过去的合在一起,放置到 mergeNodes 中
  5. 同时将最后的节点的所有 to 放入栈中

此外,需要一个 accessed 来保证不重复,确保不会陷入死循环中

// Set还是List其实没有关系,最终都是通过遍历判断地址是否相同来保证唯一的
private Set<List<Instruction>> mergedNodes = new HashSet<>();
public void findEntry() {
    for (Instruction i : nodes) {
        if (i.getAddress() == 0x1befc + moduleBase) {
            System.out.println(fromMap.containsKey(i.getAddress()));
        }
        if (!fromMap.containsKey(i.getAddress())) {
            entry = i;
        }
    }
    if (entry == null) {
        throw new RuntimeException("entry is null");
    }
}

public void mergeNodes(boolean isFirst) {
    Set<Long> accessed = new HashSet<>();
    Stack<Instruction> pending = new Stack<>();
    // 前一个区块的最后一个地址对应的node,相当于包含了link的信息
    List<Link> graphLinkMap = new ArrayList<>();
    pending.push(entry);
    accessed.add(entry.getAddress());
    while (!pending.empty()) {
        Instruction ins = pending.pop();
        List<Instruction> from = fromMap.get(ins.getAddress());

        List<Instruction> mergedNode = new ArrayList<>();
        mergedNode.add(ins);
        while (canMerge(ins)) {
            ins = toMap.get(ins.getAddress()).get(0);
            mergedNode.add(ins);
        }

        mergedNodes.add(mergedNode);

        if (toMap.get(ins.getAddress()) != null) {
            for (Instruction i : toMap.get(ins.getAddress())) {
                if (!accessed.contains(i.getAddress())) {
                    pending.push(i);
                    accessed.add(i.getAddress());
                }
            }
        }
    }
}

private boolean canMerge(Instruction ins) {
    long address = ins.getAddress();
    List<Instruction> to = toMap.get(address);
    if (to == null || to.size() != 1) {
        return false;
    }
    // 如果下一节点的from只有当前这个元素,认为可合并
    return fromMap.get(to.get(0).getAddress()).size() == 1 && to.get(0).getAddress() <= ins.getAddress() + 4;
}

3. 区分控制流平坦化中的各种块

getPreDispatcher是获取预分发块,它的特征是 from 的节点最多的

然后从 preDispather 的后继块中获取符合如下特征的的几个指令,那么这里的 w8 就是 flag reg 了

mov   w10, #0x1293
movk  w10, #0xb85f, LSL #16
cmp   w8, w10

遍历所有 mergedNodes,找出所有符合上述特征的几个指令,这里需要指定 cmp 中存在 flag reg(这里是 w8),符合特征的块就是分发器

不过后面我注意到以 br 结尾的块其实都是控制块,不过不能完全确认,所以之前的特征方式也保留了,如有不符合的但以 br 结尾的打 log

预分发器、主分发器、子分发器都认为是控制块,那么剩余的块就都是真实块了

private Set<List<Instruction>> controlNodes = new HashSet<>();
private Set<List<Instruction>> realNodes = new HashSet<>();

public void splitBlockToControlAndReal(boolean isFirst) {
    // 由于补充了新的分块,需要进行重置了
    controlNodes = new HashSet<>();
    realNodes = new HashSet<>();

    this.preDispather = getPreDispather();
    controlNodes.add(preDispather);

    for (Instruction ins : toMap.get(preDispather.get(preDispather.size() - 1).getAddress())) {
        List<Instruction> toBlock = getMergeNodeByStart(ins.getAddress());
        controlNodes.add(toBlock);
        assert toBlock != null;
        List<Instruction> matched = fuzzyMatchDispatch(toBlock);
        if (matched != null) {
            String otherReg = matched.get(0).getOpStr().split(",")[0];
            String[] regs = matched.get(2).getOpStr().split(",");
            reg = otherReg.equals(regs[0].trim())
                ? regs[1].trim()
                : regs[0].trim();
            System.out.printf("Find flag reg, %x reg: %s, ins: %s\n", ins.getAddress(), reg, matched);
        }
    }

    if (reg == null)
        throw new RuntimeException("flag reg is null");
    System.out.println("flag reg: " + reg);
    System.out.println(mergedNodes.size());

    for (List<Instruction> block: mergedNodes) {
        List<Instruction> matched = exactMatchDispatch(block, reg);
        if (matched != null) {
            controlNodes.add(block);
        } else if (block.get(block.size() - 1).getMnemonic().equals("br")) {
            controlNodes.add(block);
            System.err.println("this block is end with br, may be a control block " + Long.toHexString(block.get(0).getAddress() - moduleBase));
        }
    }

    for (List<Instruction> block: mergedNodes) {
        if (!controlNodes.contains(block)) {
            realNodes.add(block);
        }
    }
    System.out.println("control block " + controlNodes.size());
    System.out.println("real block " + realNodes.size());
}

4. 获取真实块的合集

我所看到的控制流平坦化反混淆的文章中,真实块是单个的,处理起来会比较简单,但实际真实块的情况会更复杂,这个样本中就是如此,会存在多个真实块,那么其实要获取在控制块和预分发块之间的真实块的合集,一个控制块就应有这样一个合集(后面发现有例外,但暂不考虑)

另外真实块合集之间并不是完全切割开来的,有一部分共用的块,但这些块都是在预分发器前面

这一步的目标是:

  1. 获取真实块的合集,其实主要是获取这个合集中最初始的指令
  2. 获取真实块合集的共用块
  3. 判断合集的后继合集是一个还是两个,即判断是顺序执行还是有分支

下面是 InstrutionGraph 中的代码,这里主要是调用 mergeOneRealArea 去获取真实块合集,在 mergeOneRealArea 中会记录 block 的次数

  1. 获取 entry 开头的真实块合集,从方法入口一直到分发器前面都是合集范围
  2. 所有真实块中,如果 from 是控制块,那么它也是一个合集的开头
  3. 如果 commonBlock 中值超过 1 的,那么就是共用块。这里顺便做了一个验证,确认之前的判断合理,共用块要不是预分发器的前驱,要不就是其他共用块的前驱
private Map<Long, Integer> commonBlock = new HashMap<>();

// 代码中都写的是 real area, block area这种,也就是上述的合集
public void mergeRealArea() {
    mergeOneRealArea(entry, to -> {
            for (List<Instruction> block : controlNodes)
                if (block.get(0).getAddress() == to.getAddress())
                    return true;
            return false;
        });

    for (List<Instruction> block : realNodes) {
        List<Instruction> from = fromMap.get(block.get(0).getAddress());
        if (from == null) {
            continue;
        }
        for (Instruction ins : from) {
            // 如果from block是控制块,那么就算是area start,才进行后续的处理
            List<Instruction> lastBlock = getMergeNodeByEnd(ins.getAddress());
            if (!controlNodes.contains(lastBlock)) {
                continue;
            }
            mergeOneRealArea(block.get(0), to -> preDispather.get(0).getAddress() == to.getAddress());
        }
    }

    for (long key : commonBlock.keySet())
        if (commonBlock.get(key) != 1) {
            // commonBlock
            // commonBlock目前认为会是preDispatcher前驱,或者是另一个commonBlock前驱
            List<Instruction> block = getMergeNodeByStart(key);
            List<Instruction> to = toMap.get(block.get(block.size() - 1).getAddress());
            if (to == null || to.size() != 1)
                throw new RuntimeException("unexpected common block form");
            long address = to.get(0).getAddress();
            if (!commonBlock.containsKey(address) && preDispather.get(0).getAddress() != address)
                throw new RuntimeException("unexpected common block form");
        }
}

下面看看 mergeOneRealArea,这里面主要是去遍历块直到末尾,类似之前第二步--合并块的处理逻辑,只是这次直接处理块

  • 遍历块时,如果块中存在指令满足 "csel {flagReg}" ... 的模式,则记其为 key ins,这个合集是会产生分支的合集。存下来它的 start 和 key ins 的地址,后续要用到
  • 遍历块时,将信息储存到commonBlock中,用于后续的处理(大于1即为共用块)
  • 将块的信息放在 blockAreaStart 中  this.blockAreaStart.put(ins.getAddress(), area);
private Map<Long, Integer> commonBlock = new HashMap<>();
private Map<Long, Set<Long>> blockAreaStart = new HashMap<>();
// 因为keyIns可能在common block中,所以start to keyins,而不是反过来
private Map<Long, Long> startMapTokeyIns = new HashMap<>();

interface OutBlockJudge {
    boolean isOutBlock(Instruction to);
}

// ins 是合集的开头
// outBlockJudge 是用于判断一个 block 是否是末尾
private void mergeOneRealArea(Instruction ins, OutBlockJudge outBlockJudge) {
    Set<Long> area = new HashSet<>();

    Set<Long> accessed = new HashSet<>();
    Stack<Instruction> pending = new Stack<>();  // 简化解构,只存block的首个ins
    accessed.add(ins.getAddress());
    pending.add(ins);
    int outBlockNum = 0;
    Instruction assign = null;

    // 遍历整个block area
    while (!pending.empty()) {
        Instruction top = pending.pop();
        List<Instruction> b = getMergeNodeByStart(top.getAddress());

        for (Instruction i : b) {
            area.add(i.getAddress());
            if (i.toString().startsWith("csel " + reg))
                assign = i;
        }

        // 计算block的命中次数,获取commonBlock
        int times = commonBlock.getOrDefault(top.getAddress(), 0);
        commonBlock.put(top.getAddress(), times + 1);

        List<Instruction> toList = toMap.get(b.get(b.size() - 1).getAddress());
        if (toList != null)
            for (Instruction to : toList)
                if (!accessed.contains(to.getAddress())) {
                    if (!outBlockJudge.isOutBlock(to)) {
                        pending.push(to);
                        accessed.add(to.getAddress());
                    } else {
                        // 这是顺便借个地方统计以下出口的数量
                        outBlockNum ++;
                    }
                }
    }

    this.blockAreaStart.put(ins.getAddress(), area);

    if (assign != null) {
        System.out.println("assign instruction: " + Long.toHexString(assign.getAddress()) + " " + assign);
        startMapTokeyIns.put(ins.getAddress(), assign.getAddress());
    }
    if (outBlockNum > 1) {
        System.out.println("the multi out block is:");
        for (Instruction i : getMergeNodeByStart(ins.getAddress())) {
            System.out.println(Long.toHexString(i.getAddress() - moduleBase) + " " + i);
        }
        throw new RuntimeException("outBlockNum is not one: " + Long.toHexString(ins.getAddress() - moduleBase));
    }
}

5. 二次探索

在理清楚块之间的信息后,需要再进行二次探索,这次探索的主要目标是获取真实块合集之间的联系,所以仅处理在真实块中的指令

另外还有一个目标,是补充块信息。在初步探索完成后,我发现有些块有缺失,原因是缺少前置信息。因为第一次探索时,所有的读取 nzcv 的指令都被当作节点,那么控制块中也有指令,而我配置了节点不重复探索,这样一次探索的范围大概就是一条分支,只有一个真实区域,如果这个真实区域依赖前置数据,就会出错,有些时候 unidbg 会强制退出,导致后续指令都获取不全。

二次探索的一个目标是为了补全之前缺失的块,但是这个块可能是commonBlock,这就是0x1e924没有成功的一个原因,所以针对部分情况需要第三次探索,每次探索后均进行信息补全,区分块,重新获取commonBlock,操作是类似的,这里就不修改这篇文章

为了实现这两个目标,需要在之前 accessedInstructions 的基础上再加一个 accessedRealInstructions 和 accessedStartInstructions

  • accessedInstructions 依旧保持,这样才能补全后续指令,需要是补全的块和预分发块的联系根据它得到
  • accessedRealInstructions 是为了遍历真实块的,是第二次探索过程中的主力
  • accessedStartInstructions 是第二次探索的结果,用于联系真实块合集,它只有走到合集开头的指令时才有作用

这一块是 emulator.getBackend().hook_add_newCodeHook() 中的内容,第二次探索前先设置一个 this.onlyRealBlock = true;,表明是第二次探索

// 连接上一条指令
if (onlyRealBlock && instructionGraph.isInRealBlock(address) && !accessedRealInstructions.isEmpty()) {
    Instruction last = accessedRealInstructions.get(accessedRealInstructions.size() - 1);
    instructionGraph.addRealLink(last, ins);
}
if (onlyRealBlock && instructionGraph.isBlockAreaStartOrEntry(address) && !accessedStartInstructions.isEmpty()) {
    Instruction last = accessedStartInstructions.get(accessedStartInstructions.size() - 1);
    // 这个稍微有点特殊,要加 keyInsCondSuc,如果合集后续是分支,就会用到
    instructionGraph.addBlockAreaLink(last, ins, keyInsCondSuc);
}

// 如果是第二次,判断节点的指令是否出现重复访问,就需要用到 accessedRealInstructions
for (Instruction i : onlyRealBlock ? accessedRealInstructions : accessedInstructions)
    if (i.getAddress() == address) {
        // 死循环的需要正确串起来,所以这里需要加ins
        accessedInstructions.add(ins);
        if (onlyRealBlock && instructionGraph.isInRealBlock(address)) {
            accessedRealInstructions.add(ins);
        }
        if (onlyRealBlock && instructionGraph.isBlockAreaStartOrEntry(address)) {
            accessedStartInstructions.add(ins);
        }
        // commonBlock可以访问多次,所以这里需要把它从正常判断里面剔除
        if (!instructionGraph.isInCommonBlock(address)) {
            explorerStatus = Status.INFINITE_LOOP;
            return;
        }
        break;
    }

// 添加指令
accessedInstructions.add(ins);
if (onlyRealBlock && instructionGraph.isInRealBlock(address)) {
    accessedRealInstructions.add(ins);
}
if (onlyRealBlock && instructionGraph.isBlockAreaStartOrEntry(address)) {
    accessedStartInstructions.add(ins);
}

这里也是 CodeHook 中的内容,处理会读取 nzcv 的指令,如果是第二次,就只处理在真实块中的指令

另外第二次时,对于在共用块中的指令,需要以 {startAddr}-{nzcvAddr} 做 key,保证可以共用块被多次遍历,但是一个合集下只遍历一次

如果命中 key ins 的话,需要知道 cond 是 true 还是 false,用于后续的连接,以及 patch

if (hasNZCV(ins, read) && (!onlyRealBlock || instructionGraph.isInRealBlock(address))) {
    // 由于第二遍探索,有一些commonBlock中的应该被遍历多遍,结果只被遍历一遍,特别排除一下
    if (!instructionGraph.isInCommonBlock(address)) {
        for (int i = 0; i < currentPath.size(); i++)
            if (i != curNodeIndex && currentPath.get(i).address == offset) {
                explorerStatus = Status.INFINITE_LOOP;
                return;
            }
    } else {
        Instruction areaStart = accessedStartInstructions.get(accessedStartInstructions.size() - 1);
        String key = areaStart.getAddress() + "-" + address;
        if (accessedNodeInCommonBlock.contains(key)) {
            explorerStatus = Status.INFINITE_LOOP;
            return;
        }
        accessedNodeInCommonBlock.add(key);
    }

    if (instructionGraph.isKeyIns(address)) {
        System.out.println("is key ins: " + Long.toHexString(offset));
        keyInsCondSuc = isCondSuc(condStr, cpsr);
    }
}

6. 合并块,对2进行补充

用二次探索的结果扩充之前合并块的结果,大体上可以复用之前的代码,主要注意 !isFirst 的逻辑,在产生 mergeNode 后,如果发现之前有记录,就直接删掉,用新的即可

另外的一些工作就是绘图,这样就会有一个相对直观的结果,其他地方也可以做这件事,比如之前区分各种块的时候,在图上标出了控制块,以及这里合并完成之后,可以将真实块之间连接起来

public void mergeNodes(boolean isFirst) {
    Set<Long> accessed = new HashSet<>();
    Stack<Instruction> pending = new Stack<>();
    // 前一个区块的最后一个地址对应的node,相当于包含了link的信息
    List<Link> graphLinkMap = new ArrayList<>();
    pending.push(entry);
    accessed.add(entry.getAddress());
    while (!pending.empty()) {
        Instruction ins = pending.pop();
        List<Instruction> from = fromMap.get(ins.getAddress());

        List<Instruction> mergedNode = new ArrayList<>();
        mergedNode.add(ins);
        while (canMerge(ins)) {
            ins = toMap.get(ins.getAddress()).get(0);
            mergedNode.add(ins);
        }

        if (!isFirst) {
            MutableNode node = Factory.mutNode(Label.html("<table border='0'>" + sb.toString() + "</table>"))
                .add(Shape.RECT, Style.ROUNDED);
            if (from != null) {
                for (Instruction i : from) {
                    graphLinkMap.add(new Link(i.getAddress(), node));
                }
            }
            graph.add(node);
            graphNodeMap.put(ins.getAddress(), node); // 记录,用于后续 graph 的链接
        }

        if (!isFirst)
            for (List<Instruction> block : mergedNodes)
                if (block.get(0).getAddress() == mergedNode.get(0).getAddress()) {
                    mergedNodes.remove(block);
                    break;
                }
        mergedNodes.add(mergedNode);

        if (toMap.get(ins.getAddress()) != null) {
            for (Instruction i : toMap.get(ins.getAddress())) {
                if (!accessed.contains(i.getAddress())) {
                    pending.push(i);
                    accessed.add(i.getAddress());
                }
            }
        }
    }

    if (!isFirst) {
        for (Link link : graphLinkMap) {
            MutableNode node = graphNodeMap.get(link.address);
            node.addLink(link.node);
        }
    }
}

7. 获取 patch 信息

到了最重要的 patch 环节,在反混淆控制流平坦化的文章中,真实块只有一个,patch的过程其实相对简单,这个样本中会比较复杂,主要原因是存在共用块,导致不能直接进行 patch

所以除了获取真实块合集之间的联系之外,最重要的是处理 commonBlock,它每多触发一次,就需要多复制一份出来。可以复制到哪呢?控制流平坦化 patch 的时候,会将所有的控制块 patch 为 nop,所以我们能将这些控制块利用起来,复制到控制块即可。

其实 commonBlock 中的内容不一定重要,但是就怕万一,所以尽可能做到还原

7.1 patchAll

这个是整体patch的方法,和前面获取真实块合集的外层逻辑是差不多的,从合集的start开始遍历,调用 patchInstructionArea 进行真正的 patch,传入了一个 HashMap (patchInfo),patchInstructionArea 会将 patch 的地址以及 patch 的指令放在这个 HashMap 中

最后将控制块中未被使用的地址都 patch 成 nop,然后输出信息

public void patchInstructionAll() {
    // 地址和要修改为的指令
    // 1. 旧跳转指令的patch
    // 2. commonBlock的复制
    // 所以用过的 controlNode 地址是会在这里面保存的
    Map<Long, String> patchInfo = new HashMap<>();

    patchInstructionArea(patchInfo, entry, to -> {
            for (List<Instruction> block : controlNodes)
                if (block.get(0).getAddress() == to.getAddress())
                    return true;
            return false;
        });

    for (List<Instruction> block : realNodes) {
        List<Instruction> from = fromMap.get(block.get(0).getAddress());
        if (from == null) {
            continue;
        }
        for (Instruction ins : from) {
            // 如果from block是控制块,那么就算是area start,才进行后续的处理
            List<Instruction> lastBlock = getMergeNodeByEnd(ins.getAddress());
            if (!controlNodes.contains(lastBlock)) {
                continue;
            }
            patchInstructionArea(patchInfo, block.get(0), to -> preDispather.get(0).getAddress() == to.getAddress());
        }
    }

    for (List<Instruction> block : controlNodes)
        for (Instruction ins : block)
            if (!patchInfo.containsKey(ins.getAddress() - moduleBase))
                patchInfo.put(ins.getAddress() - moduleBase, "nop");

    for (long key : patchInfo.keySet())
        System.out.println("patch: [" + Long.toHexString(key) + "] " + patchInfo.get(key));
}

7.2 patch 一个真实块合集

这个是 patch 的逻辑,后面有三部分空出来了,下面再进行介绍,这里先看以下对真实块合集的遍历,和之前获取真实块合集类似,要遍历合集中的每个真实块

private void patchInstructionArea(Map<Long, String> patchInfo, Instruction start, OutBlockJudge outBlockJudge) {
    int addition = condBlockAreaMap.containsKey(start.getAddress()) ? 1 : 0;
    Set<Long> area = new HashSet<>();

    // block 原有的到新的映射,在一次area中一个block只会被复制一次
    Map<Long, Long> copyBlock = new HashMap<>();
    Set<Long> accessed = new HashSet<>();
    Stack<Instruction> pending = new Stack<>();  // 只存block的首个ins
    accessed.add(start.getAddress());
    pending.add(start);
    // 遍历整个block area
    while (!pending.empty()) {
        Instruction top = pending.pop();
        List<Instruction> b = getMergeNodeByStart(top.getAddress());
        Instruction lastIns = b.get(b.size() - 1);
        long lastInsAddress = lastIns.getAddress();
        List<Instruction> toList = toMap.get(lastInsAddress);

        for (Instruction i : b)
            area.add(i.getAddress());

        if (toList != null) {

            // [1] 复制 commonBlock

            for (Instruction to : toList) {

                // [2] 处理 to 是 commonBlock 的块,会在这里面分配新区域用来复制commonBlock

                if (!accessed.contains(to.getAddress()))
                    if (outBlockJudge.isOutBlock(to)) {

                        // [3] 结尾块,这里要 patch 跳转指令,要跳转到下一个真实区块的合集

                    } else {
                        pending.push(to);
                        accessed.add(to.getAddress());
                    }
            }
        }
    }
}

7.2.1 复制 commonBlock

这部分相对简单,因为关键逻辑在7.2.2中,这里只是将commonBlock复制到新的区块中。7.2.2中会进行区块的分配,然后将原区块的首地址和新区块的首地址放在 copyBlock 中,所以只要 copyBlock 中有,开始复制就对了,然后将新分配的地址和相应的指令存放在 patchInfo 中

这里需要注意的是,之前遍历得到的指令,如果有跳转,是加了 module base 的,所以这里将 #0x400 给替掉,这个其实有点风险,可能正常逻辑里面也有这个,暂时做的粗糙一点

if (commonBlock.getOrDefault(top.getAddress(), 0) > 1 && copyBlock.containsKey(top.getAddress())) {
    // 遍历到commonBlock,此时copyBlock中已经包含了它的信息
    List<Instruction> common = getMergeNodeByStart(top.getAddress());
    long copyTo = copyBlock.get(top.getAddress());
    long base = 0;
    for (int i = 0; i < common.size(); i++, base ++) {
        if (i != 0 && copyBlock.containsKey(common.get(i).getAddress())) {
            copyTo = copyBlock.get(common.get(i).getAddress());
            base = 0;
        }
        copyBlock.put(common.get(i).getAddress(), copyTo + base * 4L);
        patchInfo.put(copyTo + i * 4L - moduleBase, common.get(i).toString().replaceAll("#0x400([0-9a-fA-F]+)", "#0x$1"));
    }
}

7.2.2 分配区块用于后续 commonBlock 的复制

其实可以走到 commonBlock 时再进行分配,不过写在这里相对方便一些,但是相对麻烦,还得判断 from 是否是当前区块

当碰到下一个区块是 commonBlock 时,就需要分配了。但是有一个例外,就是 commonBlock 是在当前块后面,那么当前块尾部就不是跳转指令,这个就不进行复制,让它保持原状即可。也就是说有一个合集会直接使用 commonBlock,其他合集就要进行复制。

allocBlock方法分配新的区块,新区块的大小是 commonBlock 的大小加一点额外空间

  1. 如果当前真实区块有分支,那么再留一条指令,用于进行条件的跳转
  2. 如果 commonBlock 是结尾不是分支语句,那么它和下一个块就是直接连接的,那么要留一条分支指令,用于跳转后续块

在分配了新的区块后,将映射信息存放在 copyBlock 中,同时将当前块的最后一个跳转指令进行 patch,使它跳转到新分配的区块

int addition = condBlockAreaMap.containsKey(start.getAddress()) ? 1 : 0;

// ...

// commonBlock的格式在前面mergeRealArea判断过了
// 如果下个block是commonBlock,并且还不是紧接着的,那么需要复制
if (commonBlock.getOrDefault(to.getAddress(), 0) > 1 && copyBlock.getOrDefault(lastInsAddress, lastInsAddress) + 4 != to.getAddress()) {
    List<Instruction> common = getMergeNodeByStart(to.getAddress());
    // 由于至少要包含一个指令以及一个跳转指令,所以必须
    // + 1 是为了避免一些问题(比如common下一个刚好是preDispatcher)
    // 或者是连续的两个common块,复制出来之后前一个要加跳转,等等
    // 总结是,如果不以b结尾,应该要改,所以要提前预备一个
    int brAddition = common.get(common.size() - 1).getMnemonic().equals("b") ? 0 : 1;
    Instruction alloc = allocBlock(common, common.size() + addition + brAddition, patchInfo, copyBlock);
    if (alloc == null)
        throw new RuntimeException("No enough controlNode " + Long.toHexString(common.get(0).getAddress()));
    else {
        // 连续common块,复制出来,需要patch,会走到这里面,但实际它是不以 b 指令结尾的,这里根据原指令筛除
        if (!lastIns.getMnemonic().equals("b") && lastInsAddress + 4 != to.getAddress())
            throw new RuntimeException("Unexpected last ins" + Long.toHexString(lastInsAddress) + " " + lastIns);
        // 如果是copy的block,那么patch地址是 copyBlock.get(lastInsAddres),否则就是lastInsAddress
        // 所以用 copyBlock.getOrDefault(lastInsAddress, lastInsAddress) 即可
        boolean needNewIns = !lastIns.getMnemonic().equals("b");
        patchInfo.put(copyBlock.getOrDefault(lastInsAddress, lastInsAddress) + (needNewIns ? 4 : 0) - moduleBase,
                      String.format("b %d", alloc.getAddress() - moduleBase));
        copyBlock.put(to.getAddress(), alloc.getAddress());
    }
}

7.2.3 尾块的处理

这里先简单说一下之前二次探索存的真实块之间的 link,如果不是keyIns中,那么就代表没有分支,那么直接存在 flowBlockAreaMap 中,如果在keyIns中,有分支,那么就存在 condBlockAreaMap 中,如果 cond 是true,就存在第一个,false就存在第零个

public void addBlockAreaLink(Instruction ins, Instruction to, boolean cond) {
    // TODO: 这里是为了筛除内部循环,但也可能是在控制流平坦化控制下的循环(如果发现 link 的有问题,有缺失,可以处理)
    if (ins.getAddress() == to.getAddress()) {
        System.out.printf("flow block area link error, link self %x -> %x\n", ins.getAddress(), to.getAddress());
        return;
    }
    if (startMapTokeyIns.containsKey(ins.getAddress())) {
        // 从 putLinkToMap 复制上来的,cond的这个有点不一样
        long fromAddress = ins.getAddress();
        if (!condBlockAreaMap.containsKey(fromAddress)) {
            List<Instruction> tmp = new ArrayList<>(2);
            tmp.add(null);
            tmp.add(null);
            condBlockAreaMap.put(fromAddress, tmp);
        }
        condBlockAreaMap.get(fromAddress).set(cond ? 1 : 0, to);
    } else {
        putLinkToMap(flowBlockAreaMap, ins.getAddress(), to);
    }
}

下面开始看尾块的处理逻辑,首先要找到跳转地址,这个无论是不是分支,都是会有一个跳转地址的,condBlockAreaMap中的就取false的那个跳转地址

然后是针对不同情况需要找不同地址进行 patch,可能是当前块后续的指令,也可能是在复制块中,具体的情况可以看下面的注释

处理完成一个跳转地址,需要处理有分支情况的另一个跳转地址

  • 首先找到当前区里的keyIns,获取keyIns的条件,即 csel w10, w11, w10, eq 中的 eq,这样就可以得到 patch 的指令了,即 b.{cond} {trueAddr}。这里的 trueAddr 是 condBlockAreaMap 中true的跳转地址
  • 从尾部的指令往前遍历,直到找到 keyIns,它们之间的 ins 假如是 ins-1, keyIns, ins1, ins2, ..., ins-end-1, ins-end,那么需要将它移动位置,修改为 ins-1, ins1, ins2, ..., ins-end-1, keyIns, ins-end,新的序列中 keyIns 的提防就被 patch 为刚刚的 b.{cond} {trueAddr}
if (outBlockJudge.isOutBlock(to)) {
    // 对于最后一个block,即out block(之前已经判断过一个area只有一个out block)
    // 首先对于所有的要进行patch的,要先找到一个用于patch跳转的地址
    // 1. 尾部直接存在b指令的,patch这个b即可
    // 2. 没有b指令,entry块,而且to是下一条指令,且to是controlBlock,那么以下一条指令作为patch
    // 3. 没有b指令,to是下一条指令,且to是preDispatcher,且当前是复制块,那么在尾部的下一条指令patch
    // 4. 没有b指令,to是下一条指令,且to是preDispatcher,那么在尾部的下一条指令patch
    // 剩下的是对于cond情况的真实块集合来说,需要一直往上找,直到在 startMapTokeyIns,过程中的指令暂存以下,然后开始 patch 这些指令(调整顺序)
    System.out.println("last ins: " + lastIns);

    long nextAddress = -1;
    if (flowBlockAreaMap.containsKey(start.getAddress()))
        nextAddress = flowBlockAreaMap.get(start.getAddress()).get(0).getAddress();
    else if (condBlockAreaMap.containsKey(start.getAddress()))
        // b.{cond} {trueAddr}
        // b        {falseAddr}
        nextAddress = condBlockAreaMap.get(start.getAddress()).get(0) == null ? -1 : condBlockAreaMap.get(start.getAddress()).get(0).getAddress();
    if (nextAddress == -1)
        // throw new RuntimeException("next address is null " + Long.toHexString(start.getAddress()));
        continue;

    String patchFlowInstruction = String.format("b %d", nextAddress - moduleBase);
    Instruction willPatchCondInstruction = null;
    if (lastIns.getMnemonic().equals("b")) {
        // 这里是假定block大小是大于1的,碰到等于1的再想办法处理
        willPatchCondInstruction = b.get(b.size() - 2);
        patchInfo.put(copyBlock.getOrDefault(lastInsAddress, lastInsAddress) - moduleBase, patchFlowInstruction);
    } else if (start.getAddress() == entry.getAddress()) {
        if (toList.size() == 1 && toList.get(0).getAddress() == lastInsAddress + 4) {
            willPatchCondInstruction = lastIns;
            patchInfo.put(copyBlock.getOrDefault(toList.get(0).getAddress(), toList.get(0).getAddress()) - moduleBase, patchFlowInstruction);
        } else
            throw new RuntimeException("Unknown entry block area format");
    } else if (toList.size() == 1 && toList.get(0).getAddress() == preDispather.get(0).getAddress()) {
        // 如果有复制的话,等后续再处理,这里只记原本的
        willPatchCondInstruction = lastIns;
        if (copyBlock.containsKey(top.getAddress())) {
            long targetPatchAddress = copyBlock.get(top.getAddress()) + b.size() * 4L;
            patchInfo.put(targetPatchAddress - moduleBase, patchFlowInstruction);
        } else {
            patchInfo.put(preDispather.get(0).getAddress() - moduleBase, patchFlowInstruction);
        }
    } else
        throw new RuntimeException("Unknown block area format " + Long.toHexString(start.getAddress()));

    if (condBlockAreaMap.containsKey(start.getAddress()) && condBlockAreaMap.get(start.getAddress()).get(1) != null) {
        List<Instruction> insBetweenKeyInsAndEnd = new ArrayList<>();
        insBetweenKeyInsAndEnd.add(willPatchCondInstruction);
        while (!startMapTokeyIns.containsValue(insBetweenKeyInsAndEnd.get(insBetweenKeyInsAndEnd.size() - 1).getAddress())) {
            Instruction cur = insBetweenKeyInsAndEnd.get(insBetweenKeyInsAndEnd.size() - 1);

            if (fromMap.containsKey(cur.getAddress())) {
                //                                        List<Instruction> from = fromMap.get(cur.getAddress());
                // 如果是commonBlock,可能存在多个from,所以这里需要筛选一下
                Instruction fromInArea = null;
                for (Instruction f : fromMap.get(cur.getAddress()))
                    if (area.contains(f.getAddress())) {
                        if (fromInArea == null)
                            fromInArea = f;
                        else
                            throw new RuntimeException("Find block area key ins error (multi from) " + Long.toHexString(cur.getAddress()) + " " + cur);
                    }
                insBetweenKeyInsAndEnd.add(fromInArea);
            } else
                throw new RuntimeException("Find block area key ins error (no from) " + Long.toHexString(cur.getAddress()) + " " + cur);
        }
        // end .... ins
        // end: b...
        long trueAddress = condBlockAreaMap.get(start.getAddress()).get(1).getAddress();
        Instruction keyIns = insBetweenKeyInsAndEnd.get(insBetweenKeyInsAndEnd.size() - 1);
        String[] insInfoList = keyIns.toString().split(",");
        String cond = insInfoList[insInfoList.length - 1].trim();
        patchInfo.put(copyBlock.getOrDefault(willPatchCondInstruction.getAddress(), willPatchCondInstruction.getAddress()) - moduleBase,
                      String.format("b.%s %d", cond, trueAddress - moduleBase));

        // 创建一个临时的list,因为跳转指令不参与重排
        List<Instruction> tmp = new ArrayList<>();
        for (int i = 0; i < insBetweenKeyInsAndEnd.size(); i++)
            if (!insBetweenKeyInsAndEnd.get(i).getMnemonic().equals("b"))
                tmp.add(insBetweenKeyInsAndEnd.get(i));
        // 从1开始,1 patch 0的,2 patch 1的
        for (int i = 1; i < tmp.size(); i++) {
            patchInfo.put(copyBlock.getOrDefault(tmp.get(i).getAddress(), tmp.get(i).getAddress()) - moduleBase, tmp.get(i - 1).toString());
        }
    }
}

7.3 allocBlock

这里是最简单的一个分配:如果说某一个控制块有足够的 size,而且 patchInfo, copyBlock 中都没出现过这个 block(代表它没有被分配过),那么就可以使用这块区域,将当前区域的首条指令返回即可,由外部进行 patch

private Instruction allocBlock(List<Instruction> common, int size, Map<Long, String> patchInfo, Map<Long, Long> copyBlock) {
    for (List<Instruction> controlNode : controlNodes) {
        // 这里需要避免entry之后的控制块被分配了,因为它可能是接着的,是没有跳转指令来覆盖的
        // 这个控制块一般是preDispatcher的后继
        List<Instruction> from = fromMap.get(controlNode.get(0).getAddress());
        boolean isLinkEntry = false;
        for (Instruction f : from)
            if (f.getAddress() == preDispather.get(preDispather.size() - 1).getAddress())
                isLinkEntry = true;
        if (isLinkEntry)
            continue;

        if (controlNode.size() >= size && !patchInfo.containsKey(controlNode.get(0).getAddress() - moduleBase) && !copyBlock.containsValue(controlNode.get(0).getAddress())) {
            return controlNode.get(0);
        }
    }
}

如果 patch 只是这样的话,可以略过,但是真正处理时,出现 size 比较大的情况,那么需要多个控制块才能复制这个 common

我们以倒序进行分配,这样比较好处理,比如 commonBlock 需要复制为 block1, block2, block3,那么倒着分配的话,可以分配 block2 时,将它的末尾 patch 为 b {block3Addr}

一直分配,直到rest为0

  • 每次分配实际能用的是 controlNode.size - 1,因为需要 patch 跳转到下个分配块的指令
  • 分配完成之后,需要将 controlNode 的首个指令映射到 commonBlock 中,将信息存放在 copyBlock 里面
private Instruction allocBlock(List<Instruction> common, int size, Map<Long, String> patchInfo, Map<Long, Long> copyBlock) {
    // 倒着分配
    Instruction lastAlloc = null;
    int rest = size;
    // 对于没有一个区块能满足的
    for (List<Instruction> controlNode : controlNodes) {
        // 这里需要进行普适,不只是entry块,所有和前面块联系的,都不能被分配,因为它要由前面的块直接使用
        List<Instruction> from = fromMap.get(controlNode.get(0).getAddress());
        boolean isDirectAfterRealBlock = false;
        for (Instruction f : from)
            if (f.getAddress() == controlNode.get(0).getAddress() + 4)
                isDirectAfterRealBlock = true;
        if (isDirectAfterRealBlock)
            continue;

        // size - common.size() 是common块之外的,会有点对不起来,所以得手动处理
        // 理论上只有第一次分配,即分配尾部需注意,这里直接全做判断,简单一些
        if (controlNode.size() > 1 + (size - common.size()) && !patchInfo.containsKey(controlNode.get(0).getAddress() - moduleBase) && !copyBlock.containsValue(controlNode.get(0).getAddress())) {
            //                return controlNode.get(0);
            if (lastAlloc != null) {
                patchInfo.put(controlNode.get(Math.min(controlNode.size() - 1, rest + 1)).getAddress() - moduleBase, String.format("b %d", lastAlloc.getAddress() - moduleBase));
            }
            lastAlloc = controlNode.get(0);
            // 不同区块之间需要一个br指令去跳转下一个alloc区域
            rest = rest - (controlNode.size() - 1);
            if (rest <= 0) {
                return lastAlloc;
            } else
                copyBlock.put(common.get(rest + 1).getAddress(), controlNode.get(0).getAddress());
        }
    }
    return null;
}

8. 根据上述 patch 信息patch so

根据7输出的 patch 信息,修改so,我这里使用了 rizin

patch: [207fc] mov w19, #0x6f67
patch: [20bfc] nop
patch: [1ee40] bl 139700

将文件保存到 patch_ins.txt,然后 lib52pojie.so 复制出一份 patch_ins_lib52pojie.so,这里有点奇怪,rizin 可以通过 wa 直接写入指令,但是存在一些问题,比如 csel 无法转化,b 指令转化不正常

所以我这里通过 keystone 来为数据,然后通过 rizin 的 wx 指令写入数据

import rzpipe
import re
from keystone import *

if __name__ == '__main__':
    so = rzpipe.open('./patch_ins_lib52pojie.so', flags=['-w'])
    ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)

    with open('./patch_ins.txt') as f:
        pattern = r"patch: \[([0-9a-fA-F]+)\] (.+)"
        for line in f.readlines():
            if (len(line.strip()) != 0):
                match = re.match(pattern, line)
                encoding, count = ks.asm(match.group(2), int(match.group(1), 16))
                data = ' '.join(["%02x" % b for b in encoding])
                so.cmd(f'wx "{data}" @ 0x{match.group(1)}')

    so.quit()

结语

到此为止,反混淆的实现已经完成,它还不是很完美,但是反混淆的结果还是很令我满意的

本文完成之后并未进行细致的检查,而且也不是十分细致,因为写一遍已经耗费我数个小时了,暂时先这样,后续可能再做修正。如果任何问题,欢迎评论

免费评分

参与人数 14吾爱币 +14 热心值 +13 收起 理由
jackyyue_cn + 1 + 1 用心讨论,共获提升!
princekin + 1 + 1 谢谢@Thanks!
抱歉、 + 1 用心讨论,共获提升!
Li1y + 2 + 1 用心讨论,共获提升!
0120 + 1 + 1 我很赞同!
X1a0 + 1 + 1 谢谢@Thanks!
yunji + 1 用心讨论,共获提升!
1MajorTom1 + 1 热心回复!
debug_cat + 2 + 1 谢谢@Thanks!
hopecolor514 + 1 热心回复!
mt0603 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
allspark + 1 + 1 用心讨论,共获提升!
Yifan2007 + 2 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
Yangzaipython 发表于 2024-6-25 09:48
仔细钻研下
3#
mt0603 发表于 2024-6-25 10:25
4#
beyondchampion 发表于 2024-6-25 10:49
5#
NDsans 发表于 2024-6-25 12:10
厉害!学习了!
6#
我是小三 发表于 2024-6-25 13:49
又学习到了新知识,多谢
7#
52wxs 发表于 2024-6-25 16:16
学习学习,感谢
8#
hopecolor514 发表于 2024-6-25 17:10
谢谢,感觉学到了,又不是很明白
9#
debug_cat 发表于 2024-6-25 17:13
先收藏,现在看不懂啊,难受
10#
Hmily 发表于 2024-6-25 17:37
厉害,牛!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-7-2 13:58

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表