最近在重写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
emmm这么发出来真的好吗。
实在是强!