因为平时看轻小说厕纸比较多,为了能够获取接近纸质书的体验,大多数都会下载epub的文档到Kindle上来阅读(其实就是想看插画),手动去找各类资源也不是不行吧,就是属实是有点麻烦,排版也不尽统一。恰好国内某站点提供相当全面的轻小说资源,且提供了Windows和Android平台客户端,获取资源不要太方便。但是吧,因为一些敏感的问题,他的web站点不再对外开放了,对于我个人来说一般来说不会在pc阅读小说,而且Android客户端的阅读体验属实有点一言难尽,就一直思量着能不能分析一下协议重构一个web端,方便在在Kindle直接用浏览器获取相应的轻小说资源。

抓包

抓包当然是第一步,不抓包怎么去分析协议呢对不对,在使用Fiddler一通乱抓以后我大概获取了以下信息

登陆接口的重要参数有3个,分别是uname,pass,appToken,其中uname为明文的账号信息,pass为32位密文,appToken为32位密文+“.”+32位密文,在成功登陆以后服务器会返回token作为全局身份认证

剩余的部分实际上并不复杂,所以就不再赘述了,关键是如何获取passappToken的加密方式,在使用多个账号登陆抓包分析后发现,密文信息只于原密码或潜在的salt有关,与时间戳或其他信息并无关联。而且pass的密文为32位,很有可能是使用了MD5算法。但是简单的使用MD5算法和嵌套加密后发现,并没有获取相应的密文。也就是说,在加密过程中很有可能加盐了,那么我们就需要对他的客户端下手了。

APK反编译静态分析

请出Android Killer,apktool,dex2jar等一系列工具来帮助我们进行分析,Android Killer作为老牌逆向软件虽然停更多年,但是仍旧宝刀未老。简单配置一下apktools就能正常使用了。我们先来直接进行一个搜素

很遗憾,这都是一些第三方依赖的内容,并没有获取到我们想要的数据,换一种思路

好耶,看起来这是一个关键点,那么让我们来尝试分析他一下

很好!很显然他使用了盐值来对密码进行加密,继续查看smali代码,果不其然的发现了MD5法方,不过直接阅读smali代码还是有些太别扭了,可读性不强,让我们使用dex2jar反编译smali代码到java代码,拖进idea来阅读,当然啦你也可以使用jd-gui来阅读,我个人更习惯于使用idea

哦老天,看到这个".“了吗,很显然他就是前面抓包时获取到appToken中的那个”.",他是被多次嵌套以后拼接在一起的。这个英文句号对于我们的逆向过程提供了巨大的帮助,但是在进一步阅读代码后发现,他实际上是一个认证客户端和DeviceInfo的token,虽然找到他确实很重要,但是我们对于最最核心的pass的加密方式仍然一无所知

Xposed Hook

在逆向出appToken以后我受到了一些启发,既然最终拼接到请求上的值是一个String,且需要加盐,那么在客户端拼接satl时一定会将两个String拼接起来。而在java的编译中,编译器会把字符串拼接的"+"编译成java.lang.StringBufferappend()方法,且最终一定会toString()。在安卓平台,我们拥有强大的Xposed框架可以让我们编写模块来对应用进行Hook操作,那么我们只需在登陆时Hook一下toString()方法并将变量值在log中输出,岂不是可以相当轻易的获取到他的盐值和加密过程了吗!

理论存在,实践开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Main : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam?) {
// 过滤器
if (lpparam!!.packageName != "【您要处理的app的包名】")
return
// 捕获
XposedHelpers.findAndHookMethod(StringBuilder::class.java,
"toString",
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam?) {
Log.d("xposed捕获", "before")
}

override fun afterHookedMethod(param: MethodHookParam?) {
Log.d("xposed捕获", "after")
val result = param!!.resultOrThrowable
Log.d("xposed捕获", result.toString())
}
})
}
}

具体的操作就懒得放了 在这里给上一个主要hook代码 如果对hook有兴趣的朋友可以自行搜索Xposed Hook教程

但是好像他什么也没有hook到,并没有获取到salt的拼接。也就是说,他没有在java代码里处理密码的加密,这实在是太奇怪了,那么他到底在哪里对密码进行加密呢?再次打开Android Killer,使用全局搜索

终于我们找到了这个pass的拼接位置,这看起来像一个js,那么很有可能他是使用了js在前端就对密码进行了加密(这个说法并不严谨),拼接好了请求再使用java代码进行http请求的,那么使用hook我们自然就没有办法钩出盐值了。

分析一下这个index.android.bundle,百度了一下才知道,app使用了ReactNative框架,也就是说他的主要逻辑代码全部写在了这个index.android.bundle里面,他确实是一个js文件,但是被压缩了。想必加密方式也隐藏其中,那么目标很明晰了,让我们开始分析吧。

JS分析

格式化以后共计,嗯,75885行,好家伙。而且变量名和函数名都丢失了,变成了a,b,c这样难懂的符号,对于我这样JS苦手的人来说相当的麻烦,没办法,硬着头皮继续分析下去。

定位到关键代码,开始逆推。果然格式化以后看起来舒服多了。

继续寻找这个fetchUser,果不其然,发现了一个相当明显的特征

他拥有4个参数,与我们上面找到的参数应该是一一对应的,"pass="后拼接的参数n在2号位置,也就是说(0, p.CryptoStr)(i)很有可能就是密码的加密方式!在参数表中我们可以看到

1
2
o = this.state,
i = o.pass;

不难猜想,o应该是一个控键之类的东西,o.pass肯定就是原密码了!一切的一切,都指向了CryptoStr这个函数

故技重施,继续搜索

很显然,我们已经逼近我们所需要的答案了,这个函数的返回值就是加密后的密码。但是这里有一个更严重的问题。这个不知道哪里冒出来的d.default是什么玩意?虽然我也尝试对其进行分析,但是一路找回去发现他的引用实在太多太杂了,根本就不是人类能够看懂的。看起来好像卡住了,就在这临门一脚的地方卡住了,冷静一下,整理一下现有的线索

  1. 密文是一个32位长的字符串,很可能使用了MD5加密
  2. 加密看起来由一个函数多次嵌套加密后,混入一部分凯撒加密完成
  3. 现在最大的问题是解决d.default

这确实有些棘手了,等一下,这个嵌套格式,怎么这么眼熟呢?有没有可能他就是MD5方法呢?本着大胆猜想小心求证的原则,试着把它粘贴出来变形一下

笑死,不能说一模一样,只能说完全一致,这个MD5嵌套可以说是典中典,而e很显然就是我们的原密码了。那么输入密码来生成一次与抓包获取到的密文来比对试试

成功!

总结

这次逆向累计花费时间长达数十个小时,主要是各种工具的使用都是第一次,还有xp模块编写时对主类指定文件xposed_init后加了.txt后缀导致模块起不来的低级错误,历经重重磨难终于抵达了终点且成功分析出了加密方式,总结来说还是那句话

大胆猜想,小心求证

本来其实是抱着玩一玩的心态来逆向的,没想到最终居然真的分析出来了,也算是相当宝贵的一次经历吧。最后感谢一下mzdluo123的思路提示和Him188对Xposed模块开发的指导以及广大Mamoe群友的出谋划策,没有他们的帮助可能我根本无法分析出结果。