0%

逆向学习 0x04开始hook

hook

Java层的代码hook可以写js,但是方法定义方式不同

1
2
3
Java.perform(function() {

})

仍然去找之前案例的登录方法,找到两个方法

image-20241009155711445

使用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
// 如果是Java hook代码,都放在perform中
Java.perform(function() {
// 获取到相应的类
var JsonRequest = Java.use("com.dodonew.online.http.JsonRequest")
console.log("JsonRequest:" + JsonRequest)

// hook类中的paraMap方法
JsonRequest.paraMap.implementation = function(a) {

// paraMap方法传入了一个参数,就给一个参数,打印出来看看
console.log("paraMap传入的参数:" + a)

// 调用原来的paraMap方法
return this.paraMap(a)

// 如果有返回值就return,void就不用return
}

JsonRequest.addRequestMap.overload('java.util.Map','int').implementation = function(a, b) {

// addRequestMap方法传入了两个参数,打印出来看看
console.log("addRequestMap传入的参数1:" + a)
console.log("addRequestMap传入的参数2:" + b)

// 调用原来的addRequestMap方法
return this.addRequestMap(a, b)

// 如果有返回值就return,void就不用return
}
})

在手机上执行这个js

先启动hook

image-20241009173459202

启动成功就是光标一直闪,挂起他,不用管

使用frida命令来执行这个JS,可以看到,在进行登录操作的时候执行的方法是addRequestMap方法

image-20241009173357970

这个是hook的一个简单应用,将代码注入到相应的进程中

一些问题

启动问题:

1、确定手机上的frida-server的版本和电脑上frida版本一致

2、确定是否有权限,赋予权限

1
chmod 777 frida

3、提示已经开启过frida-server,无法正常启动,杀进程解决

1
kill frida-server

模拟器问题:

1、当时我开了USB调试的,可能设置密码之后给关闭了,一直没注意,怎么执行都执行不了,一看是模拟器的开发者模式没开,打开开发模式,开启USB调试

2、虽然说 -U 是USB连接,但是使用模拟器执行 -U 也是没问题的

1
frida -UF -l js文件路径

3、通过usb连接真机不需要转发端口,通过ip连接需要转发,连接模拟器也需要转发

1
adb forward tcp:27042 tcp:27042

一些注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1、找到一些疑似关键函数,可以通过hook来确认app执行某个操作的时候,是否调用了他们

2、如果没有触发这些函数,考虑以下问题
a) app在执行这个操作的时候,真的没有调用这个函数,换一个其他的关键函数
b) 代码写错了,导致hook函数没执行
c) 一般可以通过主动调用上层函数,来触发这些hook函数

3、如果触发了这些函数,可以通过hook来打印执行过程中传入函数的参数和返回值

4、frida -U -F -l 1.js
-U 代表远程USB设备
-F 代表附加到最前面这个app
-l 后面指明需要加载的JS脚本
-f 指明附加的进程名

5、写好的js脚本要注入手机端,并不是在Node.js中使用,所以只能用v8和fridaAPl支持的代码

继续hook

上面只是简单的打印,打印出来的还是[object],没有真正读取到内容,继续找字段,获取内容,可以看到这里将两个字段传入了addRequestMap方法中,将这两个打印出来

image-20241009185105967

注意要使用 变量名.get 不能直接点,否则为 undefined 。

因为function接收到的a属于是Java类型,只能使用Java中的方法。而这里指定的类型是Map接口,意味着是不能直接使用toString()来输出的

1
2
3
4
5
6
7
8
9
10
11
12
13
JsonRequest.addRequestMap.overload('java.util.Map','int').implementation = function(a, b) {

// addRequestMap方法传入了两个参数,打印出来看看
console.log("addRequestMap传入的参数1:" + a)
console.log(a.get("username"))
console.log(a.get("userPwd"))
console.log("addRequestMap传入的参数2:" + b)

// 调用原来的addRequestMap方法
return this.addRequestMap(a, b)

// 如果有返回值就return,void就不用return
}

打印出结果

image-20241009185355960

如果要使用toString()来输出的话也不是不可以,这就涉及到多态了,需要做一个向下转型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	JsonRequest.addRequestMap.overload('java.util.Map','int').implementation = function(a, b) {

// addRequestMap方法传入了两个参数,打印出来看看
console.log("addRequestMap传入的参数1:" + a)

// 向下转型
var map = Java.cast(a, Java.use("java.util.HashMap"));
console.log(map.toString())

console.log("addRequestMap传入的参数2:" + b)

// 调用原来的addRequestMap方法
return this.addRequestMap(a, b)

// 如果有返回值就return,void就不用return
}

// 使用Java.cast进行转型,第一个参数给要转型的变量,第二个参数给要转到的类型,需要的数据是class类型,所以需要Java.use

这样就可以成功打印了

image-20241009192739762

然而到现在为止,还没有找到加密的数据,继续向下分析

image-20241008213436432

image-20241009193849686

image-20241009200420640

1
2
3
4
5
6
7
8
var utils = Java.use("com.dodonew.online.util.Utils")

utils.md5.implementation = function(a) {
console.log("md5传入的参数:" + a)
var result = this.md5(a)
console.log("md5返回值:" + result)
return result
}

可以看到传入的数据,前两个很显然是固定的,然后是时间戳,账号密码,后面还有一个key值,多次测试发现他也是固定的,发现key值,包有用的

image-20241009201246501

key为定值

image-20241009202427897

经测试这个md5也是一个标准的md5

image-20241009202623832

继续向下

image-20241009203245578

继续hook

1
2
3
4
5
6
7
8
9
var Reques = Java.use("com.dodonew.online.http.RequestUtil")

Reques.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a,b,c) {

console.log("encodeDesMap传入的参数:"+a+","+b+","+c)
var result = this.encodeDesMap(a,b,c)
console.log("encodeDesMap返回值:"+result)
return result
}

可以看到三个参数分别为

1
2
3
4
5
{"equtype":"ANDROID","loginImei":"Android358523029800728","sign":"8D44FC2EB51F27C6B828BB617EC9EC20","timeStamp":"1728477083894","userPwd":"123123123","username":"13112345678"}
65102933
32028092

虽然说key和IV也可以根据代码分析找到,但是可能存在热修复的情况,能hook还是hook

image-20241009210422351

分析这些代码可以分析出来,最后的返回值是byte数组经过base64加密的,解密出来是乱码的,而且这个byte数组是经过DES对称加密,他的key和IV在InitCipher方法中处理了

image-20241009210453484

继续分析key和IV,hook javax.crypto.spec.DESKeySpec 这个方法是系统方法,但是也可以hook

1
2
3
4
5
6
7
8
9
10
var dDESKeySpec = Java.use("javax.crypto.spec.DESKeySpec")

// 值得注意的是,使用的是其构造方法,所以表示方法的时候就要使用 $init 来表示构造方法
dDESKeySpec.$init.overload('[B').implementation = function(a) {

console.log("DESKeySpec传入的参数:"+a)
var result = this.$init(a)
console.log("DESKeySpec返回值:"+result)
return result
}

这里成功输出了byte数组的内容,如果不成功的话,还可以进行fridahook函数构造

image-20241009212747402

1
2
3
4
5
6
7
8
9
10
11
var base64 = Java.use("android.util.Base64")
var dDESKeySpec = Java.use("javax.crypto.spec.DESKeySpec")

dDESKeySpec.$init.overload('[B').implementation = function(a) {

console.log("DESKeySpec传入的参数:"+a)
console.log("params", base64.encodeToString(a,0))
var result = this.$init(a)
console.log("DESKeySpec返回值:"+result)
return result
}

将其转换为base64字符串输出,再进行解码获取字节

image-20241009213612245

总结API

API 作用
Java.use 获取一个Java的类
Java.cast 强转某个数据的数据类型
$init 指构造函数
.implementation 执行函数

算法复现

以上操作只是了解了大题的加密流程

利用JS代码来模拟复现加密后的数据

1
NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii3YDM4Ldj62VpjjNQTngbACPLo2OP5oJEUP2uHky2gXh1XxeGRPcoZdFd4azu5U7k+Y7XTZH76IREt+HpFXupG8d63zw+ofK/XyuC0fLQmg8VPPASfP0NU3goG2EBcncto3VohIjavtyt0lQ2dkwXzsQmg9WJdSP4/FazKBwf2c1JgpoKWXi5pg=

加密流程如下

1
2
3
先加密了数据
equtype=ANDROID&loginImei=Android358523029800728&timeStamp=" +time+"&userPwd=" +pwd+ "&username="+user+"&key=sdlkjsdljf0j2fsjk
其中有三个不是固定的,时间戳,账号和密码,取出这三个值,写一个方法
1
2
// 别忘了下载crypto-js
npm install crypto-js
1
2
3
4
5
6
7
// sign值是一个MD5值,所以先进行MD5加密
function getSign(user, pwd, time) {
var data = "equtype=ANDROID&loginImei=Android358523029800728&timeStamp=" +time+
"&userPwd=" +pwd+ "&username="+user+"&key=sdlkjsdljf0j2fsjk"

return CryptoJS.MD5(data).toString();
}

这样就获取到了下一个需要加密的明文数据的sign值

注意hook出来的MD5是大写,需要对sign值进行大写处理

然后难点就是处理解析key和iv值了

key值经过MD5加密之后得到32位十六进制,为确保16位密钥,将32位的十六进制数进行hex解密,得到16位

iv值只有8位,进行utf-8解析,得到16位

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
// 模拟DES加密
function encodeDesMap(user, pwd) {

// 加工明文
// var time = new Date().getTime();
var time = "1728480847190";
var sign = getSign(user, pwd, time).toUpperCase();

var data = '{"equtype":"ANDROID","loginImei":"Android358523029800728","sign":"'
+sign+'","timeStamp":"'+ time +'","userPwd":"' + pwd+'","username":"' +user+ '"}'

// 密钥和向量,原算法中key和iv也是经过处理的
// 解析key值
var keyMD5 = CryptoJS.MD5("65102933").toString();
var _key = CryptoJS.enc.Hex.parse(keyMD5);
// 解析iv值
var _iv = CryptoJS.enc.Utf8.parse("32028092")


return CryptoJS.DES.encrypt(data, _key, {
iv: _iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
}

输出结果如下,如此一来就脱离了app,模拟出来了app发出去的包的加密数据,加密算法复现成功。

image-20241010112932978

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
const CryptoJS = require('crypto-js');

// sign值是一个MD5值,所以先进行MD5加密
function getSign(user, pwd, time) {
var data = "equtype=ANDROID&loginImei=Android358523029800728&timeStamp=" +time+
"&userPwd=" +pwd+ "&username="+user+"&key=sdlkjsdljf0j2fsjk"

return CryptoJS.MD5(data).toString();
}

function encodeDesMap(user, pwd) {

// 加工明文
// var time = new Date().getTime();
var time = "1728480847190";
var sign = getSign(user, pwd, time).toUpperCase();

var data = '{"equtype":"ANDROID","loginImei":"Android358523029800728","sign":"'
+sign+'","timeStamp":"'+ time +'","userPwd":"' + pwd+'","username":"' +user+ '"}'

// 密钥和向量,原算法中key和iv也是经过处理的
// 解析key值
var keyMD5 = CryptoJS.MD5("65102933").toString();
var _key = CryptoJS.enc.Hex.parse(keyMD5);
// 解析iv值
var _iv = CryptoJS.enc.Utf8.parse("32028092")


return CryptoJS.DES.encrypt(data, _key, {
iv: _iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
}

console.log(encodeDesMap("13112345678", "123123123"));

算法复现:

​ 利用hook出来的信息,来进行算法复现

​ 算法复现不限语言,能复现出来就是成功,这是用JS尝试的,使用JS的好处是=时可以被其他语言调用

主动调用

之前也试过,这个还是很有用的

1
2
3
4
5
6
7
8
作用:
1、可以用来测试Hook代码的正确性
2、调用加密函数观察算法输出结果特征
3、调用加密函数测试算法复现正确性
标准算法一般不需要,非标准算法需要反复测试

4、比较复杂的算法,需要借助主动调用实现算法转发
算法复杂可以不复现啊,直接hook把自己想要算的数据传输进去,打印出来,转发出去即可

2、拿DES加密举例,有CBC模式和ECB模式,那么如果没有找到的话,怎么区分呢,加密一串数据试一下就可以看出来

8位数以下是16位,8位数就是32位

image-20241010115758308

如果一组数是一样的话看输出结果,很明显的一个特点,如果是so层这样的难以辨别模式的,就可以通过hook的主动调用来测试

AES算法的分组长度是16字节,DES算法的分组长度是8字节,也就是说AES算法每16字节增加一次长度

1
2
3
4
5
6
7
8
9
10
1234567812345678
96d0028878d58c89
96d0028878d58c89
feb959b7d4642fcb

123456781234567812345678
96d0028878d58c89
96d0028878d58c89
96d0028878d58c89
feb959b7d4642fcb

image-20241010115908507

协议复现

将以上算法复现完毕之后,还需要与服务器交互才能进行正常登录操作,现在进行协议复现,去与服务器进行交互

先完成加密解密这一部分的功能,已经写好加密了,解密方法直接一把梭,在把加密和解密的两个方法交出去

注意交出使用 module.exports 而不是直接使用 exports

image-20241010143830706

模拟http请求

很多语言都可以写,这里用的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
54
55
56
57
58
59
60
61
62
63
64
65
// 导入HTTP模块,创建客户端请求,导入加密解密算法方法
var http = require('http');
var jscode = require('./DES');

// 接收的三个变量分别是请求的路径,发送的数据,响应后的回调
function post(action, send, callback) {

// 定义request对象,包括主机名、端口、路径、方法、请求头
var options = {
hostname: 'api.dodovip.com',
port: 80,
path: action,
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9; NX627J Build/PQ3B.190801.03191204)'
}
};

// 创建一个post请求
var req = http.request(options, function (res) {
// 打印响应状态码、响应头
console.log('STATUS:'+ res.statusCode);
console.log('HEADERS:'+ JSON.stringify(res.headers));

// 定义了一个post变量,用于存储请求体的信息
var body = '';
res.setEncoding('utf8');

// 通过res的data事件监听函数,每次接受到数据,就累加到post变量中
res.on('data', function (chunk) {
body += chunk;
});

// 在res的end事件触发后,通过json.parse将post变量解析成JSON对象
res.on('end', function () {
callback(body);
});
});

req.on('error', function (e) {
console.log('problem with request:'+ e.message);
});

// 将数据写入请求流
req.write(send);

// 请求结束
req.end();
}

// 加密数据
cipherText = jscode.encodeDesMap("13112345678", "123123123");

// 伪造原APP的数据格式
var data = JSON.stringify({"Encrypt": cipherText});

// 调用post函数,向服务器的指定路径发送加密数据,并设置回调函数处理响应数据
post('/api/user/login', data, function (json) {
console.log(json);

// 解密文本
plainText = jscode.decodeDesMap(json)
console.log(plainText);
});

python复现

1
2
pip install requests
pip install PyExecJS
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
import requests, json
import execjs

import _locale
_locale._getdefaultlocale = (lambda *args: ['zh_CN', 'utf8'])

f = open('DES.js', 'r', encoding='utf-8')
demo_list = f.readlines()
print(list(range(0, len(demo_list))))
jscode = ""
for i in range(0, len(demo_list)):
jscode += demo_list[i]
f.close()

js = execjs.compile(jscode)
cipherText = js.call('encrypt', "13112345678", "123123123")
print(cipherText)

url = 'http://api.dodovip.com/api/user/login'
data = json.dumps({"Encrypt": cipherText})
headers = {
"Content-Type": "application/json; charset=utf-8",
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9; NX627J Build/PQ3B.190801.03191204)'
}
r = requests.post(url, data=data, headers=headers)
print(r)
print(r.text)
print(type(r.text))
print(r.content)

ciptherText = js.call('decrypt', r.text)
print(ciptherText)

// 这个实现就不需要JS文件导出模块了,是python直接读取的内容