一、Frida介绍

摘抄自吾爱破解大佬:《安卓逆向这档事》十三、是时候学习一下Frida一把梭了(上) - 吾爱破解 - 52pojie.cn

1.什么是Frida?

Frida 是一款开源的动态插桩工具,可以插入一些代码到原生App的内存空间去动态地监视和修改其行为,支持Windows、Mac、Linux、Android或者iOS,从安卓层面来讲,可以实现Java层和NativeHook操作。

Hook操作就是一个类似动态插桩的功能,可以在app执行的任意过程中插入或修改一些代码以及参数和返回值,来修改程序的执行逻辑。

2.Frida原理及重要组件

frida注入的原理就是找到目标进程,使用ptrace跟踪目标进程获取mmap,dlpoen,dlsym等函数库的偏移获取mmap在目标进程申请一段内存空间将在目标进程中找到存放frida-agent-32/64.so的空间启动执行各种操作由agent去实现

组件名称 功能描述
frida-gum 提供了inline-hook的核心实现,还包含了代码跟踪模块Stalker,用于内存访问监控的MemoryAccessMonitor,以及符号查找、栈回溯实现、内存扫描、动态代码生成和重定位等功能
frida-core fridahook的核心,具有进程注入、进程间通信、会话管理、脚本生命周期管理等功能,屏蔽部分底层的实现细节并给最终用户提供开箱即用的操作接口。包含了frida-server、frida-gadget、frida-agent、frida-helper、frida-inject等关键模块和组件,以及之间的互相通信底座
frida-gadget 本身是一个动态库,可以通过重打包修改动态库的依赖或者修改smali代码去实现向三方应用注入gadget,从而实现Frida的持久化或免root
frida-server 本质上是一个二进制文件,类似于前面学习到的android_server,需要在目标设备上运行并转发端口,在Frida hook中起到关键作用

3.Frida与Xposed的对比

工具 优点 缺点
Xposed 直接编写Java代码,Java层hook方便,可打包模块持久化hook 环境配置繁琐,兼容性较差,难以Hook底层代码。
Frida 配置简单,免重启hook。支持Java层和Native层的hook操作 持久化hook相对麻烦

4.Frida环境配置

1、安装Python

我的python是3.8版本

2、安装frida和frida-tools。

frida` 是 Frida 工具链的核心组件,实际上是一个跨平台的动态分析框架。它通过在目标进程中注入 JavaScript 代码,使得开发者可以直接与目标应用进行交互,修改其行为或收集调试信息。Frida 支持多种平台,包括 Windows、macOS、Linux、Android、iOS 等。

frida-tools 是 Frida 的一组命令行工具,目的是为了让用户可以方便地使用 Frida 进行操作。它封装了 Frida 的功能,提供了简单易用的命令行接口,帮助用户进行动态分析和调试。

需要安装和pyton对应版本的frida。

这里安装的是frida是14.2.18,frida-tools是9.2.5(需要在官网查询后安装指定版本https://github.com/frida/frida/releases/tag/14.2.18)

fridaAndroid以及python的对应关系:

1
2
3
4
frida12.3.6            Android5-6       python3.7
frida12.8.0 Android7-8 python3.8
frida14+ Android9+ python3.8
frida15+ Android12 也可以用

因为测试机piexl xl是Android10,因此使用的python是3.8,firda是14+。

3.安装frida-server

同样在relaease页面,找到frida-server-14.2.18-android-arm64.xz下载后push到安卓的data/local/tmp下,改名fr(这里是因为有时候app会对frida名字进行检查),并添加可执行权限,hook的时候需要启动该fr,并且不能关闭命令框

5.Frida使用

1.基础指令

1.frida-ps -U 查看当前手机运行的进程
2.frida-ps –help 查看help指令

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
frida-ps --help
使用方式: frida-ps [选项]

选项:
-h, --help 显示帮助信息并退出
-D ID, --device ID 连接到具有给定ID的设备
-U, --usb 连接到USB设备
-R, --remote 连接到远程frida-server
-H HOST, --host HOST 连接到HOST上的远程frida-server
--certificate CERTIFICATE
与HOST进行TLS通信,期望的CERTIFICATE
--origin ORIGIN 连接到设置了"Origin"头为ORIGIN的远程服务器
--token TOKEN 使用TOKEN验证HOST
--keepalive-interval INTERVAL
设置心跳包间隔(秒),或设置为0以禁用(默认为-1,根据传输方式自动选择)
--p2p 与目标建立点对点连接
--stun-server ADDRESS
设置与--p2p一起使用的STUN服务器地址
--relay address,username,password,turn-{udp,tcp,tls}
添加与--p2p一起使用的中继
-O FILE, --options-file FILE
包含额外命令行选项的文本文件
--version 显示程序版本号并退出
-a, --applications 只列出应用程序
-i, --installed 包括所有已安装的应用程序
-j, --json 以JSON格式输出结果

2.操作模式:

操作模式 描述 优点 主要用途
CLI(命令行)模式 通过命令行直接将JavaScript脚本注入进程中,对进程进行操作 便于直接注入和操作 在较小规模的操作或者需求比较简单的场景中使用
RPC模式 使用Python进行JavaScript脚本的注入工作,实际对进程进行操作的还是JavaScript脚本,可以通过RPC传输给Python脚本来进行复杂数据的处理 在对复杂数据的处理上可以通过RPC传输给Python脚本来进行,有利于减少被注入进程的性能损耗 在大规模调用中更加普遍,特别是对于复杂数据处理的需求

3.注入模式与启动命令:

注入模式 描述 命令或参数 优点 主要用途
Spawn模式 将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App 在CLI模式中,Frida通过加上 -f 参数指定包名以spawn模式操作App 适合于需要在App启动时即进行注入的场景,可以在App启动时即捕获其行为 当需要监控App从启动开始的所有行为时使用
Attach模式 在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作 在CLI模式中,如果不添加 -f 参数,则默认会通过attach模式注入App 适合于已经运行的App,不会重新启动App,对用户体验影响较小 在App已经启动,或者我们只关心特定时刻或特定功能的行为时使用

Spawn模式:

1
frida -U -f 进程名 -l hook.js

attach模式 :

1
frida -U 进程名 -l hook.js

frida_server自定义端口

1
2
3
4
5
frida server 默认端口:27042        //有时app会占用该端口以绕过frida,此时需要自定义一个端口

taimen:/ $ su
taimen:/ # cd data/local/tmp/
taimen:/data/local/tmp # ./fs1280 -l 0.0.0.0:6666

logcat |grep "D.zj2595"日志捕获
adb connect 127.0.0.1:62001模拟器端口转发

4.基础语法

API名称 描述
Java.use(className) 获取指定的Java类并使其在JavaScript代码中可用。
Java.perform(callback) 确保回调函数在Java的主线程上执行。
Java.choose(className, callbacks) 枚举指定类的所有实例。
Java.cast(obj, cls) 将一个Java对象转换成另一个Java类的实例。
Java.enumerateLoadedClasses(callbacks) 枚举进程中已经加载的所有Java类。
Java.enumerateClassLoaders(callbacks) 枚举进程中存在的所有Java类加载器。
Java.enumerateMethods(targetClassMethod) 枚举指定类的所有方法。

5.日志输出语法区别

日志方法 描述 区别
console.log() 使用JavaScript直接进行日志打印 多用于在CLI模式中,console.log()直接输出到命令行界面,使用户可以实时查看。在RPC模式中,console.log()同样输出在命令行,但可能被Python脚本的输出内容掩盖。
send() Frida的专有方法,用于发送数据或日志到外部Python脚本 多用于RPC模式中,它允许JavaScript脚本发送数据到Python脚本,Python脚本可以进一步处理或记录这些数据。

6.Hook框架模板

1
2
3
4
5
6
function main(){
Java.perform(function(){
hookTest1();
});
}
setImmediate(main);

6.Frida常用API

1.Hook普通方法、打印参数和修改返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//定义一个名为hookTest1的函数
function hookTest1(){
//获取一个名为"类名"的Java类,并将其实例赋值给JavaScript变量utils
var utils = Java.use("类名");
//修改"类名"的"method"方法的实现。这个新的实现会接收两个参数(a和b)
utils.method.implementation = function(a, b){
//将参数a和b的值改为123和456。
a = 123;
b = 456;
//调用修改过的"method"方法,并将返回值存储在`retval`变量中
var retval = this.method(a, b);
//在控制台上打印参数a,b的值以及"method"方法的返回值
console.log(a, b, retval);
//返回"method"方法的返回值
return retval;
}
}

2.Hook重载参数

这里需要注意,hook重载函数的时候需要加overload指定,不知道也可以直接运行,frida的报错会给出提示。

1
2
3
4
5
6
7
8
9
10
11
12
// .overload()        
// .overload('自定义参数')
// .overload('int')
function hookTest2(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//overload定义重载函数,根据函数的参数类型填
utils.Inner.overload('com.zj.wuaipojie.Demo$Animal','java.lang.String').implementation = function(a,b){
b = "aaaaaaaaaa";
this.Inner(a,b);
console.log(b);
}
}

3.Hook构造函数

1
2
3
4
5
6
7
8
9
function hookTest3(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改类的构造函数的实现,$init表示构造函数
utils.$init.overload('java.lang.String').implementation = function(str){
console.log(str);
str = "52";
this.$init(str);
}
}

4.Hook字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hookTest5(){
Java.perform(function(){
//静态字段的修改
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改类的静态字段"flag"的值
utils.staticField.value = "我是被修改的静态变量";
console.log(utils.staticField.value);
//非静态字段的修改
//使用`Java.choose()`枚举类的所有实例
Java.choose("com.zj.wuaipojie.Demo", {
onMatch: function(obj){
//修改实例的非静态字段"_privateInt"的值为"123456",并修改非静态字段"privateInt"的值为9999。
obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线
obj.privateInt.value = 9999;
},
onComplete: function(){

}
});
});

}

5.Hook内部类

1
2
3
4
5
6
7
8
9
10
11
function hookTest6(){
Java.perform(function(){
//内部类
var innerClass = Java.use("com.zj.wuaipojie.Demo$innerClass");
console.log(innerClass);
innerClass.$init.implementation = function(){
console.log("eeeeeeee");
}

});
}

6.枚举所有的类与类的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookTest7(){
Java.perform(function(){
//枚举所有的类与类的所有方法,异步枚举
Java.enumerateLoadedClasses({
onMatch: function(name,handle){
//过滤类名
if(name.indexOf("com.zj.wuaipojie.Demo") !=-1){
console.log(name);
var clazz =Java.use(name);
console.log(clazz);
var methods = clazz.class.getDeclaredMethods();
console.log(methods);
}
},
onComplete: function(){}
})
})
}

7.枚举所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hookTest8(){
Java.perform(function(){
var Demo = Java.use("com.zj.wuaipojie.Demo");
//getDeclaredMethods枚举所有方法
var methods =Demo.class.getDeclaredMethods();
for(var j=0; j < methods.length; j++){
var methodName = methods[j].getName();
console.log(methodName);
for(var k=0; k<Demo[methodName].overloads.length;k++){
Demo[methodName].overloads[k].implementation = function(){
for(var i=0;i<arguments.length;i++){
console.log(arguments[i]);
}
return this[methodName].apply(this,arguments);
}
}
}
})
}

8.主动调用

静态方法

1
2
var ClassName=Java.use("com.zj.wuaipojie.Demo"); 
ClassName.privateFunc("传参");

非静态方法

1
2
3
4
5
6
7
8
9
10
11
12
var ret = null;
Java.perform(function () {
Java.choose("com.zj.wuaipojie.Demo",{ //要hook的类
onMatch:function(instance){
ret=instance.privateFunc("aaaaaaa"); //要hook的方法
},
onComplete:function(){
//console.log("result: " + ret);
}
});
})
//return ret;

这里讲一个CTF中安卓逆向的技巧,如果app内存在一个flag_activity,但是main_activity中没有对该函数进行调用,可以使用如下命令启动该activity。

1
2
3
adb shell
su
am start -n com.droidlearn.activity_travel/.FlagActivity

7.frida辅助逆向分析嘟嘟牛

1.抓包分析

首先使用HttpCanary抓包嘟嘟牛,输入手机号和密码后点击登录按钮,抓到login包如下:

1
2
3
4
5
6
7
8
9
10
POST /api/user/login HTTP/1.1
If-Modified-Since: Sun, 24 Nov 2024 13:31:54 GMT
Content-Type: application/json; charset=utf-8
User-Agent: Dalvik/2.1.0 (Linux; U; Android 10; Pixel XL Build/QP1A.191005.007.A3)
Host: api.dodovip.com
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 250

{"Encrypt":"NIszaqFPos1vd0pFqKlB42Np5itPxaNH\/\/FDsRnlBfgL4lcVxjXii\/UNcdXYMk0Egsgxk5jT\/u++\nFScUC0CWLqgQd5Ch0CGNlWbC31xZEvotZVwDxb4yfwXObnJclSJqRmUD9r4JeX2iP8wa2TLNGQ7v\nlHkV7rgE82NSubt\/+Bq3PrOC+sdWZSZooFXh19hqgYFyxQlXOBPLFCzBPku+q5Er5o4JZov\/\n"}

这里的Encrypt是post请求的内容,因此我们要逆向分析他的加密流程,并复现加密算法,最后复现协议,使用python的request发包即可模拟实现用户登录操作,以此来实现批量登录

2.静态分析&frida动态辅助分析

1、首先使用jadx打开,通过搜索字符串Encrypt来定位。

找到如下两处,一个addRequestMap,另一个paraMap,由于不知道具体是调用的哪个方法或者都调用了,因此使用frida来辅助分析。

pAhu5Kx.png

用frida实现两个函数并加入console.log打印内容来分析程序是否执行了该函数。

hook代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java.perform(function(){
var jsonRequest = Java.use("com.dodonew.online.http.JsonRequest");
//paraMap
jsonRequest.paraMap.implementation = function(addMap)
{
console.log("paraMap params: " + addMap);
var res = this.addRequestMap(addMap);
return res;
}
//addRequestMap
jsonRequest.addRequestMap.overload('java.util.Map', 'int').implementation = function(addMap,a)
{
var hashmap = Java.cast(addMap,Java.use("java.util.HashMap"))
console.log("addRequestMap params: " + hashmap.toString());
var res = this.addRequestMap(addMap,a);
return res;
}
})

frida输出如下:

1
addRequestMap params: {loginImei=Androidnull, equtype=ANDROID, userPwd=123456, username=17860581502}

因此,可以得知程序只走了addRequestMap函数。

2、定位到addRequestMap,静态分析代码

源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
}
}

由上方addRequestMap的输出可知,参数就是一个map集合,包括固定的loginImei=Androidnull, equtype=ANDROID以及后边的用户名(其实就是手机号)和密码。

这里的大致逻辑是获取当前时间,并将时间加入到集合中。之后调用RequestUtil.paraMap函数对addMap进行处理,获取一个code。

之后,把code传给RequestUtil.encodeDesMap进行Des加密。

3、跟进RequestUtil.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
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 "";
}
}

首先使用frida hook该函数获取参数内容。

hook代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
//requestUtil.paraMap
var requestUtil = Java.use("com.dodonew.online.http.RequestUtil");
requestUtil.paraMap.overload('java.util.Map', 'java.lang.String', 'java.lang.String').implementation = function(addMap,key,str)
{
var hashmap = Java.cast(addMap,Java.use("java.util.HashMap"))
console.log("requestUtil.paraMap param1: " + hashmap.toString())
console.log("requestUtil.paraMap param2: " + key)
console.log("requestUtil.paraMap param3: " + str)
var res = this.paraMap(addMap,key,str);
console.log("requestUtil.paraMap res: " + res)
return res;
}

输出如下:

1
2
3
4
requestUtil.paraMap param1: {timeStamp=1732455562761, loginImei=Androidnull, equtype=ANDROID, userPwd=123456, username=17860581502}
requestUtil.paraMap param2: sdlkjsdljf0j2fsjk
requestUtil.paraMap param3: sign
requestUtil.paraMap res: {"equtype":"ANDROID","loginImei":"Androidnull","sign":"FDA8AF1573CEDF84AF1DCC521609E92F","timeStamp":"1732455562761","userPwd":"123456","username":"17860581502"}

且每次运行都一样,可知参数2和参数3是固定的字符串。

之后分析逻辑,大致为将map集合中各项以字符串方式拼接起来后进行md5加密,之后再转大写得到checkCode,并把checkCode作为值,sign作为键放到map里,返回map。

4、跟进md5验证

frida hook一下md5函数,看看requestUtil.paraMap返回时的参数sign是不是多个拼接后的md5_大写。

hook代码如下:

1
2
3
4
5
6
7
8
9
//md5
var utils = Java.use("com.dodonew.online.util.Utils")
utils.md5.implementation = function(a)
{
console.log("md5 param: " + a)
var res = this.md5(a)
console.log("md5 res: " + res)
return res
}

输出如下:

1
2
md5 param: equtype=ANDROID&loginImei=Androidnull&timeStamp=1732455562761&userPwd=123456&username=17860581502&key=sdlkjsdljf0j2fsjk 
md5 res: fda8af1573cedf84af1dcc521609e92f

可知参数确实是各项拼接的,并且使用CyberChef等验证可知为正常的md5,未经魔改。

5、分析RequestUtil.encodeDesMap函数

源代码如下:

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
//function1
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 "";
}
}


//function2
public class DesSecurity {
Cipher deCipher;
Cipher enCipher;

public DesSecurity(String key, String iv) throws Exception {
if (key == null) {
throw new NullPointerException("Parameter is null!");
}
InitCipher(key.getBytes(), iv.getBytes());
}

private void InitCipher(byte[] secKey, byte[] secIv) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(secKey);
DESKeySpec dsk = new DESKeySpec(md.digest());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(dsk);
IvParameterSpec iv = new IvParameterSpec(secIv);
this.enCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
this.deCipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
this.enCipher.init(1, key, iv);
this.deCipher.init(2, key, iv);
}

public String encrypt64(byte[] data) throws Exception {
return Base64.encodeToString(this.enCipher.doFinal(data), 0);
}

public byte[] decrypt64(String data) throws Exception {
return this.deCipher.doFinal(Base64.decode(data, 0));
}
}

分析加密逻辑,大致为首先对传入的key进行md5加密,之后初始化了一个SecretKeyFactory对象来进行EES加密,模式为DES/CBC/PKCS5Padding,最后的encrypt64即使用该DES加密器通过key,iv对data进行DES加密并转成base64字节串。

那么,首先hook一下encodeDesMap得到参数key和iv。

hook代码如下:

1
2
3
4
5
6
7
8
9
10
//requestUtil.encodeDesMap
requestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String','java.lang.String').implementation = function(code,key,iv)
{
console.log("encodeDesMap param1: " + code)
console.log("encodeDesMap param2: " + key)
console.log("encodeDesMap param3: " + iv)
var res = this.encodeDesMap(code,key,iv)
console.log("encodeDesMap res: " + res)
return res
}

输出如下:

1
2
3
4
5
6
encodeDesMap param1: {"equtype":"ANDROID","loginImei":"Androidnull","sign":"FDA8AF1573CEDF84AF1DCC521609E92F","timeStamp":"1732455562761","userPwd":"123456","username":"17860581502"}
encodeDesMap param2: 65102933
encodeDesMap param3: 32028092
encodeDesMap res: NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii/UNcdXYMk0Egsgxk5jT/u++
FScUC0CWLqgQd5Ch0CGNlWbC31xZEvotZVwDxb4yfwXObnJclSJqRmUD9r4JeX2iP8wa2TLNGQ7v
lHkV7rgE82NSubt/+Bq3PrOC+sdWZSZooFXh19hqgYFyxQlXOBPLFCzBPku+q5Er5o4JZov/

可以发现这里最后的返回值就是我们抓包获得的post的内容(略微不一样是因为抓包得到的有些字符进行了url转义编码),并且key和iv也是程序中的已初始化变量,是固定的。

那么到此,我们就一步步分析完了所有的加密流程。

3.算法还原

接下来就模拟app的逻辑来还原加密算法。

  • 首先是将时间戳和一些输入信息以及一些固有值组成一个map集合。

  • 之后使用字符串拼接各键值对并进行md5加密后转大写获取sign值,再加入到map集合中。

  • 将集合转成字符串,通过DES进行加密,这里的key是经过md5加密后的(由于是DES加密,所以会取前八位)。

  • DES加密后转成base64字节串。

这里选择使用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
import base64
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad
import hashlib

time = "1732455562761"
username = "17860581502"
password = "123456"
key = "65102933"
iv = "32028092"
def get_sign(username,password):
data = "equtype=ANDROID&loginImei=Androidnull&timeStamp=" + time + "&userPwd=" + password + "&username=" + username + "&key=sdlkjsdljf0j2fsjk"
code = hashlib.md5(data.encode('utf-8')).hexdigest()
return str(code).upper()

def DesEncode(code,key,iv):
data = code.encode('utf-8')
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
key_byte = bytes.fromhex(key_md5)
iv_byte = iv.encode('utf-8')
cipher = DES.new(key_byte[:8], DES.MODE_CBC, iv_byte)
padded_data = pad(data, DES.block_size)
ciphertext = cipher.encrypt(padded_data)
ciphertext_base64 = base64.b64encode(ciphertext)
return ciphertext_base64

code = get_sign(username,password)
code_ = '{"equtype":"ANDROID","loginImei":"Androidnull","sign":"' + code + '","timeStamp":"' + time + '","userPwd":"' + password + '","username":"' + username + '"}'
res_byte = DesEncode(code_,key,iv)
res_string = res_byte.decode('utf-8')
print(res_string)
#NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii/UNcdXYMk0Egsgxk5jT/u++FScUC0CWLqgQd5Ch0CGNlWbC31xZEvotZVwDxb4yfwXObnJclSJqRmUD9r4JeX2iP8wa2TLNGQ7vlHkV7rgE82NSubt/+Bq3PrOC+sdWZSZooFXh19hqgYFyxQlXOBPLFCzBPku+q5Er5o4JZov/

完成,结果与抓到的请求包中的数据一致!

4.协议还原

最后进行协议还原,使用requests进行post发包模拟用户登录操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
import json

url = "http://api.dodovip.com/api/user/login"
data = json.dumps({"Encrypt": res_string})
print(data)
headers = {
"content-type": "application/json; charset=utf-8",
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 10; Pixel XL Build/QP1A.191005.007.A3)"
}
r = requests.post(url = url,data = data,headers = headers)
recv = r.text
print(recv)

得到的recv输出如下:

1
2v+DC2gq7RuAC8PE5GZz5wH3/y9ZVcWhFwhDY9L19g9iEd075+Q7xwewvfIN0g0ec/NaaF43/S0=     

对比抓包得到的响应体内容,两者一致。

pAhuIr6.png

对该响应的再调用DES解密得到如下:

DES解密代码如下:

1
2
3
4
5
6
7
8
9
10
11
def DesDecode(code,key,iv):
data = code.encode('utf-8')
data = base64.b64decode(data)
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
key_byte = bytes.fromhex(key_md5)
iv_byte = iv.encode('utf-8')
cipher = DES.new(key_byte[:8], DES.MODE_CBC, iv_byte)
decrypted_data = cipher.decrypt(data)
decrypted_data = unpad(decrypted_data, DES.block_size)
return decrypted_data.decode('utf-8')
print(DesDecode(recv,key,iv))

结果如下:

1
{"code":-1,"message":"账号或密码错误","data":{}}

如果,post发送的data不对,recv输出如下:

1
2
2v+DC2gq7RsxFUyk6XO+ZO/zrkhUqGNy
解密后为 {"code":4,"message":4}

另外,后端可能对时间做了检测,如果请求包中的时间戳与当前实际时间相差太大的话,即使伪造的Encrypt数据正确,也不会成功。

此时recv输出如下:

1
2
2v+DC2gq7Rs2vBLjHBwgrO0gyauGMTE6
解密后为 {"code":2,"message":2}

5.完整exp:

1、hook.js

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
Java.perform(function(){
var jsonRequest = Java.use("com.dodonew.online.http.JsonRequest");
//paraMap
jsonRequest.paraMap.implementation = function(addMap)
{
console.log("paraMap params: " + addMap);
var res = this.addRequestMap(addMap);
return res;
}
//addRequestMap
jsonRequest.addRequestMap.overload('java.util.Map', 'int').implementation = function(addMap,a)
{
var hashmap = Java.cast(addMap,Java.use("java.util.HashMap"))
console.log("addRequestMap params: " + hashmap.toString());
var res = this.addRequestMap(addMap,a);
return res;
}

//md5
var utils = Java.use("com.dodonew.online.util.Utils")
utils.md5.implementation = function(a)
{
console.log("md5 param: " + a)
var res = this.md5(a)
console.log("md5 res: " + res)
return res
}

//requestUtil.paraMap
var requestUtil = Java.use("com.dodonew.online.http.RequestUtil");
requestUtil.paraMap.overload('java.util.Map', 'java.lang.String', 'java.lang.String').implementation = function(addMap,key,str)
{
var hashmap = Java.cast(addMap,Java.use("java.util.HashMap"))
console.log("requestUtil.paraMap param1: " + hashmap.toString())
console.log("requestUtil.paraMap param2: " + key)
console.log("requestUtil.paraMap param3: " + str)
var res = this.paraMap(addMap,key,str);
console.log("requestUtil.paraMap res: " + res)
return res;
}

//requestUtil.encodeDesMap
requestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(code,key,iv)
{
console.log("encodeDesMap param1: " + code)
console.log("encodeDesMap param2: " + key)
console.log("encodeDesMap param3: " + iv)
var res = this.encodeDesMap(code,key,iv)
console.log("encodeDesMap res: " + res)
return res
}

})

2、main.py

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
import base64
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad
from Crypto.Util.Padding import unpad
import hashlib
import requests
import json

time = "1732458560882" //这里需要用frida hook获取到的时间戳替换,否则返回{"code":2,"message":2}
username = "17860581502"
password = "123456"
key = "65102933"
iv = "32028092"
def get_sign(username,password):
data = "equtype=ANDROID&loginImei=Androidnull&timeStamp=" + time + "&userPwd=" + password + "&username=" + username + "&key=sdlkjsdljf0j2fsjk"
code = hashlib.md5(data.encode('utf-8')).hexdigest()
return str(code).upper()

def DesEncode(code,key,iv):
data = code.encode('utf-8')
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
key_byte = bytes.fromhex(key_md5)
iv_byte = iv.encode('utf-8')
cipher = DES.new(key_byte[:8], DES.MODE_CBC, iv_byte)
padded_data = pad(data, DES.block_size)
ciphertext = cipher.encrypt(padded_data)
return base64.b64encode(ciphertext)

def DesDecode(code,key,iv):
data = code.encode('utf-8')
data = base64.b64decode(data)
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
key_byte = bytes.fromhex(key_md5)
iv_byte = iv.encode('utf-8')
cipher = DES.new(key_byte[:8], DES.MODE_CBC, iv_byte)
decrypted_data = cipher.decrypt(data)
decrypted_data = unpad(decrypted_data, DES.block_size)
return decrypted_data.decode('utf-8')

code = get_sign(username,password)
code_ = '{"equtype":"ANDROID","loginImei":"Androidnull","sign":"' + code + '","timeStamp":"' + time + '","userPwd":"' + password + '","username":"' + username + '"}'
print(code_)
res_byte = DesEncode(code_,key,iv)
res_string = res_byte.decode('utf-8')
print(res_string)

#模拟访问请求
url = "http://api.dodovip.com/api/user/login"
data = json.dumps({"Encrypt": res_string})
print(data)
headers = {
"content-type": "application/json; charset=utf-8",
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 10; Pixel XL Build/QP1A.191005.007.A3)"
}
r = requests.post(url = url,data = data,headers = headers)
recv = r.text
print(recv)
print(DesDecode(recv,key,iv))

6.使用算法转发

当然也可以使用frida的算法转发,就是对于一些比较复杂的加密逻辑,我们直接通过frida主动调用获得加密后的值(sign),就不需要逆向分析加密逻辑了,然后通过转发发送到python里,在python里通过爬虫实现批量处理的操作。这里需要使用到python的frida库,而frida-server是使用的frida-tools。

完整代码

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
import base64
import hashlib

from Crypto.Cipher import DES
from Crypto.Util.Padding import pad
from Crypto.Util.Padding import unpad
import json

import frida
import requests

# frida js主动调用代码
jsCode = """
function hookTest(username,password){
var result;
Java.perform(function(){
var time = new Date().getTime();

var str = Java.use('java.lang.String');
var signData = str.$new("equtype=ANDROID&loginImei=Androidnull&timeStamp=" + time + "&userPwd=" + password + "&username=" + username + "&key=sdlkjsdljf0j2fsjk")
//md5
var utils = Java.use("com.dodonew.online.util.Utils")
var sign = utils.md5(signData).toUpperCase();
console.log('sign: ',sign);

var encryptData = '{"equtype":"ANDROID","loginImei":"Androidnull","sign":"' + sign + '","timeStamp":"' + time + '","userPwd":"' + password + '","username":"' + username + '"}'

var requestUtil = Java.use("com.dodonew.online.http.RequestUtil");
//requestUtil.encodeDesMap
var Encrypt = requestUtil.encodeDesMap(encryptData,'65102933','32028092'); //传入encryptData、key和iv
console.log('Encrypt: ',Encrypt);
result = Encrypt;
});
return result;
}
rpc.exports = {
hooktest : hookTest //这里进行导出供python接收
}
"""

# 调用frida转发脚本
process = frida.get_device_manager().add_remote_device('192.168.0.104:27042').attach("com.dodonew.online")
#192.168.0.104为手机端ip address
#这里启动frida-server的时候需要指定到本地
#./frida-server -l 0.0.0.0:27042
script = process.create_script(jsCode)
script.load()

# 接收js的导出
result = script.exports.hooktest('17860581502', '123456')
# 之后就是python发包
url = "http://api.dodovip.com/api/user/login"
data = json.dumps({"Encrypt": result})
print(data)
headers = {
"content-type": "application/json; charset=utf-8",
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 10; Pixel XL Build/QP1A.191005.007.A3)"
}
r = requests.post(url=url, data=data, headers=headers)
recv = r.text
print(recv)

#解密响应内容
def DesDecode(code,key,iv):
data = code.encode('utf-8')
data = base64.b64decode(data)
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
key_byte = bytes.fromhex(key_md5)
iv_byte = iv.encode('utf-8')
cipher = DES.new(key_byte[:8], DES.MODE_CBC, iv_byte)
decrypted_data = cipher.decrypt(data)
decrypted_data = unpad(decrypted_data, DES.block_size)
return decrypted_data.decode('utf-8')

key = "65102933"
iv = "32028092"
#print(DesDecode(recv,key,iv))

完结撒花!!!