这篇文章上次修改于 1365 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
title: 哔哩哔哩APP视频和字幕接口探索笔记
date: 2020/10/01 16:28:46
updated: 2020/10/04 17:59:12
permalink: explore-bilibili-grpc-service/
toc: true
注意:本文图片较多,以及部分逻辑混乱
起因
有一个番剧(我開動物園那些年)只能在APP看,网页只有移动端能看几分钟的预览,于是抓包之。
记录
然后找到了请求接口如下:
然后还有一个字幕,请求是另一个,没有和视频一起返回,请求接口如下:
接口信息小结
视频api
https://grpc.biliapi.net/bilibili.pgc.gateway.player.v1.PlayURL/PlayView
user-agent: Dalvik/2.1.0 (Linux; U; Android 9; ***) 6.7.0 os/android model/*** mobi_app/android build/6070600 channel/bilibili140 innerVer/6070600 osVer/9 network/2 grpc-java-cronet/1.21.0
content-type: application/grpc
te: trailers
x-bili-fawkes-req-bin: ***
x-bili-metadata-bin: ***
authorization: identify_v1 ***
x-bili-device-bin: ***
x-bili-network-bin: ***
x-bili-restriction-bin:
x-bili-locale-bin: ***
grpc-encoding: gzip
grpc-accept-encoding: identity,gzip
grpc-timeout: ***
字幕api
https://app.bilibili.com/bilibili.community.service.dm.v1.DM/DmView
env: prod
app-key: android
user-agent: Dalvik/2.1.0 (Linux; U; Android 9; ***) 6.7.0 os/android model/*** mobi_app/android build/6070600 channel/bilibili140 innerVer/6070600 osVer/9 network/2
x-bili-metadata-bin: ***
authorization: identify_v1 ***
x-bili-device-bin: ***
x-bili-network-bin: ***
x-bili-restriction-bin:
x-bili-locale-bin: ***
x-bili-fawkes-req-bin: ***
content-type: application/grpc
accept-encoding: gzip
cookie: bfe_id=***; sid=***
可见请求头中有一些关键参数,同时payload里面也不再是明文的了,不过从请求接口上看应该是grpc编码过
(后续指正:是protobuf编码后进行了base64编码)
- x-bili-fawkes-req-bin
- x-bili-metadata-bin
- authorization
- x-bili-device-bin
- x-bili-network-bin
- x-bili-restriction-bin
- x-bili-locale-bin
探索请求头
字符串搜索虽然看起来不那么高大上,不过能搜到倒是能节省很多时间。
用jadx-gui搜索字符串x-bili-fawkes-req-bin
,一下就定位到了:
然后直接用objection搜了一下,发现直接出来了
首先验证一下返回的是什么
如果对请求头x-bili-fawkes-req-bin
的value进行base64解码,可以看到和objection的结果对的上
对应前面的结果,推测是通过com.google.protobuf.GeneratedMessageLite
做了编码处理
以此类推,得到一张关于header在protobuf编码前的对应关系:
header | values |
---|---|
x-bili-fawkes-req-bin | android, prod |
x-bili-metadata-bin | identify_v1, MobiApp, Device, Build, Channel, Buvid, Platform |
authorization | identify_v1 |
x-bili-device-bin | AppId, Build, Buvid, MobiApp, Platform, Device, Channel, Brand, Model, Osver |
x-bili-network-bin | Type, Tf, Oid |
x-bili-restriction-bin | |
x-bili-locale-bin | Locale |
一些对应关系:
key | value | description |
---|---|---|
AccessKey | identify_v1 | identify_v1 指代authorization里面的参数 |
MobiApp | android | APP标识 |
Device | (none)? | 可能是空 |
Build | 6070600 | Build 是APP版本号 |
Channel | bilibili140 | 应该是固定值 |
Buvid | XY* | Buvid 是一个XY开头+35位16进制的字符串 |
Platform | android | 系统平台 |
AppId | 1 | 可能是API版本 |
Brand | Xiaomi | 手机厂商 |
Model | Mi 9se | 手机型号 |
Osver | 10 | 系统版本 |
Type | 1 | 网络类型 1是WIFI |
Tf | TF_UNKNOWN | ? |
Oid | 46007 | ? |
Locale | zh, CN zh, CN | 推测是当前APP(current)语言 和系统(system)语言 |
一些相关的类:
- b.eds
- b.edr
- b.efd
- com.bapis.bilibili.metadata.Metadata
- com.bilibili.lib.moss.internal.impl.common.header.a
- com.bilibili.lib.moss.internal.impl.common.header.HeadersKt$reqDevice$2
header部分除了protobuf编码部分,基本清楚了
探索payload
既然都已经找到请求头了,那就在这里看一下调用来源
。。。一时没找到,还是从bilibili.pgc.gateway.player.v1.PlayURL
下手
watch一下,可以看到最后有两个io.grpc.MethodDescriptor.a(java.io.InputStream)
后面又有一个InputStream
,根据抓包来看,依次对应下面三个请求:
- https://grpc.biliapi.net/bilibili.pgc.gateway.player.v1.PlayURL/PlayView
- https://app.bilibili.com/bilibili.community.service.dm.v1.DM/DmView
- https://broadcast.chat.bilibili.com/bilibili.broadcast.v1.BroadcastTunnel/CreateTunnel
不出意外的话,InputStream
相关内容就是payload了
然后看下这个方法的调用栈,可以确定payload从这几步中产生
挨个看了下,发现在b.eby.a
能看到未处理的明文参数
那么从这里追踪就能知道payload是怎么转换的了
追踪payload
根据前面的调用栈,依次查看:
com.bilibili.lib.moss.internal.impl.failover.a.a
io.grpc.stub.ClientCalls.a
emmm
突然发现(傻了)b.eby.a
的返回类型是PlayViewReply
,既然是Reply那说明这都是返回数据了吧。。一看果然是,还是序列化好的那种。。
x-bili-*-bin生成
通过base64解码后可以发现实际上是相关参数protobuf编码的结果,就不用细说了
payload生成——PlayView
相关调用栈如下
(agent) [6ep2cowu7tk] Backtrace:
com.bapis.bilibili.pgc.gateway.player.v1.PlayViewReq.writeTo(Native Method)
com.google.protobuf.AbstractMessageLite.writeTo(BL:83)
b.hya.a(BL:52)
io.grpc.internal.ba.a(BL:268)
io.grpc.internal.ba.b(BL:187)
io.grpc.internal.ba.a(BL:138)
io.grpc.internal.d.a(BL:53)
io.grpc.internal.ae.a(BL:37)
io.grpc.internal.o.b(BL:463)
io.grpc.internal.o.a(BL:447)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.stub.ClientCalls.a(BL:284)
io.grpc.stub.ClientCalls.a(BL:191)
io.grpc.stub.ClientCalls.a(BL:129)
com.bilibili.lib.moss.internal.impl.failover.a.a(BL:151)
com.bilibili.lib.moss.api.MossService.blockingUnaryCall(BL:41)
com.bapis.bilibili.pgc.gateway.player.v1.PlayURLMoss.playView(BL:59)
(agent) [29ivcbse6b2] Backtrace:
io.grpc.internal.o.b(Native Method)
io.grpc.internal.o.a(BL:447)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.t.a(BL:37)
io.grpc.stub.ClientCalls.a(BL:284)
io.grpc.stub.ClientCalls.a(BL:191)
io.grpc.stub.ClientCalls.a(BL:129)
com.bilibili.lib.moss.internal.impl.failover.a.a(BL:151)
com.bilibili.lib.moss.api.MossService.blockingUnaryCall(BL:41)
com.bapis.bilibili.pgc.gateway.player.v1.PlayURLMoss.playView(BL:59)
b.eby.a(BL:51)
b.ebo.a(BL:271)
b.ebo.resolveMediaResource(BL:170)
com.bilibili.lib.media.resolver.resolve.a.a(BL:143)
com.bilibili.lib.media.resolver.resolve.MediaResolveProvider.a(BL:151)
com.bilibili.lib.media.resolver.resolve.MediaResolveProvider.call(BL:59)
android.content.ContentProvider$Transport.call(ContentProvider.java:403)
android.content.ContentResolver.call(ContentResolver.java:1772)
com.bilibili.lib.media.resolver.resolve.MediaResolveProvider.a(BL:116)
com.bilibili.lib.media.resolver.resolve.a.a(BL:93)
b.eaz.intercept(BL:36)
b.eav.a(BL:32)
b.eav.call(BL:18)
java.util.concurrent.FutureTask.run(FutureTask.java:266)
b.eas$b.call(BL:136)
java.util.concurrent.FutureTask.run(FutureTask.java:266)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
java.lang.Thread.run(Thread.java:764)
(agent) [v2zkta7cw3h] Called com.google.common.io.BaseEncoding.a([B, int, int)
(agent) [v2zkta7cw3h] Backtrace:
com.google.common.io.BaseEncoding.a(Native Method)
com.google.common.io.BaseEncoding.a(BL:148)
io.grpc.internal.ca.a(BL:64)
b.hxu.a(BL:342)
b.hxu.a(BL:62)
b.hxu$c.a(BL:162)
io.grpc.internal.a.a(BL:163)
io.grpc.internal.ae.a(BL:87)
io.grpc.internal.aq$b$1.a(BL:704)
io.grpc.internal.o.b(BL:287)
io.grpc.internal.o.a(BL:188)
io.grpc.internal.l$e$1.a(BL:394)
io.grpc.internal.k$c$1.a(BL:695)
io.grpc.t.a(BL:32)
b.edc$a.a(BL:45)
io.grpc.t.a(BL:32)
b.edg$a.a(BL:28)
io.grpc.t.a(BL:32)
b.edj$a.a(BL:24)
io.grpc.t.a(BL:32)
b.edf$a.a(BL:34)
io.grpc.t.a(BL:32)
b.ede$a.a(BL:25)
io.grpc.stub.ClientCalls.a(BL:310)
io.grpc.stub.ClientCalls.a(BL:282)
io.grpc.stub.ClientCalls.a(BL:191)
io.grpc.stub.ClientCalls.a(BL:129)
com.bilibili.lib.moss.internal.impl.failover.a.a(BL:151)
com.bilibili.lib.moss.api.MossService.blockingUnaryCall(BL:41)
com.bapis.bilibili.pgc.gateway.player.v1.PlayURLMoss.playView(BL:59)
b.eby.a(BL:51)
b.ebo.a(BL:271)
b.ebo.resolveMediaResource(BL:170)
com.bilibili.lib.media.resolver.resolve.a.a(BL:143)
com.bilibili.lib.media.resolver.resolve.MediaResolveProvider.a(BL:151)
com.bilibili.lib.media.resolver.resolve.MediaResolveProvider.call(BL:59)
android.content.ContentProvider$Transport.call(ContentProvider.java:403)
android.content.ContentResolver.call(ContentResolver.java:1772)
com.bilibili.lib.media.resolver.resolve.MediaResolveProvider.a(BL:116)
com.bilibili.lib.media.resolver.resolve.a.a(BL:93)
b.eaz.intercept(BL:36)
b.eav.a(BL:32)
b.eav.call(BL:18)
java.util.concurrent.FutureTask.run(FutureTask.java:266)
b.eas$b.call(BL:136)
java.util.concurrent.FutureTask.run(FutureTask.java:266)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
java.lang.Thread.run(Thread.java:764)
(agent) [v2zkta7cw3h] Arguments com.google.common.io.BaseEncoding.a([10,8,10,2,122,104,26,2,67,78,18,8,10,2,122,104,26,2,67,78], "(none)", 20)
(agent) [v2zkta7cw3h] Return Value: "CggKAnpoGgJDThIICgJ6aBoCQ04"
private void b(ReqT reqt) {
i.b(this.l != null, (Object) "Not started");
i.b(!this.n, (Object) "call was cancelled");
i.b(!this.o, (Object) "call was half-closed");
try {
if (this.l instanceof bo) {
((bo) this.l).a(reqt);
} else {
this.l.a(this.f8993c.a(reqt));
}
if (!this.i) {
this.l.j();
}
} catch (RuntimeException e2) {
this.l.a(Status.f8827b.b((Throwable) e2).a("Failed to stream message"));
} catch (Error e3) {
this.l.a(Status.f8827b.a("Client sendMessage() failed with Error"));
throw e3;
}
}
- 第一步,将有关参数通过
com.google.protobuf.CodedOutputStream
有关方法写数据到codedOutputStream
注意这里是com.bapis.bilibili.pgc.gateway.player.v1.PlayViewReq.writeTo
方法,hook看下这些值都是什么
初步对比可以看到在AbstractMessageLite.writeTo
这里,没有进行编码
- 第二步,通过调用flush函数将结果写到
outputStream
- 第三步,这里会返回序列化后的大小,当然数据已经在
outputStream
里面了
需要注意的是outputStream
的具体实现方法如下:
也就是数据会被压缩
结合抓包结果,小结如下:
- 将下列参数进行protobuf编码
epId:316825
cid:173415955
qn:112
fnver:0
fnval:16
download:0
forceHost:0
fourk:true
spmid:pgc.pgc-video-detail.0.0
fromSpmid:search.search-result.0.0
teenagersMode:0
preferCodecType:2
isPreview:false
roomId:0
- gzip压缩,计算压缩后的数据长度,添加到头部并补充4位0
- 根据grpc文档,知道一次请求可以包含多条信息,也就是payload第一位是消息编号(?)
payload生成——DmView
方法com.bapis.bilibili.community.service.dm.v1.DMMoss.dmView(BL:64)
的返回是DmViewReply
,参数是DmViewReq
,且通过hook可以确定DmViewReply的内容就是抓包的返回数据。
追踪分析,发现在b.edp.a(BL:78)
这里数据和抓包结果一致。
而在这个函数的上一步b.edp.a(BL:56)
有一个生成byte的过程,查看发现调用了writeTo
方法,具体实现是com.bapis.bilibili.community.service.dm.v1.DmViewReq.writeTo
在这里可以看到数据被写入到codedOutputStream
,具体的写入过程是protobuf编码转换,在此不展开。编码用到的数据hook结果如下:
其中b.edv.a
后续几个调用不太重要,(r4)本质上是统计当前数据长度,写到byte中,填充4位0,最后和(r7)原数据写到结果中
至此字幕请求的payload生成分析就完成了,小结如下:
- 将下列参数进行protobuf编码
pid:497543586
oid:173415955
type:1
spmid:
HardBoot:1
- 计算编码后的数据长度,并填充4位0
- 返回数据需要通过gzip解开再通过protobuf格式化
后记
这些是探索过程的记录,后面会整理一篇更合理、更科学、更高效的分析方案。
其他记录和感想:
要注意:implements、interface、extends,水平有限,参考这篇文章:关键字 extend interface implement,然后记录三句话:
- implements是实现interface定义的类的具体方法功能,可以理解为为这个类附加一些额外的功能
- extends是继承一个抽象类,继承父类的全部功能
- interface是定义了一个比抽象类还要抽象的类,即极度抽象类。如果不通过关键字来实现具体的功能,是没有任何意思的
- 结合objection查看调用栈时,一定要注意传入的参数的具体实现方法,不然很可能花费大量时间寻找下一步的具体调用,在那里瞎找,白白浪费时间
- 对于流式对象及类似的对象,有的只能读取一次
- java基础很重要
- jadx看反编译好的代码时要结合smali具体分析,特别是一些抽象类的对象,在反编译的代码里面不容易看出来
- 通过frida hook的时候,要注意合理选择对应系统架构的server
- 笔记记得有些混乱,可能还有错误的地方
- 待补充
没有评论