声明
本文仅限于技术讨论,不得用于非法途径,后果自负。
前言
随着混淆技术和虚拟化的发展,我们不可能很方便的去得到我们想要的东西,既然如此,那只能比谁头更铁了,本文列举一下在逆向某一个签名字段中开发者所布下的铜墙铁壁,以及我的头铁方案,本文更多的是分析思路,而不是解决方案,所以样本自己找
BR reg
在ARM64 中 寄存器跳转只剩下BR 指令了,由于ida 为了 br的准确性(cs:ip跳转),ida会识别成函数跳转,但是这却给开发者带来了天大的便利,于是乎,一个贼好用的anti 反编译器函数分析方案横空出世

让我们回到汇编
1
2
3
4
|
.text: 000000000008D1E8 BL sub_8D1F8
.text: 000000000008D1EC MOV X1, X0
.text: 000000000008D1F0 ADD X1, X1,
.text: 000000000008D1F4 BR X1
|
可以看到 X1的值是由sub_8D1F8 返回值加上 0x38 ,而sub_8D1F8一看地址 不对啊,为什么这么近。我们再看看sub_8D1F8的汇编

如果经常看汇编的小伙伴已经懂了
1
2
|
.text: 000000000008D1FC STP X29, X30, [SP]
.text: 000000000008D200 LDR X0, [SP,
|
sub_8D1F8的返回值 被x30 也就是lr寄存器赋值 所以 X1 = sub_8D1F8返回地址 +0x38 = 0x8D1EC + 0x38 = 0x8d224

既然如此 直接改跳转吧,写个脚本模式匹配下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
def antiBR1(start,end):
addr = start
while addr < end :
insn = idc.print_insn_mnem(addr)
op0 = idc.print_operand(addr, 0 )
op1 = idc.print_operand(addr, 1 )
if insn = = "BL" and (idc.print_insn_mnem(addr + 0xc ).find( "BR" ) ! = - 1 or idc.print_insn_mnem(addr + 0x8 ).find( "BR" ) ! = - 1 ):
addr1 = addr
while 1 :
str = get_instruction_at_address(addr1)
if str ! = None :
if str .find( "ADD" ) ! = - 1 :
break
addr1 = addr1 + 4
opeValue = idc.get_operand_value(addr1, 2 )
print ( "find add: %x" % opeValue)
code,count = ks.asm( "B " + hex (addr + 4 + opeValue),addr)
print ( hex (addr))
ida_bytes.patch_bytes(addr, bytes(code))
addr = addr + 4
|
修复后

花指令
可以看到一排奇怪的东西

正常程序怎么会有呢,看看汇编
1
2
3
4
5
6
7
8
9
10
11
|
.text: 000000000008D53C loc_8D53C ; CODE XREF: sub_8D170:loc_8D530↑j
.text: 000000000008D53C MRS X1, NZCV
.text: 000000000008D540 MOV X0, XZR
.text: 000000000008D544 CMP X0, XZR
.text: 000000000008D548 MSR NZCV, X0
.text: 000000000008D54C B.NE loc_8D558
.text: 000000000008D550 CLREX
.text: 000000000008D554 BRK
.text: 000000000008D558
.text: 000000000008D558 loc_8D558 ; CODE XREF: sub_8D170 + 3DC ↑j
.text: 000000000008D558 MSR NZCV, X1
|
MSR NZCV, X0 x0 = 0 所以zf位为0 所以B.NE 恒成立 所以啥事没干 所以可以直接nop
简单的看下逻辑

0x8d240 在这个函数内 所以不是代码自解码 就是校验

查看sub_13C704

那就是检验咯 不管 下一个函数 sub_13DC3C
修复后发现f5 没东西

这怎么可能 切换到汇编,研究后
1
2
3
4
5
6
7
|
.text: 000000000013DCE4 000 MOV X30, X17
.text: 000000000013DCE8 000 SUB SP, SP,
.text: 000000000013DCEC 050 STP X29, X17, [SP,
.text: 000000000013DCF0 050 ADD X29, SP,
.text: 000000000013DCF4 050 STUR X0, [X29,
.text: 000000000013DCF8 050 STUR X1, [X29,
.text: 000000000013DCFC 050 STR X0, [SP,
|
1
2
3
|
.text: 000000000013DD94 050 LDR X9, [SP,
.text: 000000000013DD98 050 LDR X10, [SP,
.text: 000000000013DD9C 050 STR X9, [X10,
|
var_38 = x29 x29指向存放 x29 x30的栈地址 所以STR X9, [X10,#8] 等价于 x30 = x9 x0 是参数 所以回上一个函数

去看看吧 sub_8d5c8
ollvm

标准控制流平坦化
这里简单介绍什么是控制流平坦化
源代码
1
2
3
4
5
6
7
8
|
if (temp){
print ( "ok" );
return 1
}
else {
print ( "no" );
return 2
}
|
经过平坦化后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
var flowState = 0 ;
while ( 1 )
{
switch(flowState):
case 0 :
if (temp){
flowState = 1
}
else {
flowState = 2
}
break
case 1 :
print ( "ok" );
flowState = 3
case 2 :
print ( "no" );
flowState = 4
case 3 :
return 1
case 4 :
return 0
}
|
可以看到一个 while switch 的结构 其中flowState 负责控制整个流程 这个就是平坦化的基本原理 可以看到8行普通的代码被膨胀到了23行 假如我再平坦化 一次呢 我们会发现随着平坦化的越来越多,肉眼阅读的能力也越来越困难
下面介绍一个我从国外看到的方案 345K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Z5k6i4S2Q4x3X3c8J5j5i4W2K6i4K6u0W2j5$3!0E0i4K6u0r3j5X3I4G2k6#2)9J5c8X3S2W2P5q4)9J5k6s2u0S2P5i4y4Q4x3X3c8E0K9h3y4J5L8$3y4G2k6r3g2Q4x3X3c8S2M7r3W2Q4x3X3c8$3M7#2)9J5k6r3!0T1k6Y4g2K6j5$3q4@1K9h3&6Y4i4K6u0V1j5$3!0E0M7r3W2D9k6i4t1`.
1.计算支配节点
下面画一个简单的cfg图
1
2
3
4
5
6
7
|
0
|
1
/ \
2 3
/ \ / \
4 5 6
|
如果走到某一个节点2 必须经过另外一个节点1 那么 2 被 1 支配 1 是 2的支配节点
通过bitset 可以快速计算出来
0: 0
1: 1
2:0,1
3: 0,1
4:0,1,2
5:0,1,2,3
6:0,1,3
将其反转
可得0 是所有节点的支配节点
有什么用?
仔细看平坦化的代码 可以发现 while 会被所有节点支配 所以 控制流分发快 必定被所有节点支配 由此定位到了分发块
2.路径计算
我们来看 如果从0要走到 5 有几种 路径
0125
0135
将cfg填充内容
1
2
3
4
5
6
7
8
9
10
11
|
0
flowState = 0
|
1
/ \
2 3
flowState = 1 flowState = 2
/ \ / \
4 5 6
| | |
1 1 1
|
可以得到 0125 flowState = 1, 0124 flowState = 2 那么如果计算支配节点的所有路径 是不是可以得到所有 flowState 的状态
状态指向计算
通过switch 可以轻松得到 状态指向的地址
修改控制流
将每一个状态所对应的 地址 连接
死代码消除
检查每一个 没有前继节点的节点 删除 反编译器自动优化
活跃变量分析
假设 0节点 flowState = 0 1 节点flowState参与 2节点中 flowState = 1 那么 flowState 1节点 将被删除 反编译器自动优化
反混淆
接下来回到这个demo
将他转换成cfg

[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
最后于 2025-2-15 10:23
被method编辑
,原因: