上一篇是web端的ProtoBuf逆向,这篇就找了个app的ProtoBuf看看练练手
目标app:aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzc0NTAxNjM=
抓包分析
随便搜索一个线路点进去,看到请求头和响应都是加密的
用jadx分析下请求头的加密参数request
记住这里其他两个函数,有"/protoc.Request.Sequence",还引用了proto的包
到这可以知道request的值是经过native函数加密后再由base64得到的。
用ida打开native so文件,定位encode2函数
可以看到是用了aes_cbc_128加密,那就是需要拿到密钥和iv
直接frida hook这两个函数
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);
结果
入参: 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和密钥就在这几个之间,两两组合试一试
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
所以这个aes是标准的算法,没经过魔改的,再把加密后的结果base64一下就是请求的参数了,过程算法知道了,那参数来源呢,是怎么得到的
使用proto
上面说了用了proto的包,看里面的函数过程,明文是经过了proto协议的才到aes加密
那就0a270a182f70726f746f632e526571756573742e53657175656e6365120b080112053930e8b7af1801转成二进制写进文件,再用proto解码器解码看看
import binascii
# hex字符串转二进制
d = binascii.a2b_hex('0a270a182f70726f746f632e526571756573742e53657175656e6365120b080112053930e8b7af1801')
with open("req.bin", 'wb') as f:
f.write(d)
protoc --decode_raw < req.bin
解码后得到
1 {
1: "/protoc.Request.Sequence"
2 {
1: 1
2: "90\350\267\257"
3: 1
}
}
果不其然,那接下来如何模拟发请求呢
经过上一篇后,现在很快就能编写proto文件了
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版本
protoc --python_out=. ./req.proto
目录下生成了req_pb2.py 拖入项目中,需要使用时就调用即可
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文件,跟原来的对比一下
发送请求
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尝试解密
解完后很像protobuf格式,写进bin文件,再用proto解码看看
decry_data = aes_decry(response.content)
with open('resp.bin', mode="wb") as f:
f.write(decry_data)
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"
}
}
}
完整代码
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大概知道它的含义就行了
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)