frida的使用

环境搭建

配环境好麻烦好麻烦的!!!哎

为了方便学习,以下所有的东西都打包到了阿里云盘:

frida_server.txt https://www.aliyundrive.com/s/RwsGa937eh9
去010修改文件头为PK(50 4B 03 04)然后修改后缀为zip解压

注意版本!!一定要注意版本!

本人环境:

Python 3.8

xiaomi9 root android 12

Frida版本: 14.2.18

tips:另外好像有python的要求,我个人是建议使用python3.8(最好是3.8.6之前的版本)

虽然差别不大,但是本人亲身经历,3.8.6之后的python打包的exe程序就用uncompyle6反编译不出来了!

下面都是按照python8展开的环境配置

frida和frida-tools

pip install frida

pip install frida-tools

python3.8配置

直接安装python3.8.3傻瓜式安装就可以了,注意自动添加PATH环境变量就好

webstorm安装

用来写js代码,一个简单的js编辑器(其实没有也行,主要是可以省很多事

另外安装一下nodejs,这是运行js必不可少的

然后用webstorm新建一个项目,在项目的文件夹下载一下frida的文件

命令:npm i @types/frida-gum

配置好这些之后,就要下载好frida-server-14.2.18-android-arm64这个文件

把frida-server-14.2.18-android-arm64给push进去

然后运行就行,具体不多说啦!

frida在js代码上的使用

首先开启windows端的server

image-20231111151201570

代码分析

然后就是用jadx看一下嘟嘟牛的反编译代码部分,通过搜索encrypt,可以看到有两个加密的类,

image-20231111151619292

第一个类:addRequestMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void addRequestMap(Map<String, String> addMap, int a) {
String time = System.currentTimeMillis() + "";
if (addMap == null) {
addMap = new HashMap<>();
}
addMap.put("timeStamp", time);
String code = RequestUtil.paraMap(addMap, Config.BASE_APPEND, "sign");
String encrypt = RequestUtil.encodeDesMap(code, this.desKey, this.desIV);
JSONObject obj = new JSONObject();
try {
obj.put("Encrypt", encrypt);
this.mRequestBody = obj + "";
} catch (JSONException e) {
e.printStackTrace();
}

第二个类:paraMap

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
private void paraMap(Map<String, String> addMap) {
String encrypt;
String time = System.currentTimeMillis() + "";
if (addMap == null) {
addMap = new HashMap<>();
}
if (this.useDes) {
addMap.put("timeStamp", time);
if (DodonewOnlineApplication.loginUser != null) {
addMap.put("userId", DodonewOnlineApplication.loginUser.getUserId());
if (TextUtils.isEmpty(DodonewOnlineApplication.devId)) {
DodonewOnlineApplication.devId = Utils.getDevId(DodonewOnlineApplication.getAppContext());
}
addMap.put("imei", "Android" + DodonewOnlineApplication.devId);
}
String code = RequestUtil.paraMap(addMap, Config.BASE_APPEND, "sign");
encrypt = RequestUtil.encodeDesMap(code, this.desKey, this.desIV);
} else {
encrypt = this.mGson.toJson(addMap);
}
JSONObject obj = new JSONObject();
try {
obj.put("Encrypt", encrypt);
this.mRequestBody = obj + "";
} catch (JSONException e) {
e.printStackTrace();
}
}

这里就直接拷贝gpt的回答:很明显的des加密,另一个也差不多

  1. useDestrue 时,使用 DES 加密算法:
    • 首先,将当前时间戳转换为字符串,并将其放入 addMap 中的 "timeStamp" 键。
    • 如果存在登录用户(DodonewOnlineApplication.loginUser 不为 null),将用户的 ID 放入 addMap 中的 "userId" 键。
    • 然后,如果设备 ID 为空(DodonewOnlineApplication.devId 为空),通过 Utils.getDevId(DodonewOnlineApplication.getAppContext()) 获取设备 ID,并将其放入 addMap 中的 "imei" 键。这里将设备 ID 加上前缀 "Android"
    • 使用 RequestUtil.paraMap() 方法对 addMap 进行处理,生成一个包含签名的字符串 code,并将其放入 addMap 中的 "sign" 键。
    • 使用 RequestUtil.encodeDesMap() 方法对 code 进行 DES 加密,使用指定的密钥 desKey 和初始向量 desIV,得到最终的加密结果 encrypt
  2. useDesfalse 时,直接使用 Gson 库将 addMap 对象转换为 JSON 字符串,并将其赋值给 encrypt

但是我们不知道这个apk走的是哪个加密,所以我们现在就用钩子来勾一下函数,测试一下:

js代码部分:

1
2
3
4
5
Java.perform(function () {});
如果是java层的hook,那就一定要用perform包裹起来
Java.use()
获取类名,要注意大小写

以下是一个测试代码。就是去hook掉JsonRequest的类,然后输出检查一下

1
2
3
4
5
6
Java.perform(function () {

var josnRequest=Java.use("com.dodonew.online.http.JsonRequest");
console.log("josnRequest",josnRequest);

});

然后去cmd注入一下: frida -UF -l test1.js

image-20231111154857481

可以看到,成功勾到了josnRequest的类,然后就试试其他:

1
2
3
josnRequest.paraMap.implementation=function (){

}

这个函数的作用是根据提供的代码,josnRequest.paraMap.implementation 是对 paraMap 方法进行重写的操作。具体来说,它将 paraMap 方法的实现替换为一个空函数体。这意味着当调用 josnRequest 对象的 paraMap 方法时,将不执行原始的 paraMap 方法的实现,而是执行上述代码中定义的空函数体。

如果是要重新调用原函数的话,也可以用this.paraMap()进行调用,运行

接下来就注入测试一下:

1
2
3
4
5
6
7
8
9
Java.perform(function () {

var josnRequest=Java.use("com.dodonew.online.http.JsonRequest");
console.log("勾到的类是:",josnRequest);
josnRequest.paraMap.implementation=function (a){
console.log("参数是:",a);
this.paraMap();
}
});

好好好,正好自动运行了,发现没有输出我的参数,所以可以断定没有调用我的那个类

image-20231111155835499

那就试试去钩一下另一个addRequest,但这个函数的参数有两个,就a,b

然后…..继续去注入一下,很好,寄,报错了!

重载函数

image-20231111160740890

大概问题是因为重载函数的问题。重载函数就是同一个类的同一个函数,但是传入类和参数是不一样的,大概是有三个重载函数,image-20231111161032842

在后面加上就好,然后在手机端输入账号和密码,就可以在终端看到我们的参数:

image-20231111161318357

成功!

然后就详细看一下这个函数的加密具体流程:

因为之前我们通过frida函数运行判断,找到了主要的加密的函数是:addRequestMap 函数,所以就集中分析一下这个函数

1
2
String encrypt = RequestUtil.encodeDesMap(code, this.desKey, this.desIV);

这是一个大的函数 RequestUtil.encodeDesMap(),然后传入了三个参数,分别是:code, this.desKey, this.desIV
然后这里面的第一次参数code,是上面的一个函数:
String code = RequestUtil.paraMap(addMap, Config.BASE_APPEND, “sign”);
code也是一个函数的值,这个函数有三个变量:addMap, Config.BASE_APPEND, “sign”

所以就先看看这个code是怎么来的,先看code的第一个调用函数:addMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static String paraMap(Map<String, String> addMap, String append, String sign) {
try {
Set<String> keyset = addMap.keySet();
StringBuilder builder = new StringBuilder();
List<String> list = new ArrayList<>();
for (String keyName : keyset) {
list.add(keyName + "=" + addMap.get(keyName));
}
Collections.sort(list);
for (int i = 0; i < list.size(); i++) {
builder.append(list.get(i));
builder.append("&");
}
builder.append("key=" + append);
String checkCode = Utils.md5(builder.toString()).toUpperCase();
addMap.put("sign", checkCode);
String result = new Gson().toJson(sortMapByKey(addMap));
Log.w(AppConfig.DEBUG_TAG, result + " result");
return result;
} catch (Exception e) {
e.printStackTrace();
return "";
}

值得注意的一个点是这个函数并不是之前的com.dodonew.online.http.JsonRequest类下的函数,而是com.dodonew.online.http.RequestUtil

下的函数所以是不能直接用之前的js代码,只能重新去hook

这里再对这个函数进行一下分析:

首先对传入的参数addMap进行一个遍历然后在后面加点东西

其次再进行一个拼接,然后转化成string,再然后转化成md5加缪

得到字节数组,然后转化成16进制的编码…….

然后把这个数值放到sign里面

不算很难理解的流程。

如果是想得到md5加密的数值的话,直接去逆或者是算,比较麻烦,还是一样·,通过frida去把程序hook出去,就可以看到数值的

找到了函数,并且是在com.dodonew.online.util.Utils类下的,直接去写hook代码,这里传入的是string,返回的也是string

image-20231113123406276

1
2
3
4
5
6
7
var utils=Java.use("com.dodonew.online.util.Utils");
utils.md5.implementation=function (a){
console.log("md5的加密之前数值是:",a);
var secret = this.md5(a);
console.log("md5加密之后的数值是:",secret);
return secret;
}

上面这个代码就不多解释了,就是一个简单的hook,然后输出,最后为了保证程序正常运行就加上了return’返回值

image-20231113124524101

得到了输出:

账号是: 1111111111
密码是: 1111111111
md5的加密之前数值是: equtype=ANDROID&loginImei=Androidnull&timeStamp=1699850537421&userPwd=1111111111&username=1111111111&key=sdlkjsdljf0j2fsjk
md5加密之后的数值是: 8d030c8406caf188ad02388465b363c5

前面是固定格式,然后timeStamp应该是一个时间戳,然后账号密码,还有key的数值

至于这些字符串是怎么来的,大概就是上面分析的那个加密吧

说完了RequestUtil.paraMap,得到的code值,然后就看看他这个md5之后的数值是怎么继续进行des加密的

看 RequestUtil.encodeDesMap

1
2
3
4
5
6
7
8
9
public static String encodeDesMap(String data, String desKey, String desIV) {
try {
DesSecurity ds = new DesSecurity(desKey, desIV);
return ds.encrypt64(data.getBytes("UTF-8"));
} catch (Exception e) {
e.printStackTrace();
return "";
}
}

这个加密传入了三个string的数据:(String data, String desKey, String desIV)

建议hook来查看具体的key和iv,因为他很有可能在运行过程中把数值发生改变

写一个hook代码查看一下:

1
2
var RequestUtil=Java.use("com.dodonew.online.http.RequestUtil");
RequestUtil.encodeDesMap.implementation=function (a,b,c){

果然,报错了,又是重载函数

image-20231113131650876

就找复合输入值的那个,加在后面就行

完整函数:

1
2
3
4
5
6
7
8
9
var RequestUtil=Java.use("com.dodonew.online.http.RequestUtil");
RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation=function (a,b,c){
console.log("data:",a);
console.log("desKe:",b);
console.log("desIV:",c);
var retval = this.encodeDesMap(a,b,c);
console.log("结果是:",retval);
return retval;
}

看一下输出的结果:所以大体流程理解了

[MI 9 Transparent Edition::嘟嘟牛在线]-> 参数是: [object Object] 0
账号是: 11111111111
密码是: 22222222222
md5的加密之前数值是: equtype=ANDROID&loginImei=Androidnull&timeStamp=1699853155009&userPwd=22222222222&username=11111111111&key=sdlkjsdljf0j2fsjk
md5加密之后的数值是: b977d63aac03bc10091ab7677736e02b
md5加密之后的数值是: b977d63aac03bc10091ab7677736e02b
data: {“equtype”:”ANDROID”,”loginImei”:”Androidnull”,”sign”:”B977D63AAC03BC10091AB7677736E02B”,”timeStamp”:”1699853155009”,”userPwd”:”22222222222”,”username”:”11111111111”}
desKe: 65102933
desIV: 32028092
结果是: NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii/UNcdXYMk0EKKwKT83MJV1M
3opnnzgh/0WqmKhtngnsYDp8smOzTJTSIcT3TBxJsinUUebTJKy3t3FK69OEaP7CYlIt888QO6L+
lVJYenmhcM4b7sm+IOxxAiY/8COm/dQ4sEkbUL/Y7edhg6LEAuSK0xDAhHRE/bj6u7AB3OxP

fridaHOOK构造函数:

​ 构造函数(Constructor)是面向对象编程中的一个特殊方法,用于创建和初始化类的实例(对象)。它通常在对象创建的过程中自动被调用,用于执行必要的初始化操作。

说白了,如果拿python举例子的话,比如在python中使用了 import base64 导入了这个功能,然后再去调用. base64.b64decode()函数就不需要自己去写的类了,所以在hook的时候,好像是点不进去的(个人理解,有错的话欢迎指出)

这样的话就要用到frida的功能–构造函数

比如:image-20231113163807899

1
DESKeySpec(md.digest());

这个函数就是系统的构造函数了,当然也是可以hook的!用这个$init来代替本来的函数名(好像用原来的也可以)

1
2
3
4
5
6
var desKeySpec=Java.use("javax.crypto.spec.DESKeySpec");
desKeySpec.$init.implementation=function (a){
console.log("构造函数的输入是:",a);
this.$init(a);

}

image-20231113164501266

小问题,重载函数,选择一个参数的就行,经过调试之后,运行成功

image-20231113171333673

但是他输出的object,这个问题困扰了我很久,额,现在知道了,原来是因为他…..是字节数组,无法输出!

主动调用

就是我们在写脚本的时候,主动调用Android里面的方法,大题和hook的方法差不多

因为字节数组无法输出,所以我们可以去主动调用Android的方法,把字节转化成base编码,具体的语法如下:

1
var base64 =Java.use("android.util.Base64");

去主动调用base64的函数

然后在下面就可以直接调用;

1
2
3
4
5
6
var base64 =Java.use("android.util.Base64");

var desKeySpec=Java.use("javax.crypto.spec.DESKeySpec");
desKeySpec.$init.overload('[B').implementation=function (a){
console.log("构造函数的输入是:",base64.encodeToString(a,0));
this.$init(a);

然后就有正常的输出;

image-20231113180950803

另外也可以调用其他的类的函数,在前面钩子的基础上,加上一个函数运行和参数

1
requestUtil.encodeDesMap("hello","iamhaha","hahahaha");

image-20231113190822238

总结:

代码:

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
Java.perform(function () {

var josnRequest=Java.use("com.dodonew.online.http.JsonRequest");
console.log("josnRequest",josnRequest);
josnRequest.addRequestMap .overload('java.util.Map', 'int').implementation=function (a,b){
console.log("参数是:",a,b);
console.log("账号是:",a.get("username"));
console.log("密码是:",a.get("userPwd"));
this.addRequestMap(a,b);

}
var utils=Java.use("com.dodonew.online.util.Utils");
utils.md5.implementation=function (a){
console.log("md5的加密之前数值是:",a);
var secret = this.md5(a);
console.log("md5加密之后的数值是:",secret);
return secret;
}
var requestUtil=Java.use("com.dodonew.online.http.RequestUtil");
requestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation=function (a,b,c){
console.log("勾到了类:",requestUtil);
console.log("data:",a);
console.log("deskey:",b);
console.log("desiv:",c);
var secret1 = this.encodeDesMap(a,b,c);
console.log("加密结果是:",secret1);
return secret1;
}
var base64 =Java.use("android.util.Base64");

var desKeySpec=Java.use("javax.crypto.spec.DESKeySpec");
desKeySpec.$init.overload('[B').implementation=function (a){
console.log("构造函数的输入是:",base64.encodeToString(a,0));
this.$init(a);


}

});

代码输出:

[MI 9 Transparent Edition::嘟嘟牛在线]-> 参数是: [object Object] 0
账号是: 11112222333
密码是: woshimima
md5的加密之前数值是: equtype=ANDROID&loginImei=Androidnull&timeStamp=1699870290703&userPwd=woshimima&username=11112222333&key=sdlkjsdljf0j2fsjk
md5加密之后的数值是: f47d70a0d19de14449c66eada93469af
勾到了类: <class: com.dodonew.online.http.RequestUtil>
data: {“equtype”:”ANDROID”,”loginImei”:”Androidnull”,”sign”:”F47D70A0D19DE14449C66EADA93469AF”,”timeStamp”:”1699870290703”,”userPwd”:”woshimima”,”username”:”11112222333”}
deskey: 65102933
desiv: 32028092
构造函数的输入是: w9JtyoYll4K9kYZIH4kq5g==

加密结果是: NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii/UNcdXYMk0Egsgxk5jT/u+3
JC3j4TS00tIk3gtA2ZxdIEkTyrSpJ8z12bUOwEAlD8X77yMdklkBknFFsoRfdi9ycqZX5r4w+K8O
9PYfRF+QnU/3qxSyn/KieEJUy2EsfXxHbi7gOZuveR0o6Egkf7fS+mFcjPgTnGw09S0ylX+5

构造函数的输入是: w9JtyoYll4K9kYZIH4kq5g==