春节期间就已经看到红包活动了,但是感觉自己完成时间比较长,当时就没做。后面断断续续做了 Android 这边的题目,还是学习到很多东西的。论坛上有许多红包活动解题的文章,Android的高级题只有一个,所以解的过程还是很有记录和分享的价值的。
这篇文章主要是记录Android高级题控制流平坦化反混淆的思路和一些实现,现在实现还有缺陷,另外目标并不是高级题flag的获取,因为还未分析完成,所以后续可能会重新编辑本文,进行更新
使用的是unidbg,目前可以已经验证可以反混淆的(2024-06-23):
简单看一下效果(这里是最外层函数0x1befc),左边是进行一些简单patch之后的效果,可以看出是控制流平坦化的特征,右边是进行反混淆之后的结果
样本混淆特征 & 为什么反混淆
样本中控制流平坦化控制块是这样的,前面部分和以前了解过的控制流平坦化差不多,但是跳转地址是动态计算的,所以我们无法看清楚方法的全貌。在 爱破解 2024 春节红包活动WP(全,含 Android 高级题) 文章中对 Android 高级题的处理主要是 1c110 处修改 nzcv 寄存器,1c11c 获取 x10 中的地址,这样就能获取符合 lt 以及不符合 lt 的跳转地址,再进行 patch 即可。
我也这样处理试了一下,我认为有一个缺点,由于还是几乎按照正常执行逻辑进行处理的,所以修改完成后会出现一些未被 patch 的逻辑,经常需要再补
另外这样是进行了简单的 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。
控制流平坦化的反混淆思路
另外这里简单介绍一下控制流平坦化的反混淆思路
[ol]
[/ol]
反混淆的过程
下面是反混淆的具体步骤,相对重要的是1和7,代码本身只有两个文件:
[ol]
[/ol]
1. 初步探索
探索其实就是遍历二叉树的一个过程,每探索一个分支就重新执行一遍程序。之前实现过一次执行,以及仅重复执行方法,均有问题。所以每次探索一个分支就跑一遍 unidbg 的 emulator build,load library,这样处理是最方便的。此外就是探索过程要实际做的事情:获取指令之间的前后顺序
1.1 遍历的整体逻辑
path是一个Node List,node中包含的信息是 address 和 cond,表示涉及判断,即获取 NZCV 寄存器指令的地址,以及当时应选择的指令条件满足的状态,true 还是 false
[ol]
[/ol]
入参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 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 tmpPath = this.currentPath;
for (int i = tmpPath.size() - 1; i >= originSize; i--) {
if (tmpPath.get(i).cond) {
List newPath = new ArrayList();
for (int j = 0; j
1.2 新分支的探索
这一块是 emulator.getBackend().hook_add_new 的 CodeHook() 中的内容,每次碰到 nzcv 节点进行如下处理:
String condStr = getCond(ins);
Cpsr cpsr = Cpsr.getArm64(backend);
boolean cond = isCondSuc(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[I] nodes = new HashSet();
private Map toMap = new HashMap();
private Map 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 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 避免死循环
需要在两个层面避免
[ol]
[/ol]
将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
1.5 避免重复探索
如果一个节点 true 和 false 均探索完成,那么就没有必要再继续探索了,下次碰到直接退出即可
这个可以极大的加快探索的速度,但是可能会引起探索不够充分的问题,代码比较乱,暂时不贴
2. 合并块
合并指令为块,其实相对简单,通过遍历之前存下的指令先后顺序即可创建出来
[ol]
[/ol]
此外,需要一个 accessed 来保证不重复,确保不会陷入死循环中
// Set还是List其实没有关系,最终都是通过遍历判断地址是否相同来保证唯一的
private Set 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 accessed = new HashSet();
Stack[I] pending = new Stack();
// 前一个区块的最后一个地址对应的node,相当于包含了link的信息
List graphLinkMap = new ArrayList();
pending.push(entry);
accessed.add(entry.getAddress());
while (!pending.empty()) {
Instruction ins = pending.pop();
List[I] from = fromMap.get(ins.getAddress());
List[I] 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[I] 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()
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 controlNodes = new HashSet();
private Set 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[I] toBlock = getMergeNodeByStart(ins.getAddress());
controlNodes.add(toBlock);
assert toBlock != null;
List[I] 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[I] block: mergedNodes) {
List[I] 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[I] block: mergedNodes) {
if (!controlNodes.contains(block)) {
realNodes.add(block);
}
}
System.out.println("control block " + controlNodes.size());
System.out.println("real block " + realNodes.size());
}
4. 获取真实块的合集
我所看到的控制流平坦化反混淆的文章中,真实块是单个的,处理起来会比较简单,但实际真实块的情况会更复杂,这个样本中就是如此,会存在多个真实块,那么其实要获取在控制块和预分发块之间的真实块的合集,一个控制块就应有这样一个合集(后面发现有例外,但暂不考虑)
另外真实块合集之间并不是完全切割开来的,有一部分共用的块,但这些块都是在预分发器前面
这一步的目标是:
[ol]
[/ol]
下面是 InstrutionGraph 中的代码,这里主要是调用 mergeOneRealArea 去获取真实块合集,在 mergeOneRealArea 中会记录 block 的次数
[ol]
[/ol]
private Map commonBlock = new HashMap();
// 代码中都写的是 real area, block area这种,也就是上述的合集
public void mergeRealArea() {
mergeOneRealArea(entry, to -> {
for (List[I] block : controlNodes)
if (block.get(0).getAddress() == to.getAddress())
return true;
return false;
});
for (List[I] block : realNodes) {
List[I] from = fromMap.get(block.get(0).getAddress());
if (from == null) {
continue;
}
for (Instruction ins : from) {
// 如果from block是控制块,那么就算是area start,才进行后续的处理
List[I] 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[I] block = getMergeNodeByStart(key);
List[I] 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,这里面主要是去遍历块直到末尾,类似之前第二步--合并块的处理逻辑,只是这次直接处理块
private Map commonBlock = new HashMap();
private Map> blockAreaStart = new HashMap();
// 因为keyIns可能在common block中,所以start to keyins,而不是反过来
private Map startMapTokeyIns = new HashMap();
interface OutBlockJudge {
boolean isOutBlock(Instruction to);
}
// ins 是合集的开头
// outBlockJudge 是用于判断一个 block 是否是末尾
private void mergeOneRealArea(Instruction ins, OutBlockJudge outBlockJudge) {
Set area = new HashSet();
Set accessed = new HashSet();
Stack[I] 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[I] 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[I] 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 会强制退出,导致后续指令都获取不全。
为了实现这两个目标,需要在之前 accessedInstructions 的基础上再加一个 accessedRealInstructions 和 accessedStartInstructions
这一块是 emulator.getBackend().hook_add_new 的 CodeHook() 中的内容,第二次探索前先设置一个 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
6. 合并块,对2进行补充
用二次探索的结果扩充之前合并块的结果,大体上可以复用之前的代码,主要注意 !isFirst 的逻辑,在产生 mergeNode 后,如果发现之前有记录,就直接删掉,用新的即可
另外的一些工作就是绘图,这样就会有一个相对直观的结果,其他地方也可以做这件事,比如之前区分各种块的时候,在图上标出了控制块,以及这里合并完成之后,可以将真实块之间连接起来
public void mergeNodes(boolean isFirst) {
Set accessed = new HashSet();
Stack[I] pending = new Stack();
// 前一个区块的最后一个地址对应的node,相当于包含了link的信息
List graphLinkMap = new ArrayList();
pending.push(entry);
accessed.add(entry.getAddress());
while (!pending.empty()) {
Instruction ins = pending.pop();
List[I] from = fromMap.get(ins.getAddress());
List[I] 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("
" + sb.toString() + " |
.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[I] 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 patchInfo = new HashMap();
patchInstructionArea(patchInfo, entry, to -> {
for (List[I] block : controlNodes)
if (block.get(0).getAddress() == to.getAddress())
return true;
return false;
});
for (List[I] block : realNodes) {
List[I] from = fromMap.get(block.get(0).getAddress());
if (from == null) {
continue;
}
for (Instruction ins : from) {
// 如果from block是控制块,那么就算是area start,才进行后续的处理
List[I] lastBlock = getMergeNodeByEnd(ins.getAddress());
if (!controlNodes.contains(lastBlock)) {
continue;
}
patchInstructionArea(patchInfo, block.get(0), to -> preDispather.get(0).getAddress() == to.getAddress());
}
}
for (List[I] 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 patchInfo, Instruction start, OutBlockJudge outBlockJudge) {
int addition = condBlockAreaMap.containsKey(start.getAddress()) ? 1 : 0;
Set area = new HashSet();
// block 原有的到新的映射,在一次area中一个block只会被复制一次
Map copyBlock = new HashMap();
Set accessed = new HashSet();
Stack[I] pending = new Stack(); // 只存block的首个ins
accessed.add(start.getAddress());
pending.add(start);
// 遍历整个block area
while (!pending.empty()) {
Instruction top = pending.pop();
List[I] b = getMergeNodeByStart(top.getAddress());
Instruction lastIns = b.get(b.size() - 1);
long lastInsAddress = lastIns.getAddress();
List[I] 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[I] common = getMergeNodeByStart(top.getAddress());
long copyTo = copyBlock.get(top.getAddress());
long base = 0;
for (int i = 0; i
7.2.2 分配区块用于后续 commonBlock 的复制
其实可以走到 commonBlock 时再进行分配,不过写在这里相对方便一些,但是相对麻烦,还得判断 from 是否是当前区块
当碰到下一个区块是 commonBlock 时,就需要分配了。但是有一个例外,就是 commonBlock 是在当前块后面,那么当前块尾部就不是跳转指令,这个就不进行复制,让它保持原状即可。也就是说有一个合集会直接使用 commonBlock,其他合集就要进行复制。
allocBlock方法分配新的区块,新区块的大小是 commonBlock 的大小加一点额外空间
[ol]
[/ol]
在分配了新的区块后,将映射信息存放在 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[I] 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[I] 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,可能是当前块后续的指令,也可能是在复制块中,具体的情况可以看下面的注释
处理完成一个跳转地址,需要处理有分支情况的另一个跳转地址
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[I] 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[I] 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[I] tmp = new ArrayList();
for (int i = 0; i
7.3 allocBlock
这里是最简单的一个分配:如果说某一个控制块有足够的 size,而且 patchInfo, copyBlock 中都没出现过这个 block(代表它没有被分配过),那么就可以使用这块区域,将当前区域的首条指令返回即可,由外部进行 patch
private Instruction allocBlock(List[I] common, int size, Map patchInfo, Map copyBlock) {
for (List[I] controlNode : controlNodes) {
// 这里需要避免entry之后的控制块被分配了,因为它可能是接着的,是没有跳转指令来覆盖的
// 这个控制块一般是preDispatcher的后继
List[I] 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
private Instruction allocBlock(List[I] common, int size, Map patchInfo, Map copyBlock) {
// ...
// 倒着分配
Instruction lastAlloc = null;
int rest = size;
// 对于没有一个区块能满足的
for (List[I] controlNode : controlNodes) {
// 这里需要避免entry之后的控制块被分配了,因为它可能是接着的,是没有跳转指令来覆盖的
// 这个控制块一般是preDispatcher的后继
List[I] 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;
// 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)
throw new RunTimeExecption("No enough control block");
return null;
}
结语
到此为止,反混淆的实现已经完成,它还不是很完美,但是反混淆的结果还是很令我满意的
本文完成之后并未进行细致的检查,而且也不是十分细致,因为写一遍已经耗费我数个小时了,暂时先这样,后续可能再做修正。如果任何问题,欢迎评论