ProtoBuf逆向之某公交app
zsk Lv4

上一篇是web端的ProtoBuf逆向,这篇就找了个app的ProtoBuf看看练练手
目标app:aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzc0NTAxNjM=

抓包分析

随便搜索一个线路点进去,看到请求头和响应都是加密的
image

用jadx分析下请求头的加密参数request
image
记住这里其他两个函数,有”/protoc.Request.Sequence”,还引用了proto的包
image
image
到这可以知道request的值是经过native函数加密后再由base64得到的。
用ida打开native so文件,定位encode2函数
!image
可以看到是用了aes_cbc_128加密,那就是需要拿到密钥和iv
直接frida hook这两个函数

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
function main(){
Java.perform(function(){
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
var UtilsClass = Java.use("com.shjt.map.tool.Native");
UtilsClass.encode2.implementation = function (a){
console.log("入参: ", ByteString.of(a).hex());
var value = this.encode2(a);
console.log('结果: ', ByteString.of(value).hex());
return value;
}
inline_hook()
})
}

function inline_hook() {
var libnative_lib_addr = Module.findBaseAddress("libnative.so");
if (libnative_lib_addr) {
var aes_decrypt_cbc = Module.findExportByName("libnative.so", "_Z15aes_encrypt_cbcPKhjPhPKjiS0_")
var aes_key_setup = Module.getExportByName('libnative.so', '_Z13aes_key_setupPKhPji');
var Native_encode2 = Module.getExportByName('libnative.so', 'Java_com_shjt_map_tool_Native_encode2');
Interceptor.attach(aes_key_setup, {
onEnter:function(args){
console.log("================aes_key_setup=================")
console.log('arg1:',args[0].readByteArray(16))
console.log('arg2:',args[1].readByteArray(16))
console.log('arg3:',args[2].toInt32())
},
onLeave:function(retval){
}
})

Interceptor.attach(aes_decrypt_cbc, {
onEnter: function (args) {
console.log("================aes_decrypt_cbc=================")
console.log('arg1:',args[0].readByteArray(16))
console.log('arg2:',args[1].toInt32())
console.log('arg3:',args[2].readByteArray(16))
console.log('arg4:',args[3].readByteArray(16))
console.log('arg5:',args[4].toInt32())
console.log('arg6:',args[5].readByteArray(16))
},
onLeave: function (retval) {
console.log("retval is :", retval)
}
})
}
}

setImmediate(main);

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
入参:  0a270a182f70726f746f632e526571756573742e53657175656e6365120b080112053930e8b7af1801
================aes_key_setup=================
arg1: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 2f d3 02 8e 14 a4 5d 1f 8b 6e b0 b2 ad b7 ca af /.....]..n......
arg2: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 39 39 37 34 20 49 ed f1 ff ff ff ff 00 00 00 00 9974 I..........
arg3: 128
================aes_decrypt_cbc=================
arg1: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 0a 27 0a 18 2f 70 72 6f 74 6f 63 2e 52 65 71 75 .'../protoc.Requ
arg2: 48
arg3: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 00 6f 6d 2f 61 6e 64 72 6f 69 64 2f 6f 6b 68 74 .om/android/okht
arg4: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 8e 02 d3 2f 1f 5d a4 14 b2 b0 6e 8b af ca b7 ad .../.]....n.....
arg5: 128
arg6: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 75 4c 8f d5 84 fa cf 62 10 37 6b 2b 72 b0 63 e4 uL.....b.7k+r.c.
retval is : 0x1
结果: 8d456e87c6801a2e18920732f80a1127e7a9548fd9f2f8b45edab8fd629e60bd804287b6fa4bb05fbc8b284ab0eb9783

明文,密文有了
iv和密钥就在这几个之间,两两组合试一试

1
2
3
4
5
6
2f d3 02 8e 14 a4 5d 1f 8b 6e b0 b2 ad b7 ca af
39 39 37 34 20 49 ed f1 ff ff ff ff 00 00 00 00
0a 27 0a 18 2f 70 72 6f 74 6f 63 2e 52 65 71 75
00 6f 6d 2f 61 6e 64 72 6f 69 64 2f 6f 6b 68 74
8e 02 d3 2f 1f 5d a4 14 b2 b0 6e 8b af ca b7 ad
75 4c 8f d5 84 fa cf 62 10 37 6b 2b 72 b0 63 e4

image
所以这个aes是标准的算法,没经过魔改的,再把加密后的结果base64一下就是请求的参数了,过程算法知道了,那参数来源呢,是怎么得到的

使用proto

上面说了用了proto的包,看里面的函数过程,明文是经过了proto协议的才到aes加密
那就0a270a182f70726f746f632e526571756573742e53657175656e6365120b080112053930e8b7af1801转成二进制写进文件,再用proto解码器解码看看

1
2
3
4
5
import binascii
# hex字符串转二进制
d = binascii.a2b_hex('0a270a182f70726f746f632e526571756573742e53657175656e6365120b080112053930e8b7af1801')
with open("req.bin", 'wb') as f:
f.write(d)
1
protoc --decode_raw < req.bin

解码后得到

1
2
3
4
5
6
7
8
1 {
1: "/protoc.Request.Sequence"
2 {
1: 1
2: "90\350\267\257"
3: 1
}
}

果不其然,那接下来如何模拟发请求呢
经过上一篇后,现在很快就能编写proto文件了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syntax = "proto3";

message Msg22 {
int32 field1 = 1;
string field2 = 2;
int32 field3 = 3;
}

message Msg11 {
string field1 = 1;
Msg22 msg22 = 2;
}

message SearchService{
Msg11 msg11 = 1;
}

编译成python版本

1
protoc --python_out=. ./req.proto

目录下生成了req_pb2.py 拖入项目中,需要使用时就调用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import binascii
import req_pb2 as pb

# hex字符串转二进制
# d = binascii.a2b_hex('0a270a182f70726f746f632e526571756573742e53657175656e6365120b080112053930e8b7af1801')
# with open("req.bin", 'wb') as f:
# f.write(d)

search_request = pb.SearchService.SearchRequest() # 实例化对象
search_request.msg11.field1 = '/protoc.Request.Sequence'
search_request.msg11.msg22.field1 = 1
search_request.msg11.msg22.field2 = '90路'
search_request.msg11.msg22.field3 = 1
# 序列化请求数据
serialize_data = search_request.SerializeToString()
# print(serialize_data)
# 保存数据玮bin文件供后续对比使用
with open('my_req.bin', mode="wb") as f:
f.write(serialize_data)

运行后生成my_req.bin文件,跟原来的对比一下
image
发送请求

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
import binascii
from urllib.parse import quote
import req_pb2 as pb
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64

def aes_encry(ori):
key = '2fd3028e14a45d1f8b6eb0b2adb7caaf'
iv = '754c8fd584facf6210376b2b72b063e4'
aes = AES.new(binascii.a2b_hex(key), AES.MODE_CBC, binascii.a2b_hex(iv))
return aes.encrypt(pad(ori, 16))

def aes_decry(ori):
key = '2fd3028e14a45d1f8b6eb0b2adb7caaf'
iv = '754c8fd584facf6210376b2b72b063e4'
aes = AES.new(binascii.a2b_hex(key), AES.MODE_CBC, binascii.a2b_hex(iv))
return unpad(aes.decrypt(ori), 16)

# hex字符串转二进制
# d = binascii.a2b_hex('0a270a182f70726f746f632e526571756573742e53657175656e6365120b080112053930e8b7af1801')
# with open("req.bin", 'wb') as f:
# f.write(d)

search_request = pb.SearchService.SearchRequest() # 实例化对象
search_request.msg1.field1 = '/protoc.Request.Sequence'
search_request.msg1.msg2.field1 = 1
search_request.msg1.msg2.field2 = '90路'
search_request.msg1.msg2.field3 = 1
# 序列化请求数据
serialize_data = search_request.SerializeToString()
# print(serialize_data)
# 保存数据玮bin文件供后续对比使用
# with open('my_req.bin', mode="wb") as f:
# f.write(serialize_data)

# 对序列化后的数据aes加密
aes_data = aes_encry(serialize_data)
# b64 aes加密数据
b64_aes_data = base64.b64encode(aes_data)
# 请求体
post_data = 'request=' + quote(b64_aes_data.decode(), safe='')+'%0A'

header = {
'Accept': 'application/json,application/xml,application/xhtml+xml,text/html;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'User-Agent': 'Mozilla/5.0 (Linux; U; Android 6.0; zh-cn; Nexus 6P Build/MDA89D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
'Host': 'lbs.jt.sh.cn:8082'
}
url = "http://lbs.jt.sh.cn:8082/app/rls/monitor"
response = requests.post(url, headers=header, data=post_data)
print(response.text)

运行后响应内容跟抓包的一样是加密的,用aes尝试解密
image
解完后很像protobuf格式,写进bin文件,再用proto解码看看

1
2
3
decry_data = aes_decry(response.content)
with open('resp.bin', mode="wb") as f:
f.write(decry_data)
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
D:\pythonProject\xxxx>protoc --decode_raw < resp.bin
1 {
1: "/protoc.Response.Dispatch"
2 {
1 {
1: "90\350\267\257"
2 {
1 {
1: "05:00"
2: "23:41"
}
2: "\351\235\226\345\256\207\345\215\227\350\267\257\346\216\247\346\261\237\350\267\257"
2: "\346\216\247\346\261\237\346\226\260\346\235\221"
2: "\351\273\204\345\205\264\350\267\257\345\233\275\346\235\203\350\267\257"
2: "\351\273\204\345\205\264\350\267\257\345\233\275\351\241\272\350\267\257"
2: "\344\272\224\350\247\222\345\234\272(\347\277\224\346\256\267\350\267\257)"
2: "\345\233\275\345\222\214\350\267\257\346\224\277\347\253\213\350\267\257"
2: "\346\201\222\344\273\201\350\267\257\346\270\205\346\272\220\347\216\257\350\267\257"
2: "\344\270\226\347\225\214\350\267\257\345\233\275\345\222\214\350\267\257"
2: "\344\270\226\347\225\214\350\267\257\346\260\221\344\272\254\350\267\257"
2: "\351\227\270\346\256\267\350\267\257\346\256\267\350\241\214\350\267\257"
2: "\351\227\270\346\256\267\350\267\257\345\206\233\345\267\245\350\267\257"
2: "\345\206\233\345\267\245\350\267\257\351\227\270\345\214\227\347\224\265\345\216\202"
2: "\351\225\277\350\210\252\351\224\232\345\234\260"
2: "\345\206\233\345\267\245\350\267\257\351\231\210\345\256\266\345\256\205"
2: "\345\215\227\345\274\240\345\215\216\346\265\234(\351\200\270\344\273\231\350\267\257)"
2: "\345\214\227\345\274\240\345\215\216\346\265\234(\351\200\270\344\273\231\350\267\257)"
2: "\346\267\236\346\273\250\350\267\257\345\220\214\346\265\216\350\267\257"
2: "\346\260\270\346\270\205\350\267\257\346\267\236\345\256\235\350\267\257"
2: "\346\260\270\346\270\205\350\267\257\346\260\264\344\272\247\350\267\257"
2: "\345\217\214\345\237\216\350\267\257\346\260\270\346\270\205\350\267\257"
2: "\346\267\236\345\256\235\350\267\257\345\217\214\345\237\216\350\267\257"
2: "\346\267\236\345\256\235\350\267\257\346\267\236\351\235\222\350\267\257"
}
2 {
1 {
1: "05:00"
2: "23:10"
}
2: "\346\267\236\345\256\235\350\267\257\346\267\236\351\235\222\350\267\257"
2: "\345\217\214\345\237\216\350\267\257\346\267\236\345\256\235\350\267\257"
2: "\346\260\270\346\270\205\350\267\257\345\217\214\345\237\216\350\267\257"
2: "\346\260\270\346\270\205\350\267\257\346\260\264\344\272\247\350\267\257"
2: "\346\260\270\346\270\205\350\267\257\346\267\236\345\256\235\350\267\257"
2: "\346\267\236\346\273\250\350\267\257\345\220\214\346\263\260\350\267\257"
2: "\351\225\277\345\276\201\346\226\260\346\235\221"
2: "\346\267\236\346\273\250\350\267\257\346\267\236\346\273\250\346\224\257\350\267\257"
2: "\345\214\227\345\274\240\345\215\216\346\265\234(\351\200\270\344\273\231\350\267\257)"
2: "\345\215\227\345\274\240\345\215\216\346\265\234(\351\200\270\344\273\231\350\267\257)"
2: "\345\206\233\345\267\245\350\267\257\351\231\210\345\256\266\345\256\205"
2: "\351\225\277\350\210\252\351\224\232\345\234\260"
2: "\345\206\233\345\267\245\350\267\257\351\227\270\345\214\227\347\224\265\345\216\202"
2: "\351\227\270\346\256\267\350\267\257\345\206\233\345\267\245\350\267\257"
2: "\351\227\270\346\256\267\350\267\257\346\256\267\350\241\214\350\267\257"
2: "\344\270\226\347\225\214\350\267\257\346\260\221\344\272\254\350\267\257"
2: "\344\270\226\347\225\214\350\267\257\345\233\275\345\222\214\350\267\257"
2: "\346\201\222\344\273\201\350\267\257\346\270\205\346\272\220\347\216\257\350\267\257"
2: "\351\225\277\346\265\267\350\267\257\351\273\221\345\261\261\350\267\257"
2: "\345\233\275\345\222\214\350\267\257\346\224\277\351\200\232\350\267\257"
2: "\344\272\224\350\247\222\345\234\272(\347\277\224\346\256\267\350\267\257)"
2: "\344\272\224\350\247\222\345\234\272(\351\273\204\345\205\264\350\267\257)"
2: "\351\273\204\345\205\264\350\267\257\345\233\275\351\241\272\350\267\257"
2: "\351\273\204\345\205\264\350\267\257\345\233\275\346\235\203\350\267\257"
2: "\346\216\247\346\261\237\346\226\260\346\235\221"
2: "\351\235\226\345\256\207\345\215\227\350\267\257\346\216\247\346\261\237\350\267\257"
}
}
2 {
1: "\346\262\252A-30515D"
2: "15:26"
}
2 {
1: "\346\262\252A-07660D"
2: "15:33"
}
2 {
1: "\346\262\252A-33990D"
2: "15:41"
}
}
}

编写proto文件并编译

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
syntax = "proto3"; // 定义proto的版本

message Msg1 {
string field1 = 1;
Msg2 msg2 = 2;
}

message Msg2 {
repeated Msg3 msg3 = 1;
repeated Msg4 msg4 = 2;
}

message Msg3 {
string field1 = 1;
repeated Msg5 msg5 = 2;
}

message Msg4 {
string field1 = 1;
string field2 = 2;
}

message Msg5 {
Msg6 msg6 = 1;
repeated string field1 = 2;
}

message Msg6 {
string field1 = 1;
string field2 = 2;
}

message SearchResponse {
Msg1 msg1 = 1;
}

image
完整代码

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
import binascii
import json
from urllib.parse import quote
from google.protobuf.json_format import MessageToJson
import req_pb2 as pb
import resp_pb2 as pb2
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64

def aes_encry(ori):
key = '2fd3028e14a45d1f8b6eb0b2adb7caaf'
iv = '754c8fd584facf6210376b2b72b063e4'
aes = AES.new(binascii.a2b_hex(key), AES.MODE_CBC, binascii.a2b_hex(iv))
return aes.encrypt(pad(ori, 16))

def aes_decry(ori):
key = '2fd3028e14a45d1f8b6eb0b2adb7caaf'
iv = '754c8fd584facf6210376b2b72b063e4'
aes = AES.new(binascii.a2b_hex(key), AES.MODE_CBC, binascii.a2b_hex(iv))
return unpad(aes.decrypt(ori), 16)

# hex字符串转二进制
# d = binascii.a2b_hex('0a270a182f70726f746f632e526571756573742e53657175656e6365120b080112053930e8b7af1801')
# with open("req.bin", 'wb') as f:
# f.write(d)

search_request = pb.SearchService() # 实例化对象
search_request.msg11.field1 = '/protoc.Request.Sequence'
search_request.msg11.msg22.field1 = 1
search_request.msg11.msg22.field2 = '90路'
search_request.msg11.msg22.field3 = 1
# 序列化请求数据
serialize_data = search_request.SerializeToString()
# print(serialize_data)
# 保存数据玮bin文件供后续对比使用
# with open('my_req.bin', mode="wb") as f:
# f.write(serialize_data)

# 对序列化后的数据aes加密
aes_data = aes_encry(serialize_data)
# b64 aes加密数据
b64_aes_data = base64.b64encode(aes_data)
# 请求体
post_data = 'request=' + quote(b64_aes_data.decode(), safe='')+'%0A'

header = {
'Accept': 'application/json,application/xml,application/xhtml+xml,text/html;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'User-Agent': 'Mozilla/5.0 (Linux; U; Android 6.0; zh-cn; Nexus 6P Build/MDA89D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
'Host': 'lbs.jt.sh.cn:8082'
}
url = "http://lbs.jt.sh.cn:8082/app/rls/monitor"
response = requests.post(url, headers=header, data=post_data)
# print(response.text)

decry_data = aes_decry(response.content)
# with open('resp.bin', mode="wb") as f:
# f.write(decry_data)

# 使用proto编写文件
root = pb2.SearchResponse()
root.ParseFromString(decry_data)
data = json.loads(MessageToJson(root))
print(data)

使用blackboxprotobuf

还有一种更快的方式,使用blackboxprotobuf
读取请求的二进制文件,通过blackboxprotobuf转为json,直接把关键词修改掉再转回去
像响应内容的key只是一个符号,可以通过value大概知道它的含义就行了

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
import blackboxprotobuf
import binascii
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
from urllib.parse import quote

def aes_encry(ori):
key = '2fd3028e14a45d1f8b6eb0b2adb7caaf'
iv = '754c8fd584facf6210376b2b72b063e4'
aes = AES.new(binascii.a2b_hex(key), AES.MODE_CBC, binascii.a2b_hex(iv))
return aes.encrypt(pad(ori, 16))

def aes_decry(ori):
key = '2fd3028e14a45d1f8b6eb0b2adb7caaf'
iv = '754c8fd584facf6210376b2b72b063e4'
aes = AES.new(binascii.a2b_hex(key), AES.MODE_CBC, binascii.a2b_hex(iv))
return unpad(aes.decrypt(ori), 16)

with open(r"req.bin", "rb") as fp:
data = fp.read()
message, typedef = blackboxprotobuf.protobuf_to_json(data, message_type=None)
bus_data = blackboxprotobuf.decode_message(data, message_type=None)[0]
print(message)
print(bus_data)
print(typedef)
bus_data['1']['2']['2'] = bytes('90路', 'utf-8')

serializedata = blackboxprotobuf.encode_message(bus_data, message_type=typedef)
print(serializedata)

# 对序列化后的数据aes加密
aesdata = aes_encry(serializedata)
# b64 aes加密数据
b64_aes_data = base64.b64encode(aesdata)
# 抓取
postdata = 'request=' + quote(b64_aes_data.decode(), safe='') + '%0A'
header = {
'Accept': 'application/json,application/xml,application/xhtml+xml,text/html;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'User-Agent': 'Mozilla/5.0 (Linux; U; Android 6.0; zh-cn; Nexus 6P Build/MDA89D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
'Host': 'lbs.jt.sh.cn:8082'
}
response = requests.post(url='http://lbs.jt.sh.cn:8082/app/rls/monitor', data=postdata, headers=header)
decry_data = aes_decry(response.content)

json_data, type_data = blackboxprotobuf.protobuf_to_json(decry_data, message_type=None)
print(json_data)

image

 评论