当前位置:网站首页>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 数。。。略略略略略

原网站

版权声明
本文为[擒贼先擒王]所创,转载请带上原文链接,感谢
https://blog.csdn.net/freeking101/article/details/123715121