驾考宝典sign逆向分析

驾考宝典sign逆向分析

环境

app 6.9.8

Java层

抓包

image-20211207161248033

jadx搜索"sign",只有一个

image-20211207161353962

查找用例

image-20211207161505133

结果不是很多,但是看起来都不太像,还有几个是调用baidutencent的包,应该是有接入什么sdk或者其他的。换个关键词,搜索"_r"

image-20211207161655299

aw.a -> a

image-20211207161729346

aw.a -> N

image-20211207161756578

cn.mucang.android.core.utils.aa -> ao

image-20211207161836619

jadx不太给力啊。。换个工具,GDA打开

image-20211207162044702

cn.mucang.android.core.jni.Riddle -> s

image-20211207162242643

最后调用了native函数,对应的so是libtnpn.so

frida hook一下

function hook_s(){
    var Riddle = Java.use("cn.mucang.android.core.jni.Riddle");
    Riddle.s.implementation = function(str0, str1) {
        console.log("s-str0=", str0);
        console.log("s-str1=", str1);
        var ret = this.s(str0, str1);
        console.log("s-ret=", JSON.stringify(ret));
        return ret;
    }
}

Java.perform(function() {
    hook_s();
})
image-20211207165228543

第一个参数str0很容易构造,但是第二个str1就不太理解了,它好像不是生成随机的,因为有些请求的str1是相等的,jadx搜索*#06#

image-20211207165502133

好家伙,这样推断的话,应该是每个接口有对应的str1?

so层

ida打开,搜索Java

image-20211207165939710

说明是静态注册的,打开Java_cn_mucang_android_core_jni_Riddle_s

image-20211207170015976

看看j_j_GetSigningVersion

image-20211207170103773
image-20211207170112261
image-20211207170125198

所以,当str1包含*#06#时,会调用j_j_SignUrl1,否则调用j_j_SignUrl0

看看j_j_SignUrl1

image-20211207170434525
image-20211207170504797
image-20211207170540432

上面的代码算是通俗易懂了,先把str2的解码实现一下

import base64

def decode(s):
    s1 = base64.b64decode(s)
    s2 = bytes(map(lambda x: (x-42) ^ 0x2A, s1))
    return s2
image-20211207173135413

然后可以看到,它还调用了j_j_SignUrl0方法

image-20211207171521716
image-20211207171532156
image-20211207171542805

简单的将a1a2拼接一下,然后做个md5,a3是结果

image-20211207195609978

然后是这一部分,是把a2转为uint8后求和,然后会调用sub_E9470

image-20211207195622298
image-20211207195632129
image-20211207195705029

函数的实现看起来花里胡哨的,里面有个__clz函数,搜了下,是计算前导零的个数

__clz:
Count Leading Zeros ,计算前导零指令;
指令编码格式
__clz指令返回操作数二进制编码中第一个1前0的个数。如果操作数为0,则指令返回32;如果操作数二进制编码第31位为1,指令返回0。

功能:假如一个数为0x1FFF FFFF,则转换为2进制为 (0001 1111 1111 1111 1111 1111 1111 1111),
则 __clz(0x1FFF FFFF) 的值为3;

还有这篇文章指出,该函数的作用就是实现除法。

然后就很容易脑补出v10就是余数,就是这么自然~

image-20211207200317234

最后就是拼接md5结果和余数了,当余数为0时,直接返回md5,此时byte数组长度为16;当余数不为0时,拼接md5结果和余数再返回,此时byte数组长度为17。这也解释了为什么sign的长度有时是32,有时又是34。

代码实现

import base64
import hashlib

def decode(s):
    s1 = base64.b64decode(s)
    s2 = bytes(map(lambda x: (x-42) ^ 0x2A, s1))
    return s2

def calc_sign1(url, key):
    key0 = decode(key)
    sign = calc_sign0(url, key0)
    v9 = sum(key)
    _, v10 = divmod(v9, 0x13)
    if v10:
        sign = sign + f'{v10:02x}'
    return sign


def calc_sign0(url, key):
    sign = hashlib.md5(url + key).hexdigest()
    return sign


def calc_sign(url, key):
    if isinstance(url, str):
        url = url.encode()
    if isinstance(key, str):
        key = key.encode()

    if key.startswith(b'*#06#'):
        sign = calc_sign1(url, key[5:])
    else:
        sign = calc_sign0(url, key)
    return sign
image-20211208155310645

其他

每个API对应的key是什么,可以用frida hook打印一下,请自行处理,因为我只是想分享一下逆向的思路,并不是要去采集什么数据。

代码仅供把玩。

你可能感兴趣的:(驾考宝典sign逆向分析)