这篇文章上次修改于 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看,网页只有移动端能看几分钟的预览,于是抓包之。

记录

然后找到了请求接口如下:

视频请求
视频返回

然后还有一个字幕,请求是另一个,没有和视频一起返回,请求接口如下:

字幕请求
字幕返回
字幕返回gzip解压

接口信息小结

视频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,一下就定位到了:

b.eds

com.bilibili.lib.moss.internal.impl.common.header.a

b.efd -> com.bapis.bilibili.metadata.fawkes.FawkesReq

然后直接用objection搜了一下,发现直接出来了

首先验证一下返回的是什么

b.efd.s

FawkesReq实例

如果对请求头x-bili-fawkes-req-bin的value进行base64解码,可以看到和objection的结果对的上

x-bili-fawkes-req-bin value

对应前面的结果,推测是通过com.google.protobuf.GeneratedMessageLite做了编码处理

以此类推,得到一张关于header在protobuf编码前的对应关系:

headervalues
x-bili-fawkes-req-binandroid, prod
x-bili-metadata-binidentify_v1, MobiApp, Device, Build, Channel, Buvid, Platform
authorizationidentify_v1
x-bili-device-binAppId, Build, Buvid, MobiApp, Platform, Device, Channel, Brand, Model, Osver
x-bili-network-binType, Tf, Oid
x-bili-restriction-bin
x-bili-locale-binLocale

一些对应关系:

keyvaluedescription
AccessKeyidentify_v1identify_v1指代authorization里面的参数
MobiAppandroidAPP标识
Device(none)?可能是空
Build6070600Build是APP版本号
Channelbilibili140应该是固定值
BuvidXY*Buvid是一个XY开头+35位16进制的字符串
Platformandroid系统平台
AppId1可能是API版本
BrandXiaomi手机厂商
ModelMi 9se手机型号
Osver10系统版本
Type1网络类型 1是WIFI
TfTF_UNKNOWN?
Oid46007?
Localezh, CN zh, CN推测是当前APP(current)语言
和系统(system)语言

com.bapis.bilibili.metadata.locale.Locale

一些相关的类:

  • 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

既然都已经找到请求头了,那就在这里看一下调用来源

watch b.edr.intercept

。。。一时没找到,还是从bilibili.pgc.gateway.player.v1.PlayURL下手

PlayView
io.grpc.MethodDescriptor

watch一下,可以看到最后有两个io.grpc.MethodDescriptor.a(java.io.InputStream)

watch io.grpc.MethodDescriptor

后面又有一个InputStream,根据抓包来看,依次对应下面三个请求:

不出意外的话,InputStream相关内容就是payload了
然后看下这个方法的调用栈,可以确定payload从这几步中产生

io.grpc.MethodDescriptor.a调用栈

挨个看了下,发现在b.eby.a能看到未处理的明文参数

原始参数

b.eby.a反编译代码

那么从这里追踪就能知道payload是怎么转换的了

追踪payload

根据前面的调用栈,依次查看:

  • com.bilibili.lib.moss.internal.impl.failover.a.a
  • io.grpc.stub.ClientCalls.a

com.bilibili.lib.moss.internal.impl.failover.a.a
io.grpc.stub.ClientCalls.a
com.google.common.util.concurrent.f
io.grpc.stub.ClientCalls.a
io.grpc.f.a

emmm

突然发现(傻了)b.eby.a的返回类型是PlayViewReply,既然是Reply那说明这都是返回数据了吧。。一看果然是,还是序列化好的那种。。

PlayViewReply

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;
        }
    }

writeTo调用栈

  1. 第一步,将有关参数通过com.google.protobuf.CodedOutputStream有关方法写数据到codedOutputStream

com.bapis.bilibili.pgc.gateway.player.v1.PlayViewReq.writeTo(Native Method)

注意这里是com.bapis.bilibili.pgc.gateway.player.v1.PlayViewReq.writeTo方法,hook看下这些值都是什么

PlayViewReq和AbstractMessageLite的信息

初步对比可以看到在AbstractMessageLite.writeTo这里,没有进行编码

  1. 第二步,通过调用flush函数将结果写到outputStream

com.google.protobuf.AbstractMessageLite.writeTo(BL:83)

  1. 第三步,这里会返回序列化后的大小,当然数据已经在outputStream里面了

b.hya.a(BL:52)

需要注意的是outputStream的具体实现方法如下:

outputStream具体实现

也就是数据会被压缩

结合抓包结果,小结如下:

  1. 将下列参数进行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
  1. gzip压缩,计算压缩后的数据长度,添加到头部并补充4位0
  2. 根据grpc文档,知道一次请求可以包含多条信息,也就是payload第一位是消息编号(?)

payload生成——DmView

方法com.bapis.bilibili.community.service.dm.v1.DMMoss.dmView(BL:64)的返回是DmViewReply,参数是DmViewReq,且通过hook可以确定DmViewReply的内容就是抓包的返回数据。

追踪分析,发现在b.edp.a(BL:78)这里数据和抓包结果一致。

最终payload

而在这个函数的上一步b.edp.a(BL:56)有一个生成byte的过程,查看发现调用了writeTo方法,具体实现是com.bapis.bilibili.community.service.dm.v1.DmViewReq.writeTo

第一步转换

toByteArray
writeTo

在这里可以看到数据被写入到codedOutputStream,具体的写入过程是protobuf编码转换,在此不展开。编码用到的数据hook结果如下:

第二步转换-1

第二步转换-1
第二步转换-2
第二步转换-3

其中b.edv.a后续几个调用不太重要,(r4)本质上是统计当前数据长度,写到byte中,填充4位0,最后和(r7)原数据写到结果中

至此字幕请求的payload生成分析就完成了,小结如下:

  1. 将下列参数进行protobuf编码
pid:497543586
oid:173415955
type:1
spmid:
HardBoot:1
  1. 计算编码后的数据长度,并填充4位0
  2. 返回数据需要通过gzip解开再通过protobuf格式化

后记

这些是探索过程的记录,后面会整理一篇更合理、更科学、更高效的分析方案。

其他记录和感想:

  1. 要注意:implements、interface、extends,水平有限,参考这篇文章:关键字 extend interface implement,然后记录三句话:

    1. implements是实现interface定义的类的具体方法功能,可以理解为为这个类附加一些额外的功能
    2. extends是继承一个抽象类,继承父类的全部功能
    3. interface是定义了一个比抽象类还要抽象的类,即极度抽象类。如果不通过关键字来实现具体的功能,是没有任何意思的
  2. 结合objection查看调用栈时,一定要注意传入的参数的具体实现方法,不然很可能花费大量时间寻找下一步的具体调用,在那里瞎找,白白浪费时间
  3. 对于流式对象及类似的对象,有的只能读取一次
  4. java基础很重要
  5. jadx看反编译好的代码时要结合smali具体分析,特别是一些抽象类的对象,在反编译的代码里面不容易看出来
  6. 通过frida hook的时候,要注意合理选择对应系统架构的server
  7. 笔记记得有些混乱,可能还有错误的地方
  8. 待补充