JS `gRPC-Web` 流量解密与协议逆向

各位好!我是老司机,今天咱们聊聊一个挺有意思的话题:JS gRPC-Web 流量解密与协议逆向。

先别觉得“解密”、“逆向”这些词儿吓人,其实没那么玄乎。咱们的目标是:搞清楚用 JS 写的 gRPC-Web 应用,它发出去的流量长啥样,里面都藏着什么秘密。就像侦探破案一样,一步步抽丝剥茧,最后把真相揪出来。

一、为啥要搞这个?

你可能会问,好好的协议,干嘛非要逆向?原因嘛,有很多:

  • 安全审计: 检查数据传输是否安全,有没有敏感信息泄露。
  • 问题排查: 当客户端和服务端通信出问题时,可以通过分析流量来定位问题。
  • 协议理解: 深入理解 gRPC-Web 协议的细节,知其然更知其所以然。
  • 第三方集成: 有时候需要与使用了 gRPC-Web 的系统进行集成,但官方文档不够详细,就需要通过逆向来补充。

总之,掌握这个技能,就像多了一把瑞士军刀,关键时刻能派上大用场。

二、gRPC-Web 基础知识回顾

在开始解密之前,咱们先简单回顾一下 gRPC-Web 的一些基本概念。如果你已经很熟悉了,可以直接跳过这部分。

  • gRPC: Google 开发的一个高性能、开源和通用的 RPC 框架。
  • Protocol Buffers (protobuf): 一种轻便高效的数据序列化格式,gRPC 默认使用它来定义服务接口和消息结构。
  • gRPC-Web: gRPC 的一个变体,允许 Web 应用(也就是跑在浏览器里的 JS 代码)直接调用 gRPC 服务,而不需要中间的 HTTP/1.1 网关。
  • HTTP/2: gRPC-Web 必须基于 HTTP/2 协议,因为 HTTP/2 支持双向流和多路复用等特性,这些都是 gRPC 所需要的。
  • Content-Type: gRPC-Web 使用特定的 Content-Type 来标识 gRPC 请求和响应,例如 application/grpc-web+proto

三、抓包:拿到第一手数据

要解密流量,首先得有流量。抓包就是获取流量数据的关键一步。咱们可以用一些常见的抓包工具,比如:

  • Chrome 开发者工具: 自带的网络面板,简单易用,适合快速分析。
  • Wireshark: 功能强大的网络协议分析器,可以捕获所有网络流量,适合深入分析。
  • Fiddler: 免费的网络调试工具,可以修改请求和响应,适合模拟各种场景。

这里我推荐使用 Chrome 开发者工具,因为我们主要关注的是 Web 应用的流量。

  1. 打开你的 gRPC-Web 应用。
  2. 打开 Chrome 开发者工具(快捷键:F12)。
  3. 切换到 "Network" (网络) 面板。
  4. 刷新页面,或者执行一些会发起 gRPC 请求的操作。
  5. 在 "Network" 面板中,找到那些 Content-Type 是 application/grpc-web+proto 的请求。

恭喜你,已经成功抓到 gRPC-Web 的流量了!

四、流量结构分析:拨开云雾见青天

抓到的流量,看起来可能是一堆乱码。别慌,咱们来一点点分析。

gRPC-Web 的流量结构,大致可以分为以下几个部分:

  1. HTTP/2 Headers: HTTP/2 的头部信息,包含了请求方法、URL、Content-Type 等。
  2. gRPC-Web Framing: gRPC-Web 使用了一种特殊的 Framing 机制,将 protobuf 消息分割成多个帧。
  3. protobuf Message: 实际的 protobuf 消息数据,也就是我们最关心的内容。
  4. Trailing Metadata: gRPC 的尾部元数据,包含了状态码、错误信息等。

咱们重点关注 gRPC-Web Framing 和 protobuf Message。

gRPC-Web Framing 格式

gRPC-Web Framing 格式如下:

Field Length (bytes) Description
Compressed Flag 1 0x00 表示未压缩,0x01 表示使用 gzip 压缩
Message Length 4 protobuf 消息的长度 (Big Endian 字节序)
Message Data Variable 实际的 protobuf 消息数据

示例代码:解析 Framing

下面是一个简单的 JS 函数,用于解析 gRPC-Web Framing:

function parseGrpcWebFrame(dataView, offset) {
  const compressedFlag = dataView.getUint8(offset);
  offset += 1;

  const messageLength = dataView.getUint32(offset, false); // false 表示 Big Endian
  offset += 4;

  const messageData = new Uint8Array(dataView.buffer, offset, messageLength);
  offset += messageLength;

  return {
    compressedFlag,
    messageData,
    nextOffset: offset,
  };
}

// 示例用法
const buffer = /* 你的流量数据 */;
const dataView = new DataView(buffer);
let offset = 0;

while (offset < dataView.byteLength) {
  const frame = parseGrpcWebFrame(dataView, offset);
  console.log("Compressed Flag:", frame.compressedFlag);
  console.log("Message Data:", frame.messageData);
  offset = frame.nextOffset;
}

这段代码会循环解析流量数据,提取出每个帧的压缩标志和消息数据。

五、protobuf 反序列化:还原真实数据

拿到 protobuf 消息数据后,下一步就是反序列化。我们需要使用 protobuf 的定义文件 ( .proto 文件) 来解析这些二进制数据。

  1. 获取 .proto 文件: 通常可以从服务端代码或者 API 文档中找到 .proto 文件。
  2. 使用 protobuf.js protobuf.js 是一个流行的 JS 库,用于在浏览器中处理 protobuf 消息。

示例代码:使用 protobuf.js 反序列化

首先,你需要安装 protobuf.js

npm install protobufjs

然后,可以使用以下代码来反序列化 protobuf 消息:

const protobuf = require("protobufjs");

async function decodeProtobuf(protoFilePath, messageType, buffer) {
  try {
    const root = await protobuf.load(protoFilePath);
    const MessageType = root.lookupType(messageType);
    const message = MessageType.decode(buffer);
    return message;
  } catch (error) {
    console.error("Error decoding protobuf:", error);
    return null;
  }
}

// 示例用法
const protoFilePath = "path/to/your.proto";
const messageType = "YourMessageType";
const buffer = /* 你的 protobuf 消息数据 */;

decodeProtobuf(protoFilePath, messageType, buffer)
  .then((message) => {
    if (message) {
      console.log("Decoded Message:", message);
    }
  });

这段代码会加载 .proto 文件,找到指定的 message type,然后使用 decode 方法将二进制数据反序列化成 JS 对象。

六、处理压缩数据:解开最后的枷锁

如果 compressedFlag 的值是 0x01,说明消息数据被 gzip 压缩了。我们需要先解压缩,才能进行 protobuf 反序列化。

示例代码:解压缩 gzip 数据

可以使用 pako 库来解压缩 gzip 数据:

const pako = require("pako");

function ungzip(buffer) {
  try {
    const decompressed = pako.ungzip(buffer);
    return decompressed;
  } catch (error) {
    console.error("Error ungzipping data:", error);
    return null;
  }
}

// 示例用法
const compressedData = /* 你的 gzip 压缩数据 */;
const decompressedData = ungzip(compressedData);

if (decompressedData) {
  // 对 decompressedData 进行 protobuf 反序列化
}

七、协议逆向:没有 .proto 文件怎么办?

有时候,我们可能无法拿到 .proto 文件。这时候,就需要进行协议逆向,手动分析 protobuf 消息的结构。

这需要一些技巧和经验,但也不是不可能。可以尝试以下方法:

  • 观察不同请求的差异: 发送不同的请求,观察流量数据的变化,推断字段的含义。
  • 猜测字段类型: 根据数据的格式和值,猜测字段的类型(例如,int32, string, bool 等)。
  • 查阅 protobuf 文档: 熟悉 protobuf 的基本类型和编码方式。

虽然逆向过程比较繁琐,但只要有耐心,总能找到突破口。

八、实战案例:分析一个简单的 gRPC-Web 应用

为了让大家更好地理解,咱们来分析一个简单的 gRPC-Web 应用。

假设我们有一个简单的 gRPC 服务,定义如下:

syntax = "proto3";

package example;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

客户端代码如下(简化版):

const { GreeterClient } = require("./generated/Greeter_grpc_web_pb");
const { HelloRequest } = require("./generated/Greeter_pb");

const client = new GreeterClient("http://localhost:8080");
const request = new HelloRequest();
request.setName("World");

client.sayHello(request, {}, (err, response) => {
  if (err) {
    console.error(err);
  } else {
    console.log(response.getMessage());
  }
});
  1. 抓包: 使用 Chrome 开发者工具抓取请求流量。
  2. 分析 Framing: 解析 gRPC-Web Framing,提取 protobuf 消息数据。
  3. 反序列化: 使用 protobuf.js.proto 文件反序列化 protobuf 消息。

通过以上步骤,我们可以看到请求中包含了 "name" 字段,值为 "World",响应中包含了 "message" 字段,值为 "Hello World!"。

九、总结与展望

今天我们一起学习了 JS gRPC-Web 流量解密与协议逆向的基本方法。虽然这只是一个入门级的介绍,但希望能给大家打开一扇新的大门。

未来,随着 gRPC-Web 的普及,流量解密和协议逆向的需求也会越来越大。掌握这些技能,将有助于我们更好地理解和使用 gRPC-Web。

一些建议:

  • 多实践:只有通过实际操作,才能真正掌握这些技能。
  • 多交流:与其他开发者交流经验,共同进步。
  • 关注最新技术:gRPC-Web 还在不断发展,要及时关注最新的技术动态。

好了,今天的分享就到这里。希望对大家有所帮助! 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注