6.1 上海公交

抓包分析

先打开 app 搜索关键字进行分析,发现了其请求和响应都是加密的

入口定位

因为其加密参数为 request , 因此使用 jadx 反编译之后直接搜索 request 尝试, 发现了以下结果

一个个点进去,发现第一个比较像,因此我们采用 frida hook 一下!

hook 代码如下, 本段代码中因为结果是一个 ByteArray , 所以我们可以按照源码来找到转为字符串的方法。然后再对比一下抓包结果,发现这里就是加密入口

        let Native = Java.use("com.shjt.map.tool.Native");
        let Base64 = Java.use("android.util.Base64")
        Native["encode2"].implementation = function (bArr) {
            console.log(`Native.encode2 is called: bArr=${bArr}`);
            let result = this["encode2"](bArr);
            console.log(`Native.encode2 result=${Base64.encodeToString(result, 0)}`);
            return result;
        };

知道了解密入口,我们还需要分析入参,因此,但是我们的入参是一个 ByteArray, 因此我们可以仔细分析一下,查看 jadx 源码可知

byte 数组来自于 protoc,因此想要还原我们可以查询一下文档 / 源码,文档地址为 Google.Protobuf.ByteString Class Reference

根据源码我们知道, 通过 ByteString.copyFrom 方法就可以还原为 ByteString, 然后再通过 toStringUtf8 方法就可以获取还原好的字符串。

因此 frida 代码可以修改为如下代码:

        let Native = Java.use("com.shjt.map.tool.Native");
        let Base64 = Java.use("android.util.Base64")
        const ByteString = Java.use("com.google.protobuf.ByteString");
        Native["encode2"].implementation = function (bArr) {
            console.log(`Native.encode2 is called: bArr=${bArr}`);
            console.log(`Native.encode2 is called: bArrStr=${ByteString.copyFrom(bArr).toStringUtf8()}`);
            let result = this["encode2"](bArr);

            console.log(`Native.encode2 result=${Base64.encodeToString(result, 0)}`);
            return result;
        };

这个时候我们的入参也分析出来了!根据上面的步骤可以知道,加密方法是存在于 so 文件的,so 文件为 native, 加密方法为 encode2, 接下来就可以步入正题,开始进行 IDA 算法还原了

算法还原

首先我们先把 apk 解压,在 lib 里面找到我们需要的 so 文件,然后用 ida 打开

看到左边的函数很快就能定位的加密函数的,点进去然后按一下 tab 键就可以看到伪代码,那么就可以开始分析加密逻辑了!先简单看一下

好像加密方式是 aes CBC ,既然是 aes, 那么我们就需要 ivpaddingkey。 这边暂时先不还原算法,我们先要搞清楚刚刚提到的关键参数值到底是什么,因此我们可以借助 frida 进行 hook,我们先猜测 aes 的秘钥相关设置都在 aes_key_setup 这个函数里面,先双击点进去

拿到导出函数的签名:_Z13aes_key_setupPKhPji

然后编写 frida hook so 层的代码, 这个是标准的模板。

        let aes_key_setup = Module.findExportByName("libnative.so", "_Z13aes_key_setupPKhPji")
        Interceptor.attach(aes_key_setup, {
            onEnter: (args) => { 

            },
            onLeave: (retval) => { 
                
            }
        })

然后我们因为要分析 key, 所以肯定要分析参数,因此可以继续在 ida 里面看一下参数类型

可以看到前两个参数是指针类型(可以理解为内存地址,可以指向原本的变量),第三个参数是 int 类型,至于怎么打印,我们需要查阅 frida 的文档,地址为:JavaScript API | Frida • A world-class dynamic instrumentation toolkit

例如我们需要打印 int 就可以使用 toInt32

而本案例中,因为我们猜测他用的是 aes,所以其实大概率就是 ByteArray 了,通过查询文档知道,如果我们需要读取 ByteArray,那么我们应该使用 readByteArray, 并且从伪代码中可以看到,这里使用的可能是 128 位的,所以换算为 bytes 那么就应该读取 16 位,因此我们的 hook 代码就可以修改为:

        let aes_key_setup = Module.getExportByName("libnative.so", "_Z13aes_key_setupPKhPji")
        Interceptor.attach(aes_key_setup, {
            onEnter: (args) => {
                console.log("\n================================================ \n")
                console.log(">>> arg0\n", args[0].readByteArray(16))
                console.log(">>> arg1\n", args[1].readByteArray(16))
                console.log(">>> arg2\n", args[2].toInt32())
                console.log("================================================ \n")

            },
            onLeave: (retval) => {

            }
        })

如果 hook 不到的话,需要延迟 hook ,因为要等这个 so 文件加载之后才可以,然后点击搜索触发,可以看到已经 hook 到了

可以看到一下出现了两条记录,这个原因我猜测就是,请求的时候加密了,响应的时候解密了,那就会有两条,那这种情况就可以解释了!既然是 aes, 那加解密的秘钥应该是一样的把,所以可以看到共同部分:2f d3 02 8e 14 a4 5d 1f 8b 6e b0 b2 ad b7 ca af

那么我们先猜测秘钥为 2fd3028e14a45d1f8b6eb0b2adb7caaf

然后我们接着 hook 一下 aes_encrypt_cbc,打印函数类型的话,就需要经验和尝试了!

hook 代码:

       let aes_key_setup = Module.getExportByName("libnative.so", "_Z15aes_encrypt_cbcPKhjPhPKjiS0_")
        Interceptor.attach(aes_key_setup, {
            onEnter: (args) => {
                console.log("\n================================================ \n")
                console.log(">>> arg0\n", args[0].readByteArray(16))
                console.log(">>> arg1\n", args[1].toInt32())
                console.log(">>> arg2\n", args[2].readByteArray(16))
                console.log(">>> arg3\n", args[3].readByteArray(16))
                console.log(">>> arg4\n", args[4].toInt32())
                console.log(">>> arg5\n", args[5].readByteArray(16))
                console.log("================================================ \n")

            },
            onLeave: (retval) => {

            }
        })

这里有几个字符串值的分析一下:

  • 0a230a182f70726f746f632e52657175

  • 616e64726f69642e737570706f72742e

  • 8e02d32f1f5da414b2b06e8bafcab7ad

  • 754c8fd584facf6210376b2b72b063e4

经过多次搜索发现第一个和第二个在变化,所以先排除第一个和第二个。那么 iv 应该是以下这两个选择

  • 8e02d32f1f5da414b2b06e8bafcab7ad

  • 754c8fd584facf6210376b2b72b063e4

写一段解密的代码进行测试:

import binascii

import blackboxprotobuf
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad


def aes_text(hexdata, key: str, iv: str):
    aes = AES.new(key=binascii.a2b_hex(key), mode=AES.MODE_CBC, iv=binascii.a2b_hex(iv))
    a2b = aes.decrypt(binascii.a2b_hex(hexdata))
    message, typedef = blackboxprotobuf.protobuf_to_json(unpad(a2b, 16), message_type=None)
    print(message)

使用 keyiv 进行解密测试,我们加密后的数据经过抓包就可以得到: wGUFixSXEs26Hu/F1P0kGbiEywc3GzF+1E2azlIHBQkoUVu9H0WS4j40GqHQdfZj

然后因为这个是一个 base64 代码,所以我们需要先解开编码之后再转换为 16 进制

import base64

data = base64.b64decode("wGUFixSXEs26Hu/F1P0kGbiEywc3GzF+1E2azlIHBQkoUVu9H0WS4j40GqHQdfZj").hex()
aes_text(
    hexdata=data,
    key="2fd3028e14a45d1f8b6eb0b2adb7caaf",
    iv="8e02d32f1f5da414b2b06e8bafcab7ad"
)

第一个解密报错了,我们修改为第二个

import base64

data = base64.b64decode("wGUFixSXEs26Hu/F1P0kGbiEywc3GzF+1E2azlIHBQkoUVu9H0WS4j40GqHQdfZj").hex()
aes_text(
    hexdata=data,
    key="2fd3028e14a45d1f8b6eb0b2adb7caaf",
    iv="754c8fd584facf6210376b2b72b063e4"
)

发现解密成功了,所以可以得出结论:

  1. 加解密方式为: AES CBC

  2. key 为:2fd3028e14a45d1f8b6eb0b2adb7caaf

  3. iv 为: 754c8fd584facf6210376b2b72b063e4

  4. 数据为 protobuf 序列化的数据

最后更新于

这有帮助吗?