APP信息
包名:tv.danmaku.bili
抓包分析数据包
抓包工具开启之后,打开bilibili,随便点一个视频,分析数据包,找到相关数据包
分析请求体
请求体是一堆二进制信息
反编译apk
reportClick
搜索/x/report/click/android2
,找到一个接口,接口里定义了reportClick
方法
查找用例,在如下方法中有对reportClick
方法的调用
1 | public final void a() { |
分析reportClick
方法的参数create
的生成过程,如下
1 | create参数通过c0.create(w.d(com.hpplay.sdk.source.protocol.h.E), H7)方法得到 |
d.this.H7()
其源码如下
1 | public final byte[] H7(long j2, long j4, int i, long j5, long j6, int i2, int i3, long j7, String str, int i4, String str2, String str3) throws Exception { |
分析
H7方法里的参数都放进了treeMap里,然后通过&
拼接成字符串,调用了t3.a.i.a.a.a.b.e.b()
方法进行签名,签名结果通过&
符号拼接到字符串末尾,最后再调用一个t3.a.i.a.a.a.b.e.a()
方法
hook H7
1 | var ByteString = Java.use("com.android.okhttp.okio.ByteString"); |
hook 结果
1 | d.H7 is called: j2=113886342676927, j4=28065268176, i=1, j5=1737979363, j6=1737960450, i2=3, i3=0, j7=0, str=, i4=0, str2=main.ugc-video-detail.0.0, str3=tm.recommend.0.0 |
主动调用H7
找到tv.danmaku.biliplayerimpl.report.heartbeat.d
类的一个实例,然后主动调用H7方法,固定输出结果
1 | var ByteString = Java.use("com.android.okhttp.okio.ByteString"); |
t3.a.i.a.a.a.b.e.b
hook b
1 | function call_h7(){ |
hook 结果
1 | b.b is called: params=aid=113886342676927&auto_play=0&build=6240300&cid=28065268176&did=PF9tVWMHYgNiUTQGegZ6&epid=&from_spmid=tm.recommend.0.0&ftime=1737960450&lv=0&mid=0&mobi_app=android&part=1&sid=0&spmid=main.ugc-video-detail.0.0&stime=1737979363&sub_type=0&type=3 |
查看t3.a.i.a.a.a.b.e.b
的源码发现,结果来源于com.bilibili.commons.m.a.g
函数
它的两个参数一个是params
,另一个是d
,可以通过hook得到它的值
其实在这个页面往上翻一下,可以找到对d
的赋值
d
是一个定值"9cafa6466a028bfb"
com.bilibili.commons.m.a.g
这里还使用了一个SHA256哈希算法
hook g
1 | function call_h7(){ |
hook 结果
1 | a.g is called: bArr=97,105,100,61,49,49,51,56,56,54,51,52,50,54,55,54,57,50,55,38,97,117,116,111,95,112,108,97,121,61,48,38,98,117,105,108,100,61,54,50,52,48,51,48,48,38,99,105,100,61,50,56,48,54,53,50,54,56,49,55,54,38,100,105,100,61,80,70,57,116,86,87,77,72,89,103,78,105,85,84,81,71,101,103,90,54,38,101,112,105,100,61,38,102,114,111,109,95,115,112,109,105,100,61,116,109,46,114,101,99,111,109,109,101,110,100,46,48,46,48,38,102,116,105,109,101,61,49,55,51,55,57,54,48,52,53,48,38,108,118,61,48,38,109,105,100,61,48,38,109,111,98,105,95,97,112,112,61,97,110,100,114,111,105,100,38,112,97,114,116,61,49,38,115,105,100,61,48,38,115,112,109,105,100,61,109,97,105,110,46,117,103,99,45,118,105,100,101,111,45,100,101,116,97,105,108,46,48,46,48,38,115,116,105,109,101,61,49,55,51,55,57,55,57,51,54,51,38,115,117,98,95,116,121,112,101,61,48,38,116,121,112,101,61,51, bArr2=57,99,97,102,97,54,52,54,54,97,48,50,56,98,102,98 |
签名之后传递给t3.a.i.a.a.a.b.e.a()
方法
t3.a.i.a.a.a.b.e.a
这里使用了aes加密
hook a
1 | var ByteString = Java.use("com.android.okhttp.okio.ByteString"); |
hook 结果
1 | b.a is called: body=aid=113886342676927&auto_play=0&build=6240300&cid=28065268176&did=PF9tVWMHYgNiUTQGegZ6&epid=&from_spmid=tm.recommend.0.0&ftime=1737960450&lv=0&mid=0&mobi_app=android&part=1&sid=0&spmid=main.ugc-video-detail.0.0&stime=1737979363&sub_type=0&type=3&sign=2e012b190fa7060fba2941e72bd14646d0bb2d5445838e2812f95dd1807b6208 |
查看源码,可在源码中,找到AES加密的Key和IV向量
1 | Key: "fd6b639dbcff0c2a1b03b389ec763c4b" |
解密请求体
1 | from Crypto.Cipher import AES |
Python实现请求体加密
请求体参数还原
1 | { |
这里面需要逆向的参数有aid
、build
、cid
、did
和sign
sign还原
sign
值我们已经分析过了,所以先把它还原一下
1 | import hashlib |
aid还原
分析源码,发现aid
参数可以从Video
类的内部类h
类中的a()
方法获取,hook一下
1 | function hook_aid(){ |
hook结果,点击不同的视频它的结果是不一样的
1 | h.a is called |
猜测aid可能与视频有关
build还原
在源码里面翻了翻,发现它是个定值6240300
cid还原
浏览源码,发现cid
参数可以从Video
类的内部类h
类中的b()
方法获取,hook一下
1 | function hook_cid(){ |
hook结果,与aid
参数类似的是,不同的视频,cid
参数的值也是不一样的
1 | h.b is called |
did还原
did
参数是通过com.bilibili.lib.biliid.utils.f.a.c()
方法得到的
hook 一下c方法
1 | function hook_did(){ |
hook 结果,点击不同的视频,结果都是一样的
1 | a.c is called: context=BiliApplication(tv.danmaku.bili)@bd968dc |
分析其如何生成的,去看c方法的源码
c
1 | 首先判断f13193c字段是否为空,非空直接返回 |
看g方法源码
g
通过f方法返回一个f字段,然后对字段f进行处理然后返回
看f方法源码
f
先看j2
字段,它通过j(context)
获取到
j
在这里面调用了一个c方法
c
它是获取mac地址的,这个东西我们也可以自己生成
接着去看f
函数,有一个a2
参数,它获取的是persist.service.bdroid.bdaddr
,也就是蓝牙地址
在f
函数里找到h
参数,h
来源于h()
方法
h
h里面调用了一个a()
方法
它是/sys/bus/mmc/devices
文件的内容,与设备总线信息有关
f函数里还有一个参数i
,通过i()
方法获得
i
i里面调用了a()
方法
这里面提到了/sys/class/android_usb/android0/iSerial
这个文件,它是Android 设备 USB 序列号的用户空间控制接口,与设备USB序列号有关。
总结
经过分析,在g()
方法里生成的f
字段与4个内容有关,分别是mac地址,蓝牙地址,设备总线和sn号,把他们通过|
拼接起来,拼接起来的字符串传入b()
方法。
b
在这里面对字符串进行处理然后通过Base64编码后返回
参数还原
接下来我们自己生成四个内容拼接起来然后通过python实现一个b方法再调用base64进行编码就可以得到did参数。
hook f
hook一下f方法
1 | import frida |
hook 结果,如下只获取到了mac地址
1 | a.f is called: context=BiliApplication(tv.danmaku.bili)@fa97dd3 |
对于Mac地址参数,源码里还对它进行了处理,如下,把非0-9A-Fa-f
的内容替换为空,并都转换为小写。
1 | String lowerCase = j2.replaceAll("[^0-9A-Fa-f]", "").toLowerCase(); |
源码里对第二个参数蓝牙地址也是这样处理,把非0-9A-Fa-f
的内容替换为空,并都转换为小写。
从源码里还可以看出的是,只要f()
方法返回值的长度大于等于4,那么它就有效,所以这四个参数只要获取到了其中一个,就可以拿过来使用。
Python生成mac地址
1 | import random |
Python实现b方法
1 | import random |
至此,did参数还原完成
Python实现请求体加密
1 | import random |
请求头参数分析
1 | { |
这里面需要还原的参数有buvid
、device-id
、fp_local
、fp_remote
、session_id
buvid参数还原
通过抓包找到请求接口,反编译apk,定位接口
请求头信息一般会在拦截器当中
进入拦截器的类进行分析
c
查看e()
方法
e
这里就看到了请求头的参数
先去分析Buvid
,通过a3赋值,a3是从com.bilibili.api.c.a()
这个方法里获得到的
a
从这里面看出a是通过str
赋值的,str
参数是b
方法里传进去的参数,对b方法查找用例
e
查找用例找到了e方法
而这里面参数str
来源于e方法,继续查找用例
a
查找用例找到了a方法
这里面str
通过this.a
赋值,接着找谁给this.a
赋值的
d
找到了一个d
方法,这里面对this.a
进行了赋值
其实还有一种方式,通过打印调用栈信息也可以追溯到参数被赋值的位置
1 | function hook_b(){ |
hook 结果
1 | [Pixel 3::tv.danmaku.bili ]-> java.lang.Throwable |
找到了this.a
之后,发现有三种赋值的情况,第一种情况MiscHelperKt.d(e.k().b())
是从XML
文件和内存中取buvid
的值,如果都取不到,会到第二种方式MiscHelperKt.d(e.k().c())
也是从XML
文件和内存中取buvid
的值,如果都取不到,会到第三种方式c2.f.b0.c.b.a.c.a().toUpperCase()
,说明第三种方式才是生成buvid
的方式
c2.f.b0.c.b.a.c.a()
首先调用Application f = BiliContext.f()
,返回一个Application
类型的字段f
可以看到返回的是当前的ActivityThread.currentApplication()
在这里有四种返回值的情况
先看第一种,return "XZ" + e(d) + d;
,它与e()
方法和d有关,d通过com.bilibili.commons.m.a.d(b2);
获得
找b2
,它通过com.bilibili.lib.biliid.utils.f.c.b(f)
获得
com.bilibili.lib.biliid.utils.f.c.b
返回j2
,它通过g.b(context)
生成
g.b
这里可以看出它是与设备状态有关
最后的返回值传入了com.bilibili.commons.m.a.d
方法
com.bilibili.commons.m.a.d
这里把传进去的参数转为字节,然后调用了e()
方法
这里是把传进去的参数再放到h
方法里进行了MD5
哈希算法
还没完,哈希算法之后对结果又进行了处理。
生成的结果要传入e(d)
这个方法里
e
它是取字符串的第2位,第12位,第22位拼接起来了
最后把XZ
和e(d)
和d
拼接起来就是buvid
了
接下来看第二种情况
com.bilibili.lib.biliid.utils.f.a.j
它调用com.bilibili.droid.g.c(context)
来生成返回值
它与mac地址有关
后面调用的方法与第一种情况相同
接下来看第三种情况
com.bilibili.lib.biliid.utils.f.c.a
这里面调用g.a(context)
来生成返回值
它返回的是android_id
也就是如下的a2
如果a2
不为空,会调用com.bilibili.commons.m.a.d
,然后返回"XX" + e(d4) + d4
如果a2
是空的话,会调用e.k().i().replace(com.bilibili.base.util.d.f, "")
,也就是调用i()
方法,返回通过replace
对字符进行替换
e.k().i()
方法如下
它其实就是生成了随机的uuid
然后字符替换把-
设置为空
然后return "XW" + e(replace) + replace
这样看的话,a2
为空的这种情况还原buvid
是最简单的
Python还原buvid
1 | import uuid |
session_id参数还原
如下,session_id
的值是字段m2
,它是com.bilibili.api.a.m()
方法的返回值
去分析m方法
com.bilibili.api.a.m()
这里通过b
调用了gerSessionId()
返回的session_id
看了一下getSessionId
方法,发现它是一个接口方法,要找其实现类
那就去看b是谁,找到一个o方法
这个方法接受一个 b
类型的参数 bVar
,并将其赋值给 b
静态变量,通过调用 o()
方法来设置 b
变量为一个实现了接口 b
的对象实例。
接下来可以通过hook o方法查看传入的参数bVar是什么类型
1 | function hook_o(){ |
hook 结果
1 | a.o is called: bVar="<instance: com.bilibili.api.a$b, $className: tv.danmaku.bili.utils.p$a>" |
去找tv.danmaku.bili.utils.p$a
tv.danmaku.bili.utils.p.a
在这个类里面找到了gerSessionId()
的实现
这里是通过com.bilibili.lib.foundation.e.b()
来调用的gerSessionId()
看b
方法的内容
它又调用了d.g.b().d()
看d
方法的内容
它返回的是this.a
去找谁给this.a
赋值的
this.a
是DefaultApps
类型
在DefaultApps
里面就会有对gerSessionId()
的重写,找到了
gerSessionId()
调用getString()
方法返回一个字符串,e()
返回的是SharedPreferences
类型, getString()
从保存的数据中找键为foundation:session_id
的值,如果没有找到,返回l
。
接下来去找l
是怎么赋值的,如下
这段代码生成了一个由4个随机字节组成的字节数组,并将这些字节转换为一个十六进制字符串。随后,这个十六进制字符串通过 x.h()
方法进行处理,并赋值给变量 l
。x.h()
用于对hex
进行是否为空的检测。
Python生成session_id
1 | import random |
fp_local参数还原
如下,fp_local
的值为字段j2
,它是通过com.bilibili.api.a.j()
方法获取的
com.bilibili.api.a.j()
这里的F()
方法是接口方法,b是其实现类,它是在一个o
方法里赋值的
找到o方法的上层调用,在tv.danmaku.bili.utils.p.b()
方法里
里面传入的是一个a对象,在a类里有对F()
方法的实现
在F()
方法里返回了一个字段a
,它使用过c2.f.b0.c.a.c.a()
,这个方法获得的
c2.f.b0.c.a.c.a()
a
方法返回的是Fingerprint.h.c()
方法的返回值
看Fingerprint.h.c()
方法
这里是首先定义了一个str
为空,然后把类字段a
赋值给了str
,然后进行返回,所以返回的值其实是a
,接下来找a
是怎么来的
如下,找到了一个地方对a
进行了赋值
通过字段d2
对a
进行赋值,接下来找d2
是怎么生成的
先调用env.d()
方法在内存中找fp_local
,找不到的话就调用d2 = com.bilibili.lib.biliid.internal.fingerprint.a.a.a(buvidLegacy, aVar);
来生成
先分析a()
方法里的两个参数是怎么来的
buvidLegacy
buvidLegacy
通过String buvidLegacy = c3.a();
方法来获取
这个a()
方法与生成buvid
时的a()
方法是一样的,它的返回值就是buvid
aVar
aVar
通过com.bilibili.lib.biliid.internal.fingerprint.b.a aVar = b;
来赋值,
字段b
来源于b = Data.a();
这里我hook了一下a()
方法
1 | function hook_a(){ |
hook 结果
1 | Data.a is called |
这个结果很长,先放在这里,后面分析。
a()
buvidLegacy
和 aVar
这两个参数知道是什么了,接下来再去看a()
方法
这里面调用了一系列函数得到返回值先看MiscHelperKt.a(f(buvidLegacy, data))
里的f(buvidLegacy, data)
f()
这里面是调用aVar.a()
得到a2
这里的a2
就是前面aVar
里的main
1 | main={str_brightness=32, app_version=6.24.0, cpuModel=ARMv8 Processor rev 13 (v8l), speed_sensor=1, adb_enabled=1, screen=1080,2028,440, ui_version=pd1a.180720.030, linear_speed_sensor=1, virtualproc=[], sensors_info=[{"name":"TMD2725 Proximity (wake-up)","vendor":"AMS","version":"1","type":"8","maxRange":"5.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"BMP380 Barometer","vendor":"Bosch","version":"8709","type":"6","maxRange":"1250.0","resolution":"0.1","power":"0.7","minDelay":"40000"}, {"name":"BMP380 Temperature","vendor":"Bosch","version":"8709","type":"33172003","maxRange":"1.0","resolution":"0.1","power":"0.3","minDelay":"200000"}, {"name":"LIS2MDL Magnetometer Uncalibrated","vendor":"STMicro","version":"262","type":"14","maxRange":"4915.2","resolution":"0.1","power":"0.2","minDelay":"10000"}, {"name":"BMI160 Temperature","vendor":"BOSCH","version":"1","type":"33172002","maxRange":"1.0","resolution":"0.1","power":"0.18","minDelay":"200000"}, {"name":"camera v-sync 2","vendor":"google","version":"1","type":"33172005","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"camera v-sync 1","vendor":"google","version":"1","type":"33172005","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"camera v-sync 0","vendor":"google","version":"1","type":"33172005","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"BMI160 Gyroscope Uncalibrated","vendor":"BOSCH","version":"1","type":"16","maxRange":"34.906555","resolution":"0.0010652635","power":"0.9","minDelay":"2500"}, {"name":"BMI160 Gyroscope","vendor":"BOSCH","version":"1","type":"4","maxRange":"34.906555","resolution":"0.0010652635","power":"0.9","minDelay":"2500"}, {"name":"LIS2MDL Temperature","vendor":"STMicro","version":"262","type":"33172004","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"10000"}, {"name":"MAX11261","vendor":"Maxim","version":"1","type":"33172001","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"10000"}, {"name":"BMI160 Accelerometer Uncalibrated","vendor":"BOSCH","version":"1","type":"35","maxRange":"1539.5468","resolution":"4.6983238E-5","power":"0.18","minDelay":"2500"}, {"name":"LIS2MDL Magnetometer","vendor":"STMicro","version":"262","type":"2","maxRange":"4915.2","resolution":"0.1","power":"0.2","minDelay":"10000"}, {"name":"TMD2725 Ambient Light","vendor":"AMS","version":"1","type":"5","maxRange":"1.0","resolution":"0.001","power":"0.001","minDelay":"0"}, {"name":"BMI160 Accelerometer","vendor":"BOSCH","version":"1","type":"1","maxRange":"1539.5468","resolution":"4.6983238E-5","power":"0.18","minDelay":"2500"}, {"name":"Binned Brightness","vendor":"Google","version":"1","type":"65541","maxRange":"255.0","resolution":"1.0","power":"0.2","minDelay":"1000000"}, {"name":"Device Pickup Sensor","vendor":"Google","version":"1","type":"25","maxRange":"1.0","resolution":"1.0","power":"0.25","minDelay":"-1"}, {"name":"Proximity Gated Double Tap Gesture","vendor":"Google","version":"1","type":"65543","maxRange":"1.0","resolution":"0.0","power":"0.001","minDelay":"-1"}, {"name":"Double Twist","vendor":"Google","version":"1","type":"65537","maxRange":"1.0","resolution":"1.0","power":"1.0","minDelay":"0"}, {"name":"Game Rotation Vector Sensor","vendor":"Google","version":"1","type":"15","maxRange":"1.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Geomagnetic Rotation Vector Sensor","vendor":"Google","version":"1","type":"20","maxRange":"1.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Gravity Sensor","vendor":"Google","version":"1","type":"9","maxRange":"9.81","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Linear Acceleration Sensor","vendor":"Google","version":"1","type":"10","maxRange":"156.96","resolution":"1.0E-5","power":"1.0","minDelay":"20000"}, {"name":"Orientation Sensor","vendor":"Google","version":"1","type":"3","maxRange":"360.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Rotation Vector Sensor","vendor":"Google","version":"1","type":"11","maxRange":"1.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Significant Motion","vendor":"Google","version":"1","type":"17","maxRange":"1.0","resolution":"1.0","power":"0.25","minDelay":"-1"}, {"name":"Step Counter","vendor":"Google","version":"1","type":"19","maxRange":"1.8446744E19","resolution":"1.0","power":"0.1","minDelay":"0"}, {"name":"Step Detector","vendor":"Google","version":"1","type":"18","maxRange":"1.0","resolution":"1.0","power":"0.1","minDelay":"0"}, {"name":"Tilt Sensor","vendor":"Google","version":"1","type":"22","maxRange":"1.0","resolution":"0.0","power":"0.25","minDelay":"0"}, {"name":"Device Orientation","vendor":"Google","version":"1","type":"27","maxRange":"3.0","resolution":"1.0","power":"1.0","minDelay":"0"}], app_version_code=6240300, batteryState=BATTERY_STATUS_CHARGING, aaid=, model=Pixel 3, band=g845-00023-180815-B-4956438, wifimac=3C:28:6D:EA:A3:E2, net=["dummy0,fe80::64cc:afff:fe17:88eb%dummy0,66:cc:af:17:88:eb", "r_rmnet_data0,fe80::9615:8f49:b195:c0c2%r_rmnet_data0,", "lo,::1,127.0.0.1,", "wlan0,fe80::3e28:6dff:feea:a3e2%wlan0,192.168.0.112,3c:28:6d:ea:a3:e2", "rmnet_data0,fe80::f39b:f668:3162:3fd1%rmnet_data0,", "rmnet_ipa0,,"], app_id=1, brand=google, cpuCount=8, biometric=1, maps=, btmac=, cpuVendor=Qualcomm, device_angle=-0.011197972,2.7591743,-0.0041870493, str_battery=96, vaid=, build_id=PD1A.180720.030, androidappcnt=260, guid=a05fc3dd-ed19-4d54-b7f7-e0a628a91c2a, files=/data/user/0/tv.danmaku.bili/files, sensor=["TMD2725 Proximity (wake-up),AMS", "BMP380 Barometer,Bosch", "BMP380 Temperature,Bosch", "LIS2MDL Magnetometer Uncalibrated,STMicro", "BMI160 Temperature,BOSCH", "camera v-sync 2,google", "camera v-sync 1,google", "camera v-sync 0,google", "BMI160 Gyroscope Uncalibrated,BOSCH", "BMI160 Gyroscope,BOSCH", "LIS2MDL Temperature,STMicro", "MAX11261,Maxim", "BMI160 Accelerometer Uncalibrated,BOSCH", "LIS2MDL Magnetometer,STMicro", "TMD2725 Ambient Light,AMS", "BMI160 Accelerometer,BOSCH", "Binned Brightness,Google", "Device Pickup Sensor,Google", "Proximity Gated Double Tap Gesture,Google", "Double Twist,Google", "Game Rotation Vector Sensor,Google", "Geomagnetic Rotation Vector Sensor,Google", "Gravity Sensor,Google", "Linear Acceleration Sensor,Google", "Orientation Sensor,Google", "Rotation Vector Sensor,Google", "Significant Motion,Google", "Step Counter,Google", "Step Detector,Google", "Tilt Sensor,Google", "Device Orientation,Google"], gadid=, fstorage=33928413184, virtual=0, memory=3723137024, mid=, oid=, emu=000, is_root=true, battery=96, mac=3C:28:6D:EA:A3:E2, network=WIFI, uid=10243, data_connect_state=0, glimit=, adid=5abb19ceea116b31, mem=3723137024, countryIso=CN, sim=1, root=1, sdkver=0.2.4, light_intensity=4.1807923, boot=976254663, str_app_id=1, oaid=, apps=["1230796800000,com.google.omadm.trigger,1,1.0,1,1230796800000","1230796800000,com.google.android.carriersetup,1,9,28,1230796800000","1230796800000,com.android.cts.priv.ctsshim,1,8.1.0-4396705,27,1230796800000","1230796800000,com.google.android.youtube,1,20.02.40,1552287168,1738224905212","1230796800000,com.vzw.apnlib,1,13.0,14,1230796800000","1230796800000,com.android.internal.display.cutout.emulation.corner,1,1.0,1,1230796800000","1230796800000,com.google.android.ext.services,1,1,1,1230796800000","1729696867893,com.example.hellojnitest,0,1.0,1,1729737782103","1230796800000,com.android.internal.display.cutout.emulation.double,1,1.0,1,1230796800000","1230796800000,com.android.providers.telephony,1,9,28,1230796800000","1230796800000,com.android.sdm.plugins.connmo,1,1.0,1,1230796800000","1230796800000,com.google.android.googlequicksearchbox,1,16.2.40.ve.arm64,301459242,1738224848635","1230796800000,com.android.providers.calendar,1,9,28,1230796800000","1711181586493,net.typeblog.socks,0,1.0.4,13,1711181586493","1230796800000,com.android.providers.media,1,9,900,1230796800000","1711199319522,com.qiyi.video,0,11.5.0,800110550,1711199319522","1230796800000,com.google.android.onetimeinitializer,1,9,28,1230796800000","1230796800000,com.google.android.ext.shared,1,1,1,1230796800000","1230796800000,com.qualcomm.ltebc_vzw,1,MSDC_LA_4.2.01.11.0,42010110,1230796800000","1230796800000,com.quicinc.cne.CNEService,1,1.1,1,1230796800000"], androidsysapp20=["1230796800000,com.google.omadm.trigger,1,1.0,1,1230796800000","1230796800000,com.google.android.carriersetup,1,9,28,1230796800000","1230796800000,com.android.cts.priv.ctsshim,1,8.1.0-4396705,27,1230796800000","1230796800000,com.google.android.youtube,1,20.02.40,1552287168,1738224905212","1230796800000,com.vzw.apnlib,1,13.0,14,1230796800000","1230796800000,com.android.internal.display.cutout.emulation.corner,1,1.0,1,1230796800000","1230796800000,com.google.android.ext.services,1,1,1,1230796800000","1230796800000,com.android.internal.display.cutout.emulation.double,1,1.0,1,1230796800000","1230796800000,com.android.providers.telephony,1,9,28,1230796800000","1230796800000,com.android.sdm.plugins.connmo,1,1.0,1,1230796800000","1230796800000,com.google.android.googlequicksearchbox,1,16.2.40.ve.arm64,301459242,1738224848635","1230796800000,com.android.providers.calendar,1,9,28,1230796800000","1230796800000,com.android.providers.media,1,9,900,1230796800000","1230796800000,com.google.android.onetimeinitializer,1,9,28,1230796800000","1230796800000,com.google.android.ext.shared,1,1,1,1230796800000","1230796800000,com.qualcomm.ltebc_vzw,1,MSDC_LA_4.2.01.11.0,42010110,1230796800000","1230796800000,com.quicinc.cne.CNEService,1,1.1,1,1230796800000","1230796800000,com.qualcomm.qti.smcinvokepkgmgr,1,1.0,1,1230796800000","1230796800000,com.android.documentsui,1,9,28,1230796800000","1230796800000,android.auto_generated_rro__,1,1.0,1,1230796800000"], proc=tv.danmaku.bili, fts=1738507220, os=android, languages=zh, systemvolume=0, free_memory=1727660032, totalSpace=55379472384, accessibility_service=["com.google.android.marvin.talkback/.TalkBackService", "com.google.android.marvin.talkback/com.google.android.accessibility.accessibilitymenu.AccessibilityMenuService", "com.google.android.marvin.talkback/com.google.android.accessibility.selecttospeak.SelectToSpeakService", "com.google.android.marvin.talkback/com.android.switchaccess.SwitchAccessService", "bin.mt.plus/bin.mt.function.ar.ActivityRecordService", "com.estrongs.android.pop/com.estrongs.fs.impl.local.AutoAuthService", "com.qihoo.appstore/.accessibility.AppstoreAccessibility", "com.wandoujia.phoenix2/com.pp.assistant.accessibility.AccessibilityService", "com.xingin.xhs/com.xingin.alpha.common.access.GameRecordAccessibilityService"], osver=9, chid=alifenfa, androidapp20=["1729696867893,com.example.hellojnitest,0,1.0,1,1729737782103","1711181586493,net.typeblog.socks,0,1.0.4,13,1711181586493","1711199319522,com.qiyi.video,0,11.5.0,800110550,1711199319522","1728583109623,com.zx.Justmeplush,0,2.0,2,1728583109623","1737391958923,com.douban.frodo,0,7.89.0,307,1737391958923","1730733792580,com.github.kr328.clash,0,3.0.3.premium,300003,1730733792580","1730769260488,com.example.packerapp,0,1.0,1,1730799296361","1725493032640,com.example.javahookdemo,0,1.0,1,1725974937739","1737130307244,com.ushaqi.zhuishushenqi,0,4.85.73,7973,1737130307244","1732863178328,com.yaotong.crackme,0,1.0,1,1732863178328","1730294059865,com.example.unidbgtest,0,1.0,1,1730295569462","1728375218269,com.izen.abc,0,1.0.9,1,1728375218269","1729958664139,com.example.aestool,0,1.0,1,1729958783366","1730368506985,com.example.develop_test,0,1.0,1,1730373040462","1723908516582,com.example.activitytest,0,1.0,1,1723909442352","1732874628219,com.xingin.xhs,0,8.57.0,8570985,1732961699211","1738373185269,com.zj.wuaipojie2025,0,1.0,1,1738373185269","1726495470725,com.example.plugindex,0,1.0,1,1726495470725","1732961995889,com.netease.community,0,1.2,67,1732961995889","1732244828849,vz.bwu,0,1.0,1,1732244828849"], biometrics=touchid, last_dump_ts=1738576816411, brightness=32, gyroscope_sensor=1, t=1738586160751, kernel_version=4.9.96-g340a9aaf92bc-ab4959234, usb_connected=1, cpuFreq=1766400, gps_sensor=1, data_activity_state=0, axposed=false, first=false} |
返回值调用了一个e()
方法e(str + a2.get(PersistEnv.KEY_PUB_MODEL) + a2.get("band"))
,这里面的str
是buvid
,PersistEnv.KEY_PUB_MODEL
的值为model
,即手机型号,band
是设备编号标识,他们拼接在一起作为e()
方法的参数
e()
它是把拼接的字符串进行了MD5哈希算法
这个可以Python实现
1 | import uuid |
MiscHelperKt.a()
接下来分析MiscHelperKt.a(f(buvidLegacy, data))
通过调用Oe
方法返回字段Oe
1 | public static String Oe(byte[] bArr, CharSequence charSequence, CharSequence charSequence2, CharSequence charSequence3, int i2, CharSequence charSequence4, kotlin.jvm.c.l lVar, int i3, Object obj) { |
这里面调用了Fe()
方法
1 | public static final String Fe(byte[] joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int i2, CharSequence truncated, kotlin.jvm.c.l<? super Byte, ? extends CharSequence> lVar) { |
Fe
方法返回一个字段sb
,sb
通过ne()
方法获得
1 | public static final <A extends Appendable> A ne(byte[] joinTo, A buffer, CharSequence separator, CharSequence prefix, CharSequence postfix, int i2, CharSequence truncated, kotlin.jvm.c.l<? super Byte, ? extends CharSequence> lVar) { |
接下来先对ne()
方法里涉及到的参数查找,往前推找到
joinTo
为前面的MD5哈希字节数组prefix
的值为""
separator
为:
postfix
为""
i2
为-1truncated
为...
lVar
为匿名内部类,其invoke
方法是对字节型参数进行十六进制格式化,返回其十六进制字符串。
用Python复现
1 | def MiscHelperKt_a(joinTo): |
h()
接下来分析h()
方法
返回一个表示当前时间和日期的 Date
对象,精确到毫秒,转为字符串类型。
format
是一个表示当前时间和日期的 Date
对象的格式化字符串。
可以通过hook h()查看效果
1 | function call_h(){ |
g()
接下来分析g()
方法
它返回的是字段a2
,a2
通过com.bilibili.commons.e.a(8)
方法获得
new了一个8字节的数组,然后随机生成8字节的值,然后返回
Python实现
1 | import random |
g()
生成的结果也是作为参数传入了MiscHelperKt.a()
前面这些得到的结果拼接成字符串得到字段str
b()
str
作为参数传递给了b()
方法
这里面先通过q.n1(0, Math.min(fpEntity.length() - 1, 62))
方法得到n1
,然后调用q.S0(n1, 2)
方法得到S0
,通过S0
获取g
、h
、i2
的值,这里面可以发现,传入n1
方法的参数fpEntity.length()
是定值,那么后面的g
、h
、i2
的值也是定值
可以通过hook b
方法,然后主动调用b
方法打印出g
、h
、i2
的值来查看
hook b
1 | function hook_b(){ |
hook_g_h_i
1 | function hook_g_h_i(){ |
call b
1 | function call_b(){ |
hook 结果
1 | i.g is called |
得出g,h,i2
的值分别为0,60,2
使用Python还原
1 | def g_b(fpEntity): |
Python还原fp_local参数
结合上面分析的参数生成过程,整合生成fp_local参数
1 | import uuid |
fp_remote参数还原
hook生成fp_local
和fp_remote
的两个方法,发现他们的值相似
1 | function hook_fp(){ |
hook 结果
1 | a.j is called |
暂时使用生成fp_local的方法来生成fp_remote
整合代码
1 | import requests |
播放量接口抓包分析
在app里播放量请求要发送两次,一次是在视频开始的时候发送,一次是在视频结束的时候发送
1 | url:https://api.bilibili.com/x/report/heartbeat/mobile |
请求头参数已经分析过了
下面只需要看请求体参数
需要分析的参数有session
、sign
session参数分析还原
jadx反编译apk搜索report/heartbeat/mobile
,双击跳转到接口
找到reportV2
查找用例,找到如下两个进行分析
reportV2()
方法转入了一个参数N7,然后去找N7是如何生成的
N7()
N7参数通过调用N7()
方法返回,如下是N7()
源码
1 | private final HeartbeatParams N7(h hVar, boolean z) { |
它返回的是new HeartbeatParams()
,然后去看HeartbeatParams
的构造方法,它里面的参数就是请求体参数
HeartbeatParams()
传给session的参数是str,接下来去找str是如何初始化的
回到N7方法找它的第二个参数
第二个参数是通过hVar.r1()
方法返回的
r1()
r1()
方法返回的是this.d
,找谁给this.d
赋值,如下,通过t2()
方法传进去的参数str
给this.d
赋值
t2()
对t2()
查找用例,找到一个b()
方法,它里面的参数是g.a.a()
的返回值
b()
查看g.a.a()
,如下
a()
这里面new了一个StringBuilder
,然后拼接E.t()
、系统时间戳和一个1000000内的随机整数,然后对拼接的字符串进行SHA1
哈希算法
首先看E.t()
,它获取的是buvid
t()
python生成sb2
1 | import time |
生成的参数sb2
方法传递给了com.bilibili.commons.m.a.i(sb2)
查看i()
方法
i()
在i()
方法里调用了j()
方法
查看j()
方法
j()
在j()
方法里把参数和SHA1
对象传递给了h()
方法
查看h()
方法
h()
这里面对传入的参数进行了SHA1
哈希算法
接下来通过hook i()方法来验证这里h()
里面是不是标准的SHA1()
1 | function hook_i(){ |
hook 结果
1 | a.i is called: str=1738746513112310995 |
如下证明h()
方法是标准版的SHA1
算法
Python还原session参数
整合上面的代码
1 | import hashlib |
sign参数分析还原
首先像找session
参数一样在源码中找是没有找到的,可能是根据已有的参数进行加密后拼接到原来的参数后面的。
分析sign
的值,其长度是16字节,猜测其是MD5加密生成的,把请求体中除了sign
值的其他参数拼接起来进行MD5哈希算法,发现结果不一致,可能有加盐。换思路分析。
猜测:可能在拦截器中进行的加密,也可能在so文件中进行的加密。同时在拼接sign
参数的时候有很大可能会出现&sign=
字样。
在jadx里搜索查找&sign=
经过测试验证得到生成sign值的类为如下类
双击进去到了toString()
方法里
hook toString()
1 | function hook_toString(){ |
hook 结果
根据参数找到对应的hook结果
1 | SignedQuery.toString is called |
通过调用堆栈找到上一层函数com.bilibili.okretro.f.a.c
这里看到在h(hashMap)
里传入了一个参数hashMap
,通过hook打印hashMap
的值
1 | function printMap(map){ |
hook 结果
1 | key: platform | value: android |
加密过程应该就在h()
方法里
在h()
方法里调用了g()
方法
在g()
方法里调用了s()
方法
s()
方法是一个native方法
hook s
1 | function hook_s(){ |
hook 结果
1 | LibBili.s is called: sortedMap=[object Object] |
so的名字是
通过hook_NewStringUTF定位
如果sign
是在so中加密的,那么在返回字符串的时候一定会调用newStringUTF
方法返回字符串
可以通过hook_NewStringUTF来打印相关信息
1 | function hook_NewStringUTF(){ |
在hook结果中找到相关信息,如下,根据抓包中得到的sign在hook结果中搜索
1 | dataString: d7472d2094ecc6fb5796cedaa92a82a7 |
hook结果中显示了入参,so名字和函数地址
so分析
用IDA打开so,在静态注册函数中没有找到s()
函数
动态注册查找
hook registernatives
1 | function hook_RegisterNatives() { |
hook 结果
1 | ClasssName: com.bilibili.nativelibrary.LibBili MethodName: a Sig: (Ljava/lang/String;)Ljava/lang/String; Function_addr: 0xbc4b3c7d ModuleName: libbili.so Fun_Offset: 0x1c7d |
s()
函数在偏移为0x1c97
处
使用IDA跳转至该地址处进行分析
首先看s()
方法的返回值,它返回的是一个对象,里面有两个参数是v13
和v14
我们回过头去看jadx里的s()
方法,它传入一个sortedMap
参数,返回一个SignedQuery
对象,返回SignedQuery
对象需要调用SignedQuery
类的构造方法
我们再去看SignedQuery
类的构造方法
这里面传入了两个参数,从toString()
方法里可以看到其中this.b
就是sign
值,也就是IDA里看到的返回值中的参数v14
接下来找v14
怎么生成的
这里的反编译代码不好读,剩下只能靠猜了
查看sub_227C()
函数,这个函数进行的数据初始化,看到这四个值很熟悉,是MD5的初始化数据
在分析Java层的时候已经猜出来了它并不是标准的MD5,可能加盐了
那么sub_22B0()
函数可能就是加盐的函数
尝试对它进行hook
hook_22B0()
1 | function hook_22B0(){ |
hook 结果
1 | arg1: actual_played_time=1&aid=113945784358610&appkey=1d8b6e7d45233436&auto_play=0&build=6240300&c_locale=zh-Hans_CN&channel=alifenfa&cid=28214757335&epid=0&epid_status=&from=7&from_spmid=tm.recommend.0.0&last_play_progress_time=1&list_play_time=0&max_play_progress_time=1&mid=0&miniplayer_play_time=0&mobi_app=android&network_type=1&paused_time=2&platform=android&play_status=0&play_type=1&played_time=1&quality=32&s_locale=zh-Hans_CN&session=22b3446fdb1b406d22b87ba3d09bbeb0bb170e21&sid=0&spmid=main.ugc-video-detail.0.0&start_ts=1738823412&statistics=%7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%226.24.0%22%2C%22abtest%22%3A%22%22%7D&sub_type=0&total_time=3&ts=1738823572&type=3&user_status=0&video_duration=284 |
主动调用s()
方法,并对so方法进行hook
1 | function call_s(){ |
hook结果
1 | call_s() |
分析
在主动调用的时候,在so里面会拼接一个ts
参数,当前时间戳,而且如果map长度太短不会生成sign
经过测试MD5加了四次盐,加的盐是560c52cc
、d288fed0
、45859ed1
、8bffd973
在s()
方法里做的事情就是给map拼接ts
参数,然后加盐,进行MD5哈希算法,如下
Python实现
代码整合
1 | import requests |
抓包数据
1 | url:"https://api.bilibili.com/x/report/click/android2" |
1 | url:"https://api.bilibili.com/x/report/heartbeat/mobile" |
1 | url:"https://api.bilibili.com/x/report/heartbeat/mobile" |
unidbg
模拟执行s方法
so位置libbili.so
分析
s方法的调用位置,如下,被g()
方法调用
1 | public static SignedQuery g(Map<String, String> map) { |
返回一个SignedQuery
对象
1 | public SignedQuery(String str, String str2) { |
我们的目标是得到SignedQuery
里的this.b
字段
在补环境的过程中,我们一定会补SignedQuery
,在补它的时候传入这两个参数,而我们可以在这里直接拿到这两个参数,就不需要继续补环境了。
1 | public class LibBili extends AbstractJni { |
补环境
补isEmpty()
1 | java.lang.UnsupportedOperationException: java/util/Map->isEmpty()Z |
补
1 | case "java/util/Map->isEmpty()Z":{ |
运行,继续补环境
补get(Ljava/lang/Object;)
1 | java.lang.UnsupportedOperationException: java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object; |
补
1 | case "java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object;":{ |
这里打印出来的key
1 | appkey |
运行,继续补环境
补put(Ljava/lang/Object;Ljava/lang/Object;)
1 | java.lang.UnsupportedOperationException: java/util/Map->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; |
补
1 | case "java/util/Map->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;":{ |
运行,继续补环境
补r(Ljava/util/Map;)
1 | java.lang.UnsupportedOperationException: com/bilibili/nativelibrary/SignedQuery->r(Ljava/util/Map;)Ljava/lang/String; |
这里补的是SignedQuery类的r方法,但是Unidbg里没有SignedQuery类,这个类需要自己实现,到jadx里copy过来
然后进行更正,更正完再补
1 | case "com/bilibili/nativelibrary/SignedQuery->r(Ljava/util/Map;)Ljava/lang/String;":{ |
补<init>
1 | java.lang.UnsupportedOperationException: com/bilibili/nativelibrary/SignedQuery-><init>(Ljava/lang/String;Ljava/lang/String;)V |
补
1 | case "com/bilibili/nativelibrary/SignedQuery-><init>(Ljava/lang/String;Ljava/lang/String;)V":{ |
over
运行就出结果了。
打包Python调用
修改,可传入参数
1 | package com.tv.danmaku.bili; |
1 | import subprocess |
1 | cd3eb3e0e90c07304828f1a2c9581d06 |