驾考宝典sign逆向分析
环境
app 6.9.8
Java层
抓包
image-20211207161248033
jadx搜索"sign",只有一个
image-20211207161353962
查找用例
image-20211207161505133
结果不是很多,但是看起来都不太像,还有几个是调用baidu,tencent的包,应该是有接入什么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
简单的将a1,a2拼接一下,然后做个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打印一下,请自行处理,因为我只是想分享一下逆向的思路,并不是要去采集什么数据。
代码仅供把玩。










网友评论