一点资讯signature分析
环境
app:4.9.6.1
Java层
抓包
jadx搜索"signature"
defpackage.bhl -> d
转到com.yidian.news.util.sign.SignUtil.a
可以看到最终是调用了native函数signInternal
。
frida hook一下
Java.perform(function(){
var signUtil = Java.use("com.yidian.news.util.sign.SignUtil");
signUtil.signInternal.implementation = function(ctx, s1) {
console.log("s1=", s1);
var ret = this.signInternal(ctx, s1);
console.log("ret=", ret);
return ret;
}
})
输入的构造可以参考上面defpackage.bhl-j
函数是如何调用d
函数,并结合抓包的结果
so层
ida打开libutil.so
,函数窗口搜索"Java"
说明是静态注册,进入signInternal
函数
sub_EAC
sub_C50
emm,里面调用的函数有点多,一时间不知道干什么,先从输出开始倒推吧。
从signInternal
可以看到v24
是返回结果,而它是在sub_EAC
的第4个参数参与计算的;
然后再看sub_EAC
的a4
,a4
由v9
赋值,而v9=v8
,v8
由v12
赋值,v12
是sub_C50
的第2个参数;
继续分析sub_C50
,可以看到a2
在sub_3560
参与了计算,所以应该看看这个函数
好家伙,一看就是base64了,那个字符串就是码表。
继续倒推,sub_C50
的输入应该是v19
和v14
,它在sub_4AF4
参与了计算,查看一下
通过里面的字符串,我们猜测整体是一个RSA
加密,然后再base64编码,有一个tomcrypt
需要上网查查
hook验证这两个函数验证一下我们的猜想
function dump(name, addr, legnth) {
console.log("======================== " + name + " ============");
console.log(hexdump(addr, {length:legnth||32}));
}
var bptr = Module.findBaseAddress("libutil.so");
console.log("base_addr:", bptr);
function hook_4fa4(){
Interceptor.attach(bptr.add(0x4fa4+1), {
onEnter: function(args) {
this.arg2 = args[2];
console.log("0x4fa4");
console.log(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
console.log(args[7], args[8], args[9], args[10]);
// dump("0x4af4-arg0", args[0], parseInt(args[1]));
// dump("0x4fa4-arg2", args[2]);
console.log("0x4af4-arg3", args[3].readPointer());
// dump("0x4af4-arg10", args[10]);
},
onLeave: function(retval) {
dump("0x4fa4-arg2-ret", this.arg2, 8*16);
}
})
}
function hook_3560(){
Interceptor.attach(bptr.add(0x3560+1), {
onEnter: function(args) {
this.arg2 = args[2];
this.arg3 = args[3];
console.log("0x3560");
console.log(args[0], args[1], args[2], args[3]);
dump("0x3560-arg0", args[0], parseInt(args[1]));
},
onLeave: function(retval) {
dump("0x3560-arg2-ret", this.arg2, parseInt(this.arg3.readPointer()));
}
})
}
Java.perform(function(){
hook_4fa4();
hook_3560();
})
和CyberChef的结果能够对上
既然是RSA加密,而且应该是公钥加密,我们需要找出公钥。
在sub_C50
看到有一长串形似base64的字符串,就先看看sub_33F8
这个函数
sub_3318
应该调用了base64解码,猜测它就是公钥,导入看看
好像不太对,应该不是公钥。。
这时候发现下面还有一个base64解码,这个的结果会不会是公钥,hook看看
function hook_33F8(){
Interceptor.attach(bptr.add(0x33F8+1), {
onEnter: function(args) {
this.arg2 = args[2];
this.arg3 = args[3];
console.log("33F8");
console.log(args[0], args[1], args[2], args[3]);
},
onLeave: function(retval) {
dump("0x33F8-arg2-ret", this.arg2, parseInt(this.arg3.readPointer()));
}
})
}
Java.perform(function(){
hook_33F8();
})
我们把第二个的结果作为公钥导入试试
导入成功!至此,我们找到了公钥,也知道了输入,只需要代码实现来验证一下。
代码实现
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
_PUB_KEY = bytes.fromhex('30819f300d06092a864886f70d010101050003818d0030818902818100cfd9263315c70cc19c42d39531c1e8ba8a315c3a824e1cf1c12b6a115fa6550d264cc5bee0847d4753adda6f1cdc5bff8e0bce393a5c42e57640f4066d7e0107758369106c9a087135ceee0bb168a8bd32e962157561af4ab3feb29d479c9f53fc0f738029a0ae0f70cb95ac0b09199695cd84a67c18b8922b11ffce9c1ec0d90203010001')
def rsa_encrypt(msg):
if isinstance(msg, str):
msg = msg.encode()
cipher = PKCS1_v1_5.new(RSA.import_key(_PUB_KEY))
content = cipher.encrypt(msg)
sign = base64.urlsafe_b64encode(content).decode()
sign = sign.rstrip('=')
return sign
其他
其实第一次base64解码的结果,会经过sub_380C
函数处理,作为第二次base64的输入。看看sub_380C
这是一个解密函数,暂不清楚是AES还是DES,或者其他,mode是CTR,填充暂不清楚。
但是实际上,我们已经无需关心这些,因为从frida hook的结果来看,第二次base64的输出是固定的,所以说代码实现的时候直接从这里开始。
总结
从signature的输入来看,只有一个reqid
会变,那么把它固定下来是不是就可以不用处理signature的问题了,因为reqid
包含时间戳信息,它的有效期我没有验证过。此外cookie里面还有个参数JSESSIONID
,如果请求的时候没有携带是没有结果的,它的有效期我也没有验证过,所以说,signature只是其中一环而已。
代码仅供把玩。