本文原载于 DIARY,但是考虑到博客很久都没更新了,加上 DIARY 设立的本意是记录个人生活,所以这篇放在 DIARY 也有些不妥,故思索再三,移至此处更新。
最近在重写 BiliAPI,由于之前采集的接口都是两年前的东西了,所以这次打算再逆向 B 站客户端。版本使用了官网下载的 5.29.1。
8.16
可以说比起两年之前,开发者安全意识高了很多。ak、sk 对原先是采用令人费解的存储在 native 层,然后 get 出来在 java 层拼接请求地址。后来对 sk 进行了加密(好像是 AES,太久了忘了),但是并没有太大的区别。而如今采用了正确的 native 层拼接的方式,安全性陡增。java 层终于混淆字符串了。
突破口就是 native 层,顺势就找到了 com.bilibili.nativelibrary.LibBili。s 方法(descriptor:(Ljava/util/SortedMap;) Lcom.bilibili.nativelibrary.SignedQuery;)简直不要太明显,顺着就找到了 bl.idh 类。里面一个加通用请求参数一个加请求头的方法直接可以挖掘出很多信息。由于 B 站 App 用的是 okhttp,而 okhttp3 包下类多有残缺,说明肯定被混淆了一部分。果然,bl.mhs 对应 okhttp3.Request、bl.mhm 对应 okhttp3.Headers…… 这样一恢复,那整个 http 请求相关的部分就清楚很多了。
native 层也终于有点安全意识了,简单的看了一下,关键几个方法都进行了一定处理。不过并没有做特别的针对 IDA 的处理。这里看关键的 libbili.so。在函数表可以看到一些方法名和 descriptor 名。
分析起来有点费劲…… 毕竟和 java 字节码不同,而且我对汇编根本只是了解了点皮毛。不过看到一些函数打印的 log 里有 J4A,那就瞬间舒服了。简单说明下,J4A 是 B 站 native 层开发自己造的轮子,开源的 ijkplayer 就用的这个。于是,几个 J4A 的方法就陆续找到了。由此,一些变量的意义也清楚了。其实最烦的就是通过 JNIEnv 调用的方法(对应源码中的 (**env).func (…);):
每一次都要跑到函数表里算一算,那叫一个辛酸啊。以后再弄好了,现在重点还是 java 层的逻辑,native 层大致了解下就行。
8.17
今天主要是研究接口的请求部分。切入点是 bl.idh,根据子类和内部实现,暂取作 BaseRequesterInterceptor,实现的接口 bl.idk 取作 IRequestInterceptor。衍生的子类有:LiveRequestInterceptor(bl.btn)、BanguiRequesterInterceptor(bl.bfn)、SsoRequesterInterceptor(bl.bta,实现很幽默)、SearchRequestInterceptor(bl.btj)…… 有些类还没来得及命名。其实搞清楚 RequesterInterceptor,基本上就可以自己构建任意请求了。比较重要的方法就是 appendParam(Obf name: a, Descriptor: (Ljava/util/Map;) V)和 appendHeader(Obf name: a, Descriptor: (Lbl/idh$a;) V)。具体接口的 RequesterInterceptor 一般都重载了这些方法以加入特有的参数。
值得一提的是,Get 与 Post 方法的区分非常迷幻。LiveRequestInterceptor 重载了 buildPostRequest 方法,然后用了一个 LiveApiChecker(bl.btm)的 isGetRequest 来检查是否为 GET 方法的接口,如果是就调用 super.buildGetRequest,可以说是很迷幻了。另外,switch 使用 String 的逆向结果看着真是累,FernFlower 不用说,CFR 竟然也不能恢复(可能是姿势不对)。
其余干了啥就想不起来了……
8.18
今天开始着手整 native 层。之前我沙雕了,只要引入 jni.h 就行了。我用的 IDA7.0 自带这些结构体,用 IDA 的 F5 大法,对着变量按 y 然后无脑 JNIEnv * 就行。这次主要是把 LibBili 的 getAppkeyByDeviceNative(Obf name: a,descriptor: (Ljava/lang/String;) Ljava/lang/String;)和 appendSignNative(Obf name: s,descriptor: (Ljava/util/SortedMap;) Lcom/bilibili/nativelibrary/SignedQuery;)俩方法搞清楚。另外一个 AES 有关的 java 层还没看是干啥用的,暂时鸽着。
getAppkeyByDeviceNative 比较好分析,主逻辑在 sub_2f60。没啥复杂的,就是字符串对比然后返回对应的 appkey,唯一的重点就是知道了几个可用的 device:android、android_i、android_b、android_tv、biliLink。
appendSignNative 比较复杂,主逻辑在 sub_27a0。先是一堆无关紧要的参数验证,然后是核心的 sign 计算。首先调用 SignedQuery 的静态方法 mapParamToString(dword_C0AC)拼接参数,然后调用 sub_2600 获取 appkey 的类型。sub_2600 接受 appkey,返回整数 0-4(对应上面的 5 个 device)。
之后将返回的整数作为数组下标,访问存储 secretkey 的 dword_7980、dword_7994、dword_79A8、dword_79BC。最后把内容拼接在之前拼接的参数之后,整体计算 MD5(sub_2070),然后 new 一个 SignedQuery 对象并返回。
secretkey 以整数形式分别存储于数组 dword_7980、dword_7994、dword_79A8、dword_79BC。
于是就可以拼出 5 个 ak、sk 对了:
Device: android
Description: 普通版
AppKey: 1d8b6e7d45233436
SecretKey: 560c52ccd288fed045859ed18bffd973Device: android_i
Description: 国际版
AppKey: bb3101000e232e27
SecretKey: 36efcfed79309338ced0380abd824ac1Device: android_b
Description: 概念版
AppKey: 07da50c9a0bf829f
SecretKey: 25bdede4e1581c836cab73a48790ca6eDevice: android_tv
Description: 电视版
AppKey: 4409e2ce8ffd12b8
SecretKey: 59b43e04ad6965f34319062b478f83ddDevice: biliLink
Description: 直播
AppKey: 37207f2beaebf8d7
SecretKey: e988e794d4d4b6dd43bc0e89d6e90c43
只能说比原先安全了点。总结下 ak、sk 存储方式的变化:
- (两年前)俩 native 方法直接 get,安全性基本没有。
- (一年前)sk 做了 RSA(也许是记错了,反正加密了下)加密,安全性有所提升。但是新建个 app 然后调用下解码方法就行,并没有什么卵用。
- (现在)sign 计算写进 native 层,使用整数数组拆分存储 sk。比较安全,然而 sk 并没有编码保存。
有趣的是,视频接口的 ak、sk 并不在 libbili.so。看来只能继续分析 java 层了。
java 层的分析最后以 IMediaResolver 入手,找到了几个 Resolver:NormalResolver(bl.gmh)、BangumiResolver(bl.gmd)、LiveResolver(bl.gmf)。然后就找到了拼接参数用的 ApiRequest(bl.glw),然后就找到了实现 ISigner(bl.gmb)的 NormalSigner(bl.glx)和 VideoSigner(bl.glz)。查看 VideoSigner 的实现发现调用了 VideoSignHelper(bl.gmu)的相关方法进行参数拼接。VideoSignHelper 使用了特殊的编码方式来存储视频用 ak、sk 对,代码如下:
public static String decode(int shift, String encodedStr) { String result = ""; byte[] encodedBytes = encodedStr.getBytes(); for (byte chr : encodedBytes) { int var8 = 65 + (chr - 65 + shift) % 57; int shiftCount = 0; while (var8 > 90 && var8 < 97) { shift += shiftCount * shift; ++shiftCount; var8 = 65 + (chr - 65 + shift) % 57; System.out.println("t" + shift); } result += (char) var8; } return result; }
类似凯撒密码,ak 的偏移是 3,sk 的偏移是 9。最后得到的 ak、sk 如下。
AppKey: iVGUTjsxvpLeuDCf
SecretKey: aHRmhWMLkdeMuILqORnYZocwMBpMEOdt
9.26
咕咕咕无人出我左。这一个半月真是特别特别忙,简单介绍下近况。由于 app 内大部分接口实现几乎一致,所以现在采用了 asm 直接采集所有的接口,算是省了些事。
没了
大佬您好,请问实现这样的操作需要哪些基础知识呢?根据手记尝试了一下,发现果然欠缺的还是太多了。期望能指点一下方向,谢谢.
首先对 Android 包括 NDK 要有一定了解,然后就是要有一定的反混淆经验(Java 逻辑)和二进制逆向(Native 逻辑)的经验。最好对 APP 开发常用的库也有一点了解。
感谢您的回复,请问有哪些书籍是可以推荐的吗?说来惭愧,会一点 Java 的开发,但是您回复中指出的 (反混淆经验),(二进制逆向 Native 逻辑)完全不知从何了解,再次感谢
最后就是,方便的话,请问手记中提到的 bl.* 一类的东西到底是什么呢?通过百度 / Google 加工具也完全没有找到,能提点一下也是非常感谢
反混淆经验主要靠多拆 app 吧,就是对那些混淆过的类名进行还原。二进制逆向可以看安全方面的书,或者看看雪论坛这类 bbs。
手记中的 bl.* 是那个版本 app 的类名,是经过混淆的。
感谢大佬您的抽空回复,最近发现大佬 blog 后真是打开了新世界的大门,感觉有了学习的动力
哈哈,很开心能帮到你:)
:), 大佬加油ヾ (◍°∇°◍)ノ゙b( ̄▽ ̄)d
请问大佬视频解析的 sk 是否已失效
我刚刚测试了下,没有失效啊。失效的判断可以参考安卓端旧版本,一般不会失效的。
跟着大佬多学习