当前位置:网站首页>frida hook so层、protobuf 数据解析
frida hook so层、protobuf 数据解析
2022-07-06 09:27:00 【擒贼先擒王】
手机安装 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 for
imports. May be specified multiple times;
directories will be searched in order. If not
given, the current working directory is used.
If not found in any of the these directories,
the --descriptor_set_in descriptors will be
checked for required proto file.
--version 显示版本号
-h, --help 帮助信息
--encode=MESSAGE_TYPE 从标准输入读取文本格式信息,然后从标准输出中输出二进制数据,
需要指定 PROTO_FILES
--deterministic_output When using --encode, ensure map fields are
deterministically ordered. Note that this order
is not canonical, and changes across builds or
releases of protoc.
--decode=MESSAGE_TYPE 从标准输入中读取2进制数据,然后以文本方式输出到标准输出,
需要指定 PROTO_FILES
--decode_raw 从标准输入中读取任意的protocol数据,然后以 tag/value的格式输出到标准输出,
不需要指定 PROTO_FILES
--descriptor_set_in=FILES Specifies a delimited list of FILES
each containing a FileDescriptorSet (a
protocol buffer defined in descriptor.proto).
The FileDescriptor for each of the PROTO_FILES
provided will be loaded from these
FileDescriptorSets. If a FileDescriptor
appears multiple times, the first occurrence
will be used.
-oFILE, Writes a FileDescriptorSet (a protocol buffer,
--descriptor_set_out=FILE defined in descriptor.proto) containing all of
the input files to FILE.
--include_imports When using --descriptor_set_out, also include
all dependencies of the input files in the
set, so that the set is self-contained.
--include_source_info When using --descriptor_set_out, do not strip
SourceCodeInfo from the FileDescriptorProto.
This results in vastly larger descriptors that
include information about the original
location of each decl in the source file as
well as surrounding comments.
--dependency_out=FILE Write a dependency output file in the format
expected by make. This writes the transitive
set 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 in
gcc). This flag will make protoc return
with a non-zero exit code if any warnings
are generated.
--print_free_field_numbers Print the free field numbers of the messages
defined in the given proto files. Groups share
the same field number space with the parent
message. Extension ranges are counted as
occupied fields numbers.
--plugin=EXECUTABLE Specifies a plugin executable to use.
Normally, protoc searches the PATH for
plugins, but you may specify additional
executables not in the path using this flag.
Additionally, EXECUTABLE may be of the form
NAME=PATH, in which case the given plugin name
is mapped to the given executable even if
the 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 a
relative file path is specified, the file
will be searched in the working directory.
The --proto_path option will not affect how
this argument file is searched. Content of
the file will be expanded in the position of
@<filename> as in the argument list. Note
that shell expansion is not applied to the
content of the file (i.e., you cannot use
quotes, 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 : XXX
import blackboxprotobuf
def main():
seg_so = None
with 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 = 0
text = text + ('\0' * add)
return text.encode('utf-8')
# 加密函数
def encrypt(text):
key = '9999999999999999'.encode('utf-8')
mode = AES.MODE_CBC
iv = 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_CBC
cryptos = 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_hex
def add_to_16(text):
if len(text.encode('utf-8')) % 16:
add = 16 - (len(text.encode('utf-8')) % 16)
else:
add = 0
text = text + ('\0' * add)
return text.encode('utf-8')
# 加密函数
def encrypt(text):
key = '9999999999999999'.encode('utf-8')
mode = AES.MODE_ECB
text = 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_ECB
cryptor = 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 : XXX
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import binascii
def 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 数。。。略略略略略
边栏推荐
- 数据在内存中的存储&载入内存,让程序运行起来
- Accounting regulations and professional ethics [5]
- cs零基础入门学习记录
- 1010 things that college students majoring in it must do before graduation
- Research Report on market supply and demand and strategy of China's land incineration plant industry
- 【练习-6】(Uva 725)Division(除法)== 暴力
- 力扣刷题记录--完全背包问题(一)
- CEP used by Flink
- Research Report on market supply and demand and strategy of China's earth drilling industry
- HDU-6025-Coprime Sequence(女生赛)
猜你喜欢
MATLAB综合练习:信号与系统中的应用
信息安全-威胁检测-NAT日志接入威胁检测平台详细设计
C语言必背代码大全
渗透测试 ( 2 ) --- 渗透测试系统、靶机、GoogleHacking、kali工具
Penetration test (3) -- Metasploit framework (MSF)
mysql导入数据库报错 [Err] 1273 – Unknown collation: ‘utf8mb4_0900_ai_ci’
TCP的三次握手与四次挥手
Gartner: five suggestions on best practices for zero trust network access
信息安全-安全编排自动化与响应 (SOAR) 技术解析
差分(一维,二维,三维) 蓝桥杯三体攻击
随机推荐
X-Forwarded-For详解、如何获取到客户端IP
滲透測試 ( 1 ) --- 必備 工具、導航
Market trend report, technical innovation and market forecast of lip care products in China and Indonesia
STM32 learning record: LED light flashes (register version)
Market trend report, technical innovation and market forecast of Chinese hospital respiratory humidification equipment
信息安全-威胁检测-flink广播流BroadcastState双流合并应用在过滤安全日志
Information security - threat detection - Flink broadcast stream broadcaststate dual stream merging application in filtering security logs
STM32 learning record: play with keys to control buzzer and led
0-1背包问题(一)
【练习-1】(Uva 673) Parentheses Balance/平衡的括号 (栈stack)
Gartner:关于零信任网络访问最佳实践的五个建议
Matlab example: two expressions of step function
Essai de pénétration (1) - - outils nécessaires, navigation
Research Report on market supply and demand and strategy of China's land incineration plant industry
VS2019初步使用
Research Report on surgical fluid treatment industry - market status analysis and development prospect prediction
Nodejs+vue网上鲜花店销售信息系统express+mysql
Opencv learning log 13 corrosion, expansion, opening and closing operations
Learning record: use STM32 external input interrupt
Opencv learning log 19 skin grinding