frida hook so层、protobuf 数据解析
手機安裝 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 數。。。略略略略略
總結
以上是生活随笔為你收集整理的frida hook so层、protobuf 数据解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web.py 十分钟创建简易博客
- 下一篇: Python3.2+ 的 concurr