记Android某APP逆向

记录一下自己探索安卓逆向的过程,纯新手脚本小子,如有谬误恳请各位斧正。

root

首先是获取手机的root权限,比较简单的方法是模拟器,一般都会直接开放root,但是有些app可能会有虚拟机检测,没有的话最好,有的话可能需要花心思考虑绕过,或者使用真机。

使用真机一般会好很多。真机root一般会用到magisk[1],然后也需要对应手机型号的镜像文件(所以一般需要厂商支持root,比如小米的手机和google的pixel),这篇blog就先不讲怎么root的了,教程其实很多。

Frida

然后是Frida[2],Frida是一个python + js的hook框架[3],本质上是一种动态插桩工具。使用的时候,在需要hook的地方,比如Android手机上以root权限运行对应架构(x86或者arm,虚拟机一般x86,真机一般arm)的frida-server,然后在进行hook的地方运行frida或者frida-tools工具,frida和frida-tools并不是同一个东西,我暂时没有专门去搞清楚这两者的联系,但是需要注意的是两者需要版本兼容[4]。一般直接安装最新版应该就可以:

1
2
pip install frida
pip install frida-tools

jadx

jadx[5]是一个开源安卓逆向工具,主要用于逆向分析apk,反编译为java源代码(当然一般不可能完美反编译,遇到过一些报错的地方)

如果apk没有加壳的话,直接把apk拖到jadx里就行,jadx会自动完成分析,比如这样:

image-20240423095710623

如果代码是混淆过的,那可能会比较难过,只能慢慢看了,或许程序分析领域会有针对反混淆的研究,但是个人感觉可能都是比较ad-hoc的,而且其实混淆不是很严重的话,根据log或者一些字符串来进行推测,应该也是能分析的,我目前还没有用什么反混淆的工具,jadx自带反混淆工具,但也只是把一些函数名或者类名中的不常见的字符给去掉,换成一些常见字符加上数字编号。

hook

hook通俗点就是钩子(沟子)(坏,沟子文学逐渐污染互联网),把正常的实现替换为自己想要的实现,比如app中某个函数想要检测环境然后返回true或者false,然后你希望它永远返回true,就可以利用hook

1
2
3
4
5
6
7
Java.perform(function () {
var java_class = Java.use("com.app.classname");

java_class.function_name.overload("para list").implemeation = function("para list"){
return true;
};
});

然后使用frida的命令行工具将代码注入到对应的app进程就可以了[6]

example

以运动健康app为例,比如希望知道它所有的加密操作(代码忘记从哪复制来的了,如有问题请联系我):

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
function showStacks() {
Java.perform(function() {
send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
});
}; // print stack info

function bytesToHex(arr) {
var str = "";
for (var i = 0; i < arr.length; i++) {
var tmp = arr[i];
if (tmp < 0) {
tmp = (255 + tmp + 1).toString(16);
} else {
tmp = tmp.toString(16);
}
if (tmp.length == 1) {
tmp = "0" + tmp;
}
str += tmp;
}
return str;
}
function bytesToBase64(e) {
var base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var r, a, c, h, o, t;
for (c = e.length, a = 0, r = ''; a < c;) {
if (h = 255 & e[a++], a == c) {
r += base64EncodeChars.charAt(h >> 2),
r += base64EncodeChars.charAt((3 & h) << 4),
r += '==';
break
}
if (o = e[a++], a == c) {
r += base64EncodeChars.charAt(h >> 2),
r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
r += base64EncodeChars.charAt((15 & o) << 2),
r += '=';
break
}
t = e[a++],
r += base64EncodeChars.charAt(h >> 2),
r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
r += base64EncodeChars.charAt(63 & t)
}
return r
}
function bytesToString(arr) {
if (typeof arr === 'string') {
return arr;
}
var str = '',
_arr = arr;
for (var i = 0; i < _arr.length; i++) {
var one = _arr[i].toString(2),
v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
var bytesLength = v[0].length;
var store = _arr[i].toString(2).slice(7 - bytesLength);
for (var st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
str += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
str += String.fromCharCode(_arr[i]);
}
}
return str;
}

Java.perform(function () {
var secretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec');
secretKeySpec.$init.overload('[B','java.lang.String').implementation = function (a,b) {
//showStacks();
var result = this.$init(a, b);
send(
`========== javax.crypto.spec.SecretKeySpec.$init(byte[], String)
算法名 : ${ b }
Dec密钥(String) : ${ bytesToString(a) }
Dec秘钥(Hex) : ${ bytesToHex(a) }
`
);
return result;
};

var mac = Java.use('javax.crypto.Mac');
mac.getInstance.overload('java.lang.String').implementation = function (a) {
//showStacks();
var result = this.getInstance(a);
send(
`========== javax.crypto.Mac.getInstance(String)
算法名 : ${ a }
`
);
return result;
};
mac.update.overload('[B').implementation = function (a) {
//showStacks();
send(
`========== javax.crypto.Mac.update(byte[])
Mac update(string) : ${ bytesToString(a) }
Mac update(hex) : ${ bytesToHex(a) }
`
);
this.update(a);
};
mac.update.overload('[B','int','int').implementation = function (a,b,c) {
//showStacks();
send(
`========== javax.crypto.Mac.update(byte[], int, int)
Mac update(string) : ${ bytesToString(a) } | ${ b } | ${ c }
Mac update(hex) : ${ bytesToHex(a) } | ${ b } | ${ c }
`
);
this.update(a, b, c)
};
mac.doFinal.overload().implementation = function () {
//showStacks();
var result = this.doFinal();
send(
`========== javax.crypto.Mac.doFinal()
doFinal结果(hex) : ${ bytesToHex(result) }
`
);
return result;
};
mac.doFinal.overload('[B').implementation = function (a) {
//showStacks();
var result = this.doFinal(a);
send(
`========== javax.crypto.Mac.doFinal(byte[])
doFinal参数(string) : ${ bytesToString(a) }
doFinal参数(hex) : ${ bytesToHex(a) }
---
doFinal结果(hex) : ${ bytesToHex(result) }
`
);
return result;
};

var md = Java.use('java.security.MessageDigest');
md.getInstance.overload('java.lang.String','java.lang.String').implementation = function (a,b) {
//showStacks();
send(
`========== java.security.MessageDigest.getInstance(String, String)
算法名 :${ a }
参数2 :${ b }
`
);
return this.getInstance(a, b);
};
md.getInstance.overload('java.lang.String').implementation = function (a) {
//showStacks();
send(
`========== java.security.MessageDigest.getInstance(String)
算法名 :${ a }
`
);
return this.getInstance(a);
};
md.update.overload('[B').implementation = function (a) {
//showStacks();
send(
`========== java.security.MessageDigest.update(byte[])
MessageDigest update(string) : ${ bytesToString(a) }
MessageDigest update(hex) : ${ bytesToHex(a) }
`
);
return this.update(a);
};
md.update.overload('[B','int','int').implementation = function (a,b,c) {
//showStacks();
send(
`========== java.security.MessageDigest.update(byte[], int, int)
MessageDigest update(string) : ${ bytesToString(a) } | ${ b } | ${ c }
MessageDigest update(hex) : ${ bytesToHex(a) } | ${ b } | ${ c }
`
);
return this.update(a,b,c);
};
md.digest.overload().implementation = function () {
//showStacks();
var result = this.digest();
send(
`========== java.security.MessageDigest.digest()
digest结果(string) : ${ bytesToString(result) }
digest结果(hex) : ${ bytesToHex(result) }
`
);
return result;
};
md.digest.overload('[B').implementation = function (a) {
//showStacks();
var result = this.digest(a);
send(
`========== java.security.MessageDigest.digest(byte[])
digest参数(string) : ${ bytesToString(a) }
digest参数(hex ) : ${ bytesToHex(a) }
---
digest结果(string) : ${ bytesToString(result) }
digest结果(hex) : ${ bytesToHex(result) }
digest结果(base64) : ${ bytesToBase64(result) }
`
);
return result;
};

var ivParameterSpec = Java.use('javax.crypto.spec.IvParameterSpec');
ivParameterSpec.$init.overload('[B').implementation = function (a) {
// showStacks();
send(
`========== javax.crypto.spec.IvParameterSpec.$init(byte[])
iv向量(string) : ${ bytesToString(a) }
iv向量(hex) : ${ bytesToHex(a) }
`
);
var result = this.$init(a);
return result;
};

var cipher = Java.use('javax.crypto.Cipher');
cipher.getInstance.overload('java.lang.String').implementation = function (a) {
//showStacks();
var result = this.getInstance(a);
send(
`========== javax.crypto.Cipher.getInstance(String)
模式填充 : ${ a }
`
);
return result;
};
cipher.update.overload('[B').implementation = function (a) {
//showStacks();
send(
`========== javax.crypto.Cipher.update(byte[])\n" +
Cipher update(string) : ${ bytesToString(a) }
Cipher update(hex) : ${ bytesToHex(a) }
`
);
var result = this.update(a);
return result;
};
cipher.update.overload('[B','int','int').implementation = function (a,b,c) {
//showStacks();
send(
`========== javax.crypto.Cipher.update(byte[], int, int)
Cipher update(string) : ${ bytesToString(a) } | ${ b } | ${ c }
Cipher update(hex) : ${ bytesToHex(a) } | ${ b } | ${ c }
`
);
var result = this.update(a,b,c);
return result;
};
cipher.doFinal.overload().implementation = function () {
//showStacks();
var result = this.doFinal();
send(
`========== javax.crypto.Cipher.doFinal()
doFinal结果(string) : ${ bytesToString(result) }
doFinal结果(hex) : ${ bytesToHex(result) }
`
);
return result;
};
cipher.doFinal.overload('[B').implementation = function (a) {
//showStacks();
var result = this.doFinal(a);
send(
`========== javax.crypto.Cipher.doFinal(byte[])
doFinal参数(string) : ${ bytesToString(a) }
doFinal参数(hex) : ${ bytesToHex(a) }
---
doFinal结果(string) : ${ bytesToString(result) }
doFinal结果(hex) : ${ bytesToHex(result) }
Stack:
${ Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()) }
`
);
return result;
};

var x509EncodedKeySpec = Java.use('java.security.spec.X509EncodedKeySpec');
x509EncodedKeySpec.$init.overload('[B').implementation = function (a) {
//showStacks();
var result = this.$init(a);
send(
`========== java.security.spec.X509EncodedKeySpec.$init(byte[])
RSA密钥(string) : ${ bytesToString(a) }
RSA密钥(hex) : ${ bytesToHex(a) }
RSA密钥(base64) : ${ bytesToBase64(a) }
`
);
return result;
};

var rSAPublicKeySpec = Java.use('java.security.spec.RSAPublicKeySpec');
rSAPublicKeySpec.$init.overload('java.math.BigInteger','java.math.BigInteger').implementation = function (a,b) {
//showStacks();
var result = this.$init(a,b);
send(
`========== java.security.spec.RSAPublicKeySpec.$init(BigInteger, BigInteger)
RSA密钥N : ${ a.toString(16) }
RSA密钥E : ${ b.toString(16) }
`
);
return result;
};
});

上面是js的代码,方便起见可以用python辅助hook过程,不用每次都在命令行敲进程名,并且send函数发送的消息也需要python代码来接收(这个我没有深究,之前找到的教程是这么做的我就这么做了)

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
27
28
29
30
31
32
# -*- coding: UTF-8 -*-
import frida, sys
from datetime import datetime

jsCode = None
with open(sys.argv[1], "r", encoding="utf-8") as r:
jsCode = r.read() # load js code
# print(jsCode)

fw = open("log/" + datetime.now().strftime('%Y-%m-%d_%H_%M_%S') + sys.argv[1].replace(".js", "").replace(".\\", "") + ".log",'w+',encoding='utf-8') # file object to write log

def message(message, data):
if message["type"] == 'send':
print(u"[*] {0}".format(message['payload']))
fw.write(u"[*] {0}\n".format(message['payload']))
fw.flush()
else:
print(message)


device = frida.get_usb_device()
# print(process)
pid = None
for a in device.enumerate_applications():
if a.identifier == "com.huawei.smarthome":
pid = a.pid # find the process by its' package name and get its' pid
break
process = device.attach(pid)
script= process.create_script(jsCode)
script.on("message", message)
script.load()
sys.stdin.read()

hook之前需要先adb[7][8]进入到设备的shell里然后启动frida-server

1
2
3
adb shell  # 先进入设备的shell
oriole:/ $ su # 切换到root
oriole:/ # /data/local/tmp/frida-server-16.1.10-android-arm64 & # 加上&,以后台方式运行frida-server

注意要有root权限,并且frida-server必须是对应的架构,和pc端的frida版本是否必须保持一致我没有确认,但是,if you can, why not。

然后手动启动应用,再启动python脚本文件开始hook就可以了

1
2
conda activate hook
python .\hook.py .\javaCipherAIO.js

References


记Android某APP逆向
http://zr4in.github.io/2024/04/16/记Android某APP逆向/
作者
zr4in
发布于
2024年4月16日
许可协议