失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > frida hook so层 protobuf 数据解析

frida hook so层 protobuf 数据解析

时间:2019-03-27 07:30:24

相关推荐

frida hook so层 protobuf 数据解析

手机安装 app ,设置代理,然后开始抓包。

发现数据没法解密,查看请求的 url 是 http://lbs.: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 语法中文翻译://03/16/Protobuf3-language-guide/

Protobuf 正向流程

Protobuf 进阶——使用 Python 操作 Protobuf:/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 -helpUsage: protoc [OPTION] PROTO_FILESParse 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_outputWhen 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_DIRGenerate Java source file.--js_out=OUT_DIR Generate JavaScript source.--kotlin_out=OUT_DIR Generate Kotlin file.--objc_out=OUT_DIRGenerate Objective-C header and source.--php_out=OUT_DIR Generate PHP source file.--python_out=OUT_DIR Generate Python source file.--ruby_out=OUT_DIRGenerate 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 (/ ) 即可以执行 cmd 命令,也可以执行 linux 的一些命令,安装 cmder 然后执行反解数据

示例 protobuf 二进制数据:/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 /widgetbox/p/11611201.html

RSA /qq_22075041/article/details/80698665

DES /p/bf6b4afaf41e

MD5 SHA等摘要算法 /baidu_34045013/article/details/80687557

HMAC摘要算法 /cdzwm/article/details/6973345

android的rsa加密填充方式是RSA时,是NoPadind RSA/ECB/NoPadding,

而标准jdk里填充是RSA时,是指PKCS1填充,RSA/ECB/PKCS1Padding,要注意

RSA加密科普 /blog//06/rsa_algorithm_part_one.html

RSA加密科普 /blog//07/rsa_algorithm_part_two.html

RSA密钥长度关系 /developer/article/1199963

python rsa加密库 https://pycryptodome.readthedocs.io/en/latest/src/examples.html#generate-an-rsa-key

公私钥ASN.1结构 /wzj_whut/article/details/86477568

ASN.1、PKCS、PEM间的关系 /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_cbchook 时 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 afv15 里面存的数据是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 参数值 48v12 参数值00 00 00 00 20 00 00 00 61 62 6c 65 2d 61 6e 79v15 参数值8e 02 d3 2f 1f 5d a4 14 b2 b0 6e 8b af ca b7 ad128v1775 4c 8f d5 84 fa cf 62 10 37 6b 2b 72 b0 63 e4 iv值

decode2 参数的 16进制数据:

现在 key、iv、16 进制数据都有了,可以尝试下解密:

Python 的 AES 加密与解密:/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 AESfrom 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 AESfrom 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 base64from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadimport 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 数。。。略略略略略

如果觉得《frida hook so层 protobuf 数据解析》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。