某音去除花指令

ida7.7 打开so文件 查看jni_onload

img_1.png

底部有个内联汇编, 跳转到X1, 但ida没识别出来.
__asm { BR X1 }

看看汇编代码
img_1.png

可以看到 BR X1
那x1来自哪里
x1 = x0 + x34
那X0呢, sub_29D9C 这个函数点进去看看

img_1.png

从图中可以看到, 不同的颜色是一个对称操作, 绿色箭头是开栈和清栈, 红色箭头是 push 和 pop, 只有这个黄色箭头 才是这个函数真正的目的.
这条指令相当于 x0 = X30, arm64 X30 就是 LR 寄存器. 此时LR 是多少呢?
BL 指令执行前就将下一条要执行的指令保存在 LR 中, 所以 LR = 0x29D94, X0 = LR = 0x29D94
接下来一条指令是 ADD X1, X0, #0x34 那 X1 = 0x29D94 + 0x34 = 0x29dc8 实际要跳转的地址就是 0x29dc8, 但是ida没有识别出来
我们来帮助 ida 进行识别.
img_1.png
现在我们知道了, 这三行其实就是花指令. 让ida 无法识别真实的跳转地址.

BL sub_29D9C 相当于 执行了 X0 = LR = pc + 4
BR X1 相当于 br 0x29D90 + 4 + #0x34
最终要跳转的地址就是 BR 0x29dc8
也就是说这三行就相当于 BR 0x29dc8, 我们直接用 ida 的插件 keypatch 来patch 第一行, 第2, 3行 进行 nop
img_1.png

将第一行改为 B 0x29dc8
剩下两行nop
img_1.png

然后再次 F5
img_1.png

好嘛, 从 XR X1 变成 BR X6了, 继续来看看汇编
img_1.png

好像跟刚才花指令类似, 一个函数, 然后加上一个数, 然后跳转.
我们看看函数 sub_29D9C
img_1.png
跟刚才的确实类似, SUB 开栈, ADD 清栈, STP 压栈, LDP 出栈. 只有中间的 LDR X0, [SP,#8] 有用, 相当于 X0 = X30 = LR

手动计算一下X6的地址.

img_1.png

X6 = 0x29DC8 + 4 + 8 = 0x29dd4
我们像刚才一样使用 keypatch 进行patch
img_1.png
将第一行改为 B 0x29dd4 第2, 3行 nop

改完后再次 F5
img_1.png

发现有效果了, 正确识别出一些函数了, 但是下面还是有 BR x1

我们跳转到 BR X1 处看看。
img_1.png
好家伙跟刚才如出一辙, 只是这次加的是 0x38
我们按照相同的方法计算出 X0 = LR + 0x38 = 0x29e68
利用keypath 进行patch. patch 完后再次 F5. 效果如下

img_1.png

可以看到又识别了很多函数, 但是又遇到了 BR X1, 我们点进去看看
img_1.png

跟刚才一模一样. 开始思考人生,这么patch 下去什么时候是个头呢?

这里总结一下, 这里遇到了三种花指令
一种是 BR LR + 8
一种是 BR LR + 0x34
一种是 BR LR + 0x38

我们看看 BR LR + 0x34 这种花指令在so中出现了多少次

设置一下ida显示指令的机器码.
img_1.png

将这里改为 4
img_1.png

我们来到 jni_onload 的第一处花指令
img_1.png

我们通过ida的二进制搜索功能搜索第2,3行对应的机器码 01 D0 00 91 20 00 1F D6 看看有多少处 0x34这种类型的混淆
img_1.png

继续勾选 find all
img_1.png

img_1.png
可以看到有122 处.

有122处匹配不代表有122处花指令, 可能其他的一些数据正好能匹配 01 D0 00 91 20 00 1F D6。
我们希望再精准一点, 不要匹配到不该匹配的内容. 怎么再精准一些呢.
第一行我们是不是也可以匹配一下呢?
BL sub_2A2BC 我们放在 https://armconverter.com/?code=BL+0x0C 这个网站中转换一下
img_1.png

跟ida中的一致.
BL 0x0C 对应的机器码是 03 00 00 94
那如果是 BL 0x1C 呢? 对应的机器码会是什么样子呢?
img_1.png

发现只是 03 00 00 94 变成了 07 00 00 94 只是第一个字节发生了变化. 可以看到小跳转只是改变了第一个字节, 我们假设花指令都是小跳转(双板小回转.jpg). 我们把 00 00 94 也加入到匹配中
在 ida中搜索 00 00 94 01 D0 00 91 20 00 1F D6
img_1.png

可以看到同样122处, 看来之前匹配到的就是全部的花指令.

那我们能不能通过脚本匹配这种花指令呢

通过python 脚本如下

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
import ida_bytes
import idaapi
import idc
import keystone # pip install keystone-engine
from keystone import *

def binSearch(start, end, pattern):
matches = []
addr = start
if end == 0:
end = idc.BADADDR
if end != idc.BADADDR:
end = end + 1
while True:
addr = ida_bytes.bin_search(addr, end, bytes.fromhex(pattern), None, idaapi.BIN_SEARCH_FORWARD,
idaapi.BIN_SEARCH_NOCASE)
if addr == idc.BADADDR:
break
else:
matches.append(addr)
addr = addr + 1
return matches

matches = binSearch(0, 0, "00 00 94 01 D0 00 91 20 00 1F D6")
#matches = binSearch(0, 0, "00 94 01 D0 00 91 20 00 1F D6")
print(len(matches)) # 122

执行过后, ida打印出 122处匹配, 通过脚本匹配的和ida搜索匹配的数量一样.
那花指令地址找到了, 怎么计算中真实跳转地址呢. 参考如下脚本

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import ida_bytes
import idaapi
import idc
import keystone
from keystone import *

def binSearch(start, end, pattern):
matches = []
addr = start
if end == 0:
end = idc.BADADDR
if end != idc.BADADDR:
end = end + 1
while True:
addr = ida_bytes.bin_search(addr, end, bytes.fromhex(pattern), None, idaapi.BIN_SEARCH_FORWARD,
idaapi.BIN_SEARCH_NOCASE)
if addr == idc.BADADDR:
break
else:
matches.append(addr)
addr = addr + 1
return matches


def getJumpAddress(addr):
# print("hex(addr)", hex(addr))
# print("hex(addr) 2", hex(addr + 4))
# print("get_operand_value 0", hex(idc.get_operand_value(addr + 4, 0)))
# print("get_operand_value 1", hex(idc.get_operand_value(addr + 4, 1)))
# print("get_operand_value 2", hex(idc.get_operand_value(addr + 4, 2)))
# print("get_operand_value 3", hex(idc.get_operand_value(addr + 4, 3)))
real_jump_adrr = addr + 4 + idc.get_operand_value(addr + 4, 2)
print("real_jump_adrr", hex(real_jump_adrr))
return real_jump_adrr


matches = binSearch(0, 0, "00 00 94 01 D0 00 91 20 00 1F D6")

print(len(matches))

#for match_addr in matches[:1]: # 0x1fbc0 # 0x1fbf8
for match_addr in matches[:]:

addr = match_addr - 1 # 因为我们匹配的时候没有要第一个字节的数据, 所以指令的地址 = 匹配的地址 - 1. 这点我踩了坑, 调试了很久...
# addr = 0x29D90
print("花指令起始地址 => ", hex(addr))
# makeInsn(addr)
targetAddr = getJumpAddress(addr)
print("targetAddr", hex(targetAddr))

获取真实跳转地址的逻辑就是 第一行地址 + 4 + 第二行汇编的第3个操作数. 对应脚本中的 real_jump_adrr = addr + 4 + idc.get_operand_value(addr + 4, 2)

有了真实地址 如何patch呢

  1. 构造出 BL real_jump_addr 这个汇编代码
  2. 将这个汇编代码转换为机器码
  3. ida_bytes.patch_bytes 将机器码修改到对应的地址处.

接下来我给出三种花指令批量处理的脚本. 可以patch so中300多处的花指令

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import ida_bytes
import idaapi
import idc
import keystone
from keystone import *

def binSearch(start, end, pattern):
matches = []
addr = start
if end == 0:
end = idc.BADADDR
if end != idc.BADADDR:
end = end + 1
while True:
addr = ida_bytes.bin_search(addr, end, bytes.fromhex(pattern), None, idaapi.BIN_SEARCH_FORWARD,
idaapi.BIN_SEARCH_NOCASE)
if addr == idc.BADADDR:
break
else:
matches.append(addr)
addr = addr + 1
return matches


def getJumpAddress(addr):
# print("hex(addr)", hex(addr))
# print("hex(addr) 2", hex(addr + 4))
# print("get_operand_value 0", hex(idc.get_operand_value(addr + 4, 0)))
# print("get_operand_value 1", hex(idc.get_operand_value(addr + 4, 1)))
# print("get_operand_value 2", hex(idc.get_operand_value(addr + 4, 2)))
# print("get_operand_value 3", hex(idc.get_operand_value(addr + 4, 3)))
real_jump_adrr = addr + 4 + idc.get_operand_value(addr + 4, 2)
print("real_jump_adrr", hex(real_jump_adrr))
return real_jump_adrr



def makeInsn(addr):
if idc.create_insn(addr) == 0:
idc.del_items(addr, idc.DELIT_EXPAND)
idc.create_insn(addr)
idc.auto_wait()


def generate(code, addr):
ks = Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
# 参数2是地址,很多指令是地址相关的,比如 B 指令,如果地址无关直接传 0 即可,比如 nop。
encoding, _ = ks.asm(code, addr)
return encoding

def getJumpAddress_third(addr):
# print("hex(addr)", hex(addr))
# print("hex(addr) 2", hex(addr + 4))
# print("get_operand_value 0", hex(idc.get_operand_value(addr + 4, 0)))
# print("get_operand_value 1", hex(idc.get_operand_value(addr + 4, 1)))
# print("get_operand_value 2", hex(idc.get_operand_value(addr + 4, 2)))
# print("get_operand_value 3", hex(idc.get_operand_value(addr + 4, 3)))
real_jump_adrr = addr + 4 + 0x38
print("real_jump_adrr", hex(real_jump_adrr))
return real_jump_adrr

matches = binSearch(0, 0, "00 00 94 01 D0 00 91 20 00 1F D6")
#matches = binSearch(0, 0, "00 94 01 D0 00 91 20 00 1F D6")
print(len(matches)) # 119

#for match_addr in matches[:1]: # 0x1fbc0 # 0x1fbf8
for match_addr in matches[:]:

addr = match_addr - 1
# addr = 0x29D90
print("花指令起始地址 => ", hex(addr))
# makeInsn(addr)
targetAddr = getJumpAddress(addr)
print("targetAddr", hex(targetAddr))
code = f"B {hex(targetAddr)}"
bCode = generate(code, addr)
nopCode = generate("nop", 0)
ida_bytes.patch_bytes(addr, bytes(bCode))
ida_bytes.patch_bytes(addr + 4, bytes(nopCode) * 2)

# 第二次patch 针对 如下花指令 所有的0x8 这种花指令都是紧挨着 0x34 这种花指令的. 可以通过ida search bytes 来确认
# ADD X6, X0, #8
# BR X6
second_jump_addr = targetAddr + 4 + 8
code = f"B {hex(second_jump_addr)}"
bCode = generate(code, targetAddr)
nopCode = generate("nop", 0)
print("第二次跳转地址 ", hex(second_jump_addr))
ida_bytes.patch_bytes(targetAddr, bytes(bCode))
ida_bytes.patch_bytes(targetAddr + 4, bytes(nopCode) * 2)



# 第三次 patch 针对 0x38 这种情形
matches = binSearch(0, 0, "00 00 94 E1 03 00 AA 21 E0 00 91 20 00 1F D6") # 有158处
print(len(matches)) # 158
#for match_addr in matches[:1]: # 0x1fbc0 # 0x1fbf8
for match_addr in matches[:]:

addr = match_addr - 1
# addr = 0x29E2C
print("花指令起始地址 => ", hex(addr))
# makeInsn(addr)
targetAddr = getJumpAddress_third(addr)
print("targetAddr", hex(targetAddr))
code = f"B {hex(targetAddr)}"
bCode = generate(code, addr)
nopCode = generate("nop", 0)
ida_bytes.patch_bytes(addr, bytes(bCode))
ida_bytes.patch_bytes(addr + 4, bytes(nopCode) * 3)

代码中要注意 0x8 这种花指令 都是出现在 0x34 之后的。 但是0x38跟 0x8, 0x34是分开的

整个脚本执行完后, 可以看到 jni_onload 好看了很多
img_1.png

完结!