gRPC Protobuf 逆向初探

逆向时开始见到 gRPC 协议和 Protobuf 编码在私信、直播等领域使用,故记录之。

gRPC 是基于 HTTP/2.0 来传输的,但 Fiddler 5 似乎尚不支持,在抓包某 App 时发现了神奇的现象,同样的功能,Fiddler 抓到了 HTTP/1.1 的请求,mitmproxy 抓到了 HTTP/2.0 的请求,URL 的 Path 相同而 Host 不一样,猜测是客户端做了 FallBack。

抓到包后其实有两种选择,最开始我找到发请求的地点,闷头逆向 Java 层代码,打印数据(注意是否有类继承 com.google.protobuf.GeneratedMessageLite),一个个字段找生成的位置,但因为不熟悉客户端所用组件,往往耗时耗力找不到关键,而且考虑到之后的目标是正向构造请求,确定 proto 协议才是关键,与其往客户端实现层分析,不如直接从报文 body 着手。

Protobuf 高效的一大原因在于其将字段名放在双方持有的 proto 中,传输的数据仅有 enum 编号,但数据本身的值却完全是可以解读的,protoc --decode_raw < file 就能打印出解析后的数据,也有在线网站,但我刚开始复制报文 body 却总是解析失败,mitmproxy 可选择以 protobuf 解码数据,却也失败,关键在于传输的是 Length-Prefixed-Message 而非直接是 protobuf,即第一个 byte 值为 1/0 表示是否压缩,再 4 byte (big endian) 表示消息长度,剩下的才是消息,而如果有压缩的话,还要再把消息解压缩,比如最常见的 gzip,其前 10 byte 又是压缩相关的头部信息,而 App 可能会刻意设置 gzip header 以防伪造,若非提前看到 哔哩哔哩视频和字幕接口分析 这篇文章,必然踩坑。

def gzip_compress(buf: bytes, bz=True) -> bytes:
    compressed = gzip.compress(buf)
    if bz:  # special header
      compressed = compressed[:3] + bytes(7*[0]) + compressed[10:]
    return compressed

def length_prefixed_enc(buf: bytes, compress: bool = True) -> bytes:
    buf = gzip_compress(buf) if compress else buf
    return struct.pack("!bl", compress, len(buf)) + buf
  
def length_prefixed_dec(msg: bytes) -> bytes:
    compress, length = struct.unpack("!bl", msg[:5])
    buf = gzip.decompress(msg[5:5+length]) if compress else msg[5:5+length]
    return buf

将 Protobuf 的原始数据提取出后,即可用 protoc 解得没有字段名的原始数据,配合动静态分析获得的值,一般就能手写对应的 proto 文件,接着就由 proto 文件生成不同语言的对应代码,可以尝试直接调用 gRPC,也可以仅生成 Protobuf 的 msg 对象再 SerializeToString(),生成 Length-Prefixed-Message 作为 body,添加 header "Content-Type": "application/grpc",这样构造 HTTP 请求亦可生效。

评论正在加载中...如果评论较长时间无法加载,你可以 搜索对应的 issue 或者 新建一个 issue