前文:https://bbs.tampermonkey.net.cn/thread-3560-1-1.html
用jd-gui打开合并后的jar,搜索encryptString,可以把下面的选项全勾上,逐个查看搜索结果,确定加密函数应该是这个:
com.cmccit.webview.jssdk.encrypt.EncryptString.invoke
其中关键加密是这个:EncryptionLogin.getEncryptTelAndURLEncoder
直接点击跳转(这也是文件合并的好处),看到代码是先getEncryptTel一下,然后URLEncoder.encode(url编码,相当于js里的encodeURIComponent)。我们跟进getEncryptTel,最终可以定位到这个地方:
注意这里用了native关键字,说明该函数来自外部,代码里还有一句System.loadLibrary("jni"),这是加载动态链接库的意思,即这个函数被封装在一个叫jni的动态链接库里。Windows系统的动态链接库通常是dll文件,而安卓则是so文件,位于lib目录下(apk解压后的目录),且命名前会加一个lib,所以我们要找的文件就是libjni.so。
so文件的反编译没有特别好用的工具,一般是用IDA打开,里面一堆汇编指令,按F5转为伪代码帮助阅读(C语言)。IDA有两个快捷方式,这个so是32位,不要用x64的打开,否则按F5没反应。这里就不细说IDA的使用方法了,有兴趣可以自行研究。搜索EncryptTEL,定位到以下代码:
这个相对比较难阅读,需要一定C语言基础。翻译一下就是用leadeon+明文+时间(sub_75D0是生成时间的函数)拼接成新的明文,再调用j_algo_encrypt加密。双击j_algo_encrypt可以跟进,不过后面代码比较抽象,其实从各种函数命名上可以看出,这是RSA加密,我们当然不是来探讨RSA加密原理的, 只需知道密钥就够了。考虑到这些APP通常用的公共加密库,代码的分析可以偷个懒,搜几个有代表性的函数名来反推他用的库,于是我搜到了这篇文章。注意对比这两句代码:
r是密钥,对应&rsa_s1;plaintext是明文,对应拼接后的结果;明文长度是原长度+21,因为“leadeon”7位,时间14位(年4月2日2时2分2秒2),加起来正好21。后面的参数不用管了,我们可以大胆推断,加密公钥就是rsa_s1。双击rsa_s1,按ctrl+x查看调用,找到以下定义处:
这是一个Elf32_Sym结构,结构的元素是name(aRsaS1)、value(内存地址,1DD68,IDA已帮我们替换为rsa_s1)、size(内存大小)、info、other、shndx。其他元素不用管,只需记住内存大小即可(0x2000对应十进制8192)。双击rsa_s1跳转到内存地址处,看到是一堆0,这是哪里出错了吗?让我们回到前面的分析中,已知加密库为polarssl,那么密钥结构是怎样的?在上面那篇文章中,generate_rsa用于生成密钥,注意这段代码:
密钥被分为8部分,每个部分是等长的BUFFER_SIZE,8192除以8等于1024,所以密钥是1024位(RSA密钥不是1024就是2048),不足1024位的部分会在前面用0填充,所以真正的密钥应该在后面。验证一下,将1DD68这个地址偏移1024位(0x400)得到1E168,往下拉到这个地址看看:
rsa_s1被分为8段,第一段N的取值范围就是1DD68~1E167,可读出N的值为(保留十六进制数):
A3123079DD30EAF54C8CDC1677576ABD8BEE25938116F64F22F0D5B5D70F0FD6FF40895D6CF554A63F7707B6A5A6EA5C7B114E2D611E2F675726961D0E5CF289B70331E3DDCAFC096E888E686E252E52DBE01D3F3970F4D7F7C44AF1AF01931925798CEEB1CC89239BA8885DF3ED5C79DEF4D71BC578959DCACC70539A2C230D
同理可得E为10001
,后面的都是0,这其实就是典型的RSA公钥对,加密只需要n和e就够了(完整的私钥在服务端,非对称加密的特点)。取得公钥后我们其实已经完成了这个算法的破解,即leadeon+UID+时间拼接成明文,然后用公钥做RSA加密,最后encodeURIComponent一下。
js做RSA加密有一个现成的库JSEncrypt,但这个库只能直接设置pem格式的密钥,可以先用别的工具把(n, e)密钥对转成pem格式,不过JSEncrypt的源码其实内含了转换的算法,我们可以利用一下,用这种方式来设置密钥:
const encrypt = new JSEncrypt();
encrypt.setKey();
encrypt.key.setPublic(N, E);
setKey实例化一个空密钥,该实例属于库内部的私有类RSAKey,调用这个私有类的setPublic方法直接传入n和e即可。有兴趣的话可以自己研究下这个库的源码。
还有个细节是拼接用的时间是UTC时间,js里的时间是计算本地时区的,用这个函数简单处理一下:
function getUTCTime() {
return new Date().toISOString().replace(/[-T:]|\..*$/g, '');
}
至此我们完成了UID的加密破解,剩下的问题就是怎么得到UID了,后面的分析大部分基于Java源码,就留待下文分解吧。