手机安装 app ,设置代理,然后开始抓包。
发现数据没法解密,查看请求的 url 是 http://lbs.jt.sh.cn:8082/app/rls/monitor,使用 jadx 反编译 app 后搜索这个 url(提示:可以只搜索 url 中一部分,因为请求的 url 可能时好几部分拼接而成的),这里搜索 rls/monitor,
点进去,然后在 右键 ---> 查找用例
再点进去
127 行是 添加 post data,和上面抓包结果可以对应上,所以这部分代码就是需要分析的代码。
查看 com.shjt.map.data.rline.Response,可以看到 Protoc.Response response = Protoc.Response.parseFrom(Native.decode2(bytes));
在查看 decode2 函数,可以看到是 native 类型的函数,是在 so 库中
解压 apk 文件,找到 so 库文件 libnative.so ,使用 ida pro 打开,然后搜索 java_ 开头的函数
点进去,然后按 F5 查看伪代码:
protobuf 语法中文翻译:https://colobu.com/2017/03/16/Protobuf3-language-guide/
Protobuf 正向流程
Protobuf 进阶——使用 Python 操作 Protobuf:https://blog.csdn.net/a464057216/article/details/54932719
proto.exe 编译命令,自动生成 python 程序:protoc --python_out=. addressbook.proto
编译 addressbook.proto 文件,生成 addressbook_pb2.py
利用 proto.exe 反解数据 protoc.exe --decode_raw < D:\a.bin
protoc 命令帮助:
protoc -help
Usage: protoc [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:-IPATH, --proto_path=PATH Specify the directory in which to search forimports. May be specified multiple times;directories will be searched in order. If notgiven, the current working directory is used.If not found in any of the these directories,the --descriptor_set_in descriptors will bechecked for required proto file.--version 显示版本号-h, --help 帮助信息--encode=MESSAGE_TYPE 从标准输入读取文本格式信息,然后从标准输出中输出二进制数据,需要指定 PROTO_FILES--deterministic_output When using --encode, ensure map fields aredeterministically ordered. Note that this orderis not canonical, and changes across builds orreleases of protoc.--decode=MESSAGE_TYPE 从标准输入中读取2进制数据,然后以文本方式输出到标准输出,需要指定 PROTO_FILES--decode_raw 从标准输入中读取任意的protocol数据,然后以 tag/value的格式输出到标准输出,不需要指定 PROTO_FILES --descriptor_set_in=FILES Specifies a delimited list of FILESeach containing a FileDescriptorSet (aprotocol buffer defined in descriptor.proto).The FileDescriptor for each of the PROTO_FILESprovided will be loaded from theseFileDescriptorSets. If a FileDescriptorappears multiple times, the first occurrencewill be used.-oFILE, Writes a FileDescriptorSet (a protocol buffer,--descriptor_set_out=FILE defined in descriptor.proto) containing all ofthe input files to FILE.--include_imports When using --descriptor_set_out, also includeall dependencies of the input files in theset, so that the set is self-contained.--include_source_info When using --descriptor_set_out, do not stripSourceCodeInfo from the FileDescriptorProto.This results in vastly larger descriptors thatinclude information about the originallocation of each decl in the source file aswell as surrounding comments.--dependency_out=FILE Write a dependency output file in the formatexpected by make. This writes the transitiveset of input file paths to FILE--error_format=FORMAT Set the format in which to print errors.FORMAT may be 'gcc' (the default) or 'msvs'(Microsoft Visual Studio format).--fatal_warnings Make warnings be fatal (similar to -Werr ingcc). This flag will make protoc returnwith a non-zero exit code if any warningsare generated.--print_free_field_numbers Print the free field numbers of the messagesdefined in the given proto files. Groups sharethe same field number space with the parentmessage. Extension ranges are counted asoccupied fields numbers.--plugin=EXECUTABLE Specifies a plugin executable to use.Normally, protoc searches the PATH forplugins, but you may specify additionalexecutables not in the path using this flag.Additionally, EXECUTABLE may be of the formNAME=PATH, in which case the given plugin nameis mapped to the given executable even ifthe executable's own name differs.--cpp_out=OUT_DIR Generate C++ header and source.--csharp_out=OUT_DIR Generate C# source file.--java_out=OUT_DIR Generate Java source file.--js_out=OUT_DIR Generate JavaScript source.--kotlin_out=OUT_DIR Generate Kotlin file.--objc_out=OUT_DIR Generate Objective-C header and source.--php_out=OUT_DIR Generate PHP source file.--python_out=OUT_DIR Generate Python source file.--ruby_out=OUT_DIR Generate Ruby source file.@<filename> Read options and filenames from file. If arelative file path is specified, the filewill be searched in the working directory.The --proto_path option will not affect howthis argument file is searched. Content ofthe file will be expanded in the position of@<filename> as in the argument list. Notethat shell expansion is not applied to thecontent of the file (i.e., you cannot usequotes, wildcards, escapes, commands, etc.).Each line corresponds to a single argument,even if it contains spaces.
注意:window Termimal 只能执行 cmd 命令,没法执行 linux 命令,cmder ( https://cmder.net/ ) 即可以执行 cmd 命令,也可以执行 linux 的一些命令,安装 cmder 然后执行反解数据
示例 protobuf 二进制数据:https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid=168855206&pid=98919207&segment_index=1
点击后会下载一个 seg.so 的文件,然后执行反解命令:protoc.exe --decode_raw < "seg.so"
注意:因为没有 proto 文件,所以反解数据后,值是对的,但是没有 key,
反解 Protobuf 方法
方法一:还原 .proto 文件:
- 1.利用 protoc.exe 反解析 protobuf 数据
- 2.根据反解析出来的数据,还原出 .proto 文件
- 3.用 protoc.exe 编译 .proto 文件,生成 py 程序
- 4.用 py 程序可以轻松序列化和反序列化
方法二:利用 blackboxprotobuf 库直接操作 protobuf 数据,不需要还原 .proto 文件
# -*- coding: utf-8 -*-
# @Author : 佛祖保佑, 永无 bug
# @Date :
# @File : temp.py
# @Software: PyCharm
# @description : XXXimport blackboxprotobufdef main():seg_so = Nonewith open('d:/seg.so', 'rb') as f:seg_so = f.read()msg, typ = blackboxprotobuf.protobuf_to_json(seg_so, message_type=None)print(msg)print(typ)if __name__ == '__main__':main()pass
加解密相关知识:
hook加密类:
各加密类的用法,key iv 明文 密文等是如何获取的,再hook对应的类和方法
AES https://www.cnblogs.com/widgetbox/p/11611201.html
RSA https://blog.csdn.net/qq_22075041/article/details/80698665
DES https://www.jianshu.com/p/bf6b4afaf41e
MD5 SHA等摘要算法 https://blog.csdn.net/baidu_34045013/article/details/80687557
HMAC摘要算法 https://blog.csdn.net/cdzwm/article/details/6973345
android的rsa加密填充方式是RSA时,是NoPadind RSA/ECB/NoPadding,
而标准jdk里填充是RSA时,是指PKCS1填充,RSA/ECB/PKCS1Padding,要注意
RSA加密科普 https://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html
RSA加密科普 https://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html
RSA密钥长度关系 https://cloud.tencent.com/developer/article/1199963
python rsa加密库 https://pycryptodome.readthedocs.io/en/latest/src/examples.html#generate-an-rsa-key
公私钥ASN.1结构 https://blog.csdn.net/wzj_whut/article/details/86477568
ASN.1、PKCS、PEM间的关系 https://blog.csdn.net/qq_39385118/article/details/107510032
AES 加密:一种对称加密,加密和解密时需要:密匙(key),iv,加密模式 三个参数,加密时明文需要先做对齐处理,kv 和 iv 有长度规定(AES-128、AES-192和AES-256),明文长度要为16的倍数,否则要给明文后面加0补齐长度。
可以看到
- 函数 j_aes_key_setup 用来构造 aes
- 函数 j_aes_encrypt_cbc 用来解密
所以需要 hook 这两个函数
首先分析 j_aes_key_setup 这个函数,一直追进去,然后找到 export 的函数名,
可以看到函数名为 _Z13aes_key_setupPKhPji,hook 的时候需要 hook 这个函数名,同理可以找到 j_aes_encrypt_cbc hook 时 export 的函数名为 _Z15aes_encrypt_cbcPKhjPhPKjiS0_
frida hook js 代码如下:
Interceptor 使用方法文档:https://frida.re/docs/javascript-api/#interceptor
function printstack() {console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}function hook_so() {console.log("\r");var Requester = Java.use('com.shjt.map.view.layout.realtime.LineLayout$Requester');Requester.request.implementation = function (p1) {this.request(p1)}var Req = Java.use('com.shjt.map.data.rline.Request');Req.toString.implementation = function (p1) {//send(this.mBuilder.build().toByteArray())var tmp = this.toString()send('11111111:' + tmp)return tmp}var ByteString = Java.use('com.android.okhttp.okio.ByteString')var Native = Java.use('com.shjt.map.tool.Native');Native.decode2.implementation = function (pp) {console.log("str :" + Java.use('java.lang.String').$new(pp));// 因为字节数组中有的转化成字符串也是不可见的,所以转成 16进制console.log("hex :" + ByteString.of(pp).hex());console.log("array :" + JSON.stringify(pp));return this.decode2(pp)}var soBaseAddress = Module.findBaseAddress("libnative.so");if (soBaseAddress) {// 查找 aes_key_setup 函数var aes_key_setup = Module.findExportByName("libnative.so", '_Z13aes_key_setupPKhPji');if (aes_key_setup) {console.log("找到 aes_key_setup")Interceptor.attach(aes_key_setup, {onEnter: function (args) {// console.log("aes_key_setup args 类型" + typeof args);// console.log("aes_key_setup args[0] " + typeof args[0].readByteArray(16) + " " + args[0].readByteArray(16));console.log("aes_key_setup args[0] ", args[0].readByteArray(16));console.log("aes_key_setup args[1] ", args[1].readByteArray(16));console.log("aes_key_setup args[2] ", args[2].toInt32());},onLeave: function (retval) {console.log("aes_key_setup 返回值:" + retval);}})} else {console.log("没找到 aes_key_setup")}// 查找 aes_encrypt_cbc 函数var aes_encrypt_cbc = Module.findExportByName("libnative.so", '_Z15aes_encrypt_cbcPKhjPhPKjiS0_');if (aes_encrypt_cbc) {console.log("找到 aes_encrypt_cbc")Interceptor.attach(aes_encrypt_cbc, {onEnter: function (args) {// console.log("aes_encrypt_cbc args 类型" + typeof args);// console.log("aes_encrypt_cbc args[0] " + typeof args[0].readByteArray(16) + " " + args[0].readByteArray(16));console.log("aes_encrypt_cbc args[0] ", args[0].readByteArray(16));console.log("aes_encrypt_cbc args[1] ", args[1].toInt32());console.log("aes_encrypt_cbc args[2] ", args[2].readByteArray(16));console.log("aes_encrypt_cbc args[3] ", args[3].readByteArray(16));console.log("aes_encrypt_cbc args[4] ", args[4].toInt32());console.log("aes_encrypt_cbc args[5] ", args[5].readByteArray(16));},onLeave: function (retval) {console.log("aes_encrypt_cbc 返回值:" + retval);}})} else {console.log("没找到 aes_encrypt_cbc")}}
}function main() {Java.perform(hook_so);
}setImmediate(main);
j_aes_key_setup((const unsigned __int8 *)v18, (unsigned int *)v15, 128) 函数有三个参数
- 第一个参数 和 第二个参数都是指针,
- 第三个参数 是一个 int 整数
j_aes_encrypt_cbc((const unsigned __int8 *)p, v11, v12, (const unsigned int *)v15, 128, (const unsigned __int8 *)v17); 函数有 6 个参数
- 第一个参数:指针类型
- 第二个参数:signed int 类型,是个整数
- 第三个参数:指针类型
- 第四个参数:指针类型
- 第五个参数:int 类型,是个整数
- 第六个参数:指针类型
frida 关于指针的操作:https://frida.re/docs/javascript-api/#nativepointer
frida js 中指针为什么用 readByteArray 来处理???
因为 AES 最终处理时,都是转换成 "字节数组" 来处理的,所以使用 readByteArray 来处理
为什么是读取 16 字节???
因为 AES 长度有规定 ( 128、192、256 ),可以看到 j_aes_key_setup 和 j_aes_encrypt_cbc 函数参数中都有 128,128bit / 8 = 16Byte,所有暂时可以假定是读取 16 字节。
要不就使用 ida pro 动态调试 so ,确定参数的值,这个属于另外技术范畴不在展开。。。
启动 frida-server
查看 apk 包名
运行 js 脚本进行 hook。执行命令:frida -U -F com.xxx.map -l .\hook_so.js --no-pause
可以看到 j_aes_key_setup((const unsigned __int8 *)v18, (unsigned int *)v15, 128) 函数有三个参数
- v18 里面存的数据是 2f d3 02 8e 14 a4 5d 1f 8b 6e b0 b2 ad b7 ca af
- v15 里面存的数据是 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
- 第三个参数 是 128
j_aes_encrypt_cbc((const unsigned __int8 *)p, v11, v12, (const unsigned int *)v15, 128, (const unsigned __int8 *)v17); 函数有 6 个参数
- p 参数值 0a 27 0a 18 2f 70 72 6f 74 6f 63 2e 52 65 71 75 key值
- v11 参数值 48
- v12 参数值 00 00 00 00 20 00 00 00 61 62 6c 65 2d 61 6e 79
- v15 参数值 8e 02 d3 2f 1f 5d a4 14 b2 b0 6e 8b af ca b7 ad
- 128
- v17 75 4c 8f d5 84 fa cf 62 10 37 6b 2b 72 b0 63 e4 iv值
decode2 参数的 16进制数据:
现在 key、iv、16 进制数据都有了,可以尝试下解密:
Python 的 AES 加密与解密:https://www.cnblogs.com/niuu/p/10107212.html
AES 加密方式有五种:ECB, CBC, CTR, CFB, OFB
从安全性角度推荐 CBC 加密方法,下面是 CBC、ECB 两种加密方法的 python 实现
python 在 Windows下使用AES时要安装的是pycryptodome 模块 pip install pycryptodome
# 先导入所需要的包
pip3 install Crypto
# 再安装pycrypto
pip3 install pycrypto
from Crypto.Cipher import AES # 就成功了python 在 Linux下使用AES时要安装的是pycrypto模块 pip install pycrypto
- CBC 加密需要一个十六位的 key (密钥) 和 一个十六位 iv(偏移量)
- ECB 加密不需要 iv
AES CBC 加密的python实现
from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hex# 如果text不足16位的倍数就用空格补足为16位
def add_to_16(text):if len(text.encode('utf-8')) % 16:add = 16 - (len(text.encode('utf-8')) % 16)else:add = 0text = text + ('\0' * add)return text.encode('utf-8')# 加密函数
def encrypt(text):key = '9999999999999999'.encode('utf-8')mode = AES.MODE_CBCiv = b'qqqqqqqqqqqqqqqq'text = add_to_16(text)cryptos = AES.new(key, mode, iv)cipher_text = cryptos.encrypt(text)# 因为AES加密后的字符串不一定是ascii字符集的,输出保存可能存在问题,所以这里转为16进制字符串return b2a_hex(cipher_text)# 解密后,去掉补足的空格用strip() 去掉
def decrypt(text):key = '9999999999999999'.encode('utf-8')iv = b'qqqqqqqqqqqqqqqq'mode = AES.MODE_CBCcryptos = AES.new(key, mode, iv)plain_text = cryptos.decrypt(a2b_hex(text))return bytes.decode(plain_text).rstrip('\0')if __name__ == '__main__':e = encrypt("hello world") # 加密d = decrypt(e) # 解密print("加密:", e)print("解密:", d)
AES ECB 加密的 python 实现
"""
ECB没有偏移量
"""
from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hexdef add_to_16(text):if len(text.encode('utf-8')) % 16:add = 16 - (len(text.encode('utf-8')) % 16)else:add = 0text = text + ('\0' * add)return text.encode('utf-8')# 加密函数
def encrypt(text):key = '9999999999999999'.encode('utf-8')mode = AES.MODE_ECBtext = add_to_16(text)cryptos = AES.new(key, mode)cipher_text = cryptos.encrypt(text)return b2a_hex(cipher_text)# 解密后,去掉补足的空格用strip() 去掉
def decrypt(text):key = '9999999999999999'.encode('utf-8')mode = AES.MODE_ECBcryptor = AES.new(key, mode)plain_text = cryptor.decrypt(a2b_hex(text))return bytes.decode(plain_text).rstrip('\0')if __name__ == '__main__':e = encrypt("hello world") # 加密d = decrypt(e) # 解密print("加密:", e)print("解密:", d)
测试:
# -*- coding: utf-8 -*-
# @Author : 佛祖保佑, 永无 bug
# @Date :
# @File : temp.py
# @Software: PyCharm
# @description : XXXimport base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import binasciidef main():# with open('D:\monitor.bin', 'rb') as f:# c = f.read()key = '2fd3028e14a45d1f8b6eb0b2adb7caaf'iv = '754c8fd584facf6210376b2b72b063e4'aes = AES.new(binascii.a2b_hex(key), AES.MODE_CBC, binascii.a2b_hex(iv))hex_str = '8509209294464b3e84a122800c9419068fa44cb5827e4df3db42212a6054243a55793243b8d6479773d67ab74749611d987ab38c274bf716a2c66a8f233e9683667af7e84119d371b9926abc6f8294b266534ddb25f8ef015a16c60b770d3198'plaintext = aes.decrypt(binascii.a2b_hex(hex_str))print(plaintext)if __name__ == '__main__':main()pass
把上面 key、iv、hex 替换下,然后运行,程序不报错,说明 传递参数正确。
下面就是写代码,请求URL得到 respone 数据,然后解密数据得到 protobuf 格式的二进制数据,再解析 protobuf 数。。。略略略略略