JS逆向 —— 基于 gRPC 的加解密对抗

admin 2025年4月21日00:54:21评论2 views字数 7809阅读26分1秒阅读模式

“A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践。”

基于 gRPC 的加解密对抗

前置知识

gRPC 介绍

gRPC 是一个由 Google 开发的开源高性能远程过程调用(RPC)框架,它基于 HTTP/2 协议和 Protocol Buffers 数据序列化格式。gRPC 支持多种编程语言,包括 C++、Java、Python、Go 和 JavaScript 等,是一种跨语言、模块化且高效的网络通信解决方案。

gRPC 的主要特点包括:

  1. 1. 高性能:使用二进制协议传输数据,比 JSON 等文本协议更高效。
  2. 2. 跨语言支持:允许不同语言编写的客户端和服务端进行通信。
  3. 3. 流式传输:支持单向流和双向流,适用于实时数据传输场景。
  4. 4. 安全性:通过 TLS 加密通信,确保数据传输的安全性。

grpc 简单示例

场景:Client 与 Server 均使用 Python(方便演示,依赖自行安装)

1、定义传输数据结果(Protobuf 文件)

首先需要创建一个 hello_world.proto 文件来定义服务接口和数据结构。

syntax = "proto3";package helloworld;// 请求消息message HelloRequest {string name = 1;}// 响应消息message HelloReponse {string message = 1;}// 服务定义service Greeter {// rpc 方法rpc SayHello (HelloRequest) returns (HelloReponse) {}}

这个文件定义了一个 Greeter 服务,其中有一个 SayHello 方法。客户端发送一个 HelloRequest 消息,服务器返回一个 HelloReply 消息。

2、生成 Python 代码

使用 grpc_tools.protoc 命令编译 hello.proto 文件,生成 hello_pb2.py 和 hello_pb2_grpc.py 文件。

  • • hello_world_pb2.py 包含消息类的定义
  • • hello_world_pb2_grpc.py包含服务端和客户端的桩代码
python -m grpc_tools.protoc -I--python_out=. --grpc_python_out=. hello_world.proto

3、编写代码

1.客户端:

import grpcimport hello_world_pb2import hello_world_pb2_grpcdefrun():    channel = grpc.insecure_channel('localhost:50051')    stub = hello_world_pb2_grpc.GreeterStub(channel)    response = stub.SayHello(hello_world_pb2.HelloRequest(name='Hello World'))print("Greeter client received: " + response.message)if __name__ == '__main__':    run()

2.服务端:

from concurrent import futuresimport grpcimport hello_world_pb2import hello_world_pb2_grpcclassGreeter(hello_world_pb2_grpc.GreeterServicer):defSayHello(self, request, context):return hello_world_pb2.HelloReply(message='【Server Say】 %s!' % request.name)defserve():    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))    hello_world_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)    server.add_insecure_port('[::]:50051')    server.start()    server.wait_for_termination()if __name__ == '__main__':    serve()

4、运行示例

首先启动服务器端:

python greeter_server.py 

然后运行客户端:

python greeter_client.py 

客户端将输出:

Greeter client received: 【Server Say】Hello World

可用时序图概括以上流程:

客户端->gRPC 序列化: Hello World(请求)gRPC 序列化->服务端: x00x00x00x00 ...服务端-->gRPC 序列化: x00x00x00x00 ...gRPC 序列化-->客户端: 【Server Say】Hello World(响应)

前端分析

作为示例,以对某系统获取证书信息接口响应的解密做为演示

先看请求包,从下图中可以看到 Content-Type 中指明了请求和响应是通过 gRPC 通信

JS逆向 —— 基于 gRPC 的加解密对抗
根据 gRPC 的规范,后台每个接口的请求与响应均存在一个 Protobuf 规范(可以理解为 Java 对象)来,存储,前端全局搜索该接口看就能看到相关的规范
JS逆向 —— 基于 gRPC 的加解密对抗

我们只做对响应包的解密,所以我们只看 GetLicenseContentResponse 的相关方法(可以直接看 decode 方法)

c.license_component.GetLicenseContentResponse = function() {functione(e) {if (e)for (var t = Object.keys(e), r = 0; r < t.length; ++r)null != e[t[r]] && (this[t[r]] = e[t[r]])    }return e.prototype.licenseFile = null,   // ......    e.decode = function(e, t) {        e instanceof o || (e = o.create(e));var r = void0 === t ? e.len: e.pos + t,        n = new c.license_component.GetLicenseContentResponse;while (e.pos < r) {var i = e.uint32();switch (i >>> 3) {case1:                n.licenseFile = c.license_cloud.license.VisibleFile.decode(e, e.uint32());break;default:                e.skipType(7 & i);break            }        }return n    },// ......} (),

可以直接问 AI,即可得到以下 proto 的部分规范

// GetLicenseContentResponse 包含许可证文件的内容message GetLicenseContentResponse {// licenseFile 是许可证文件的内容    VisibleFile licenseFile = 1;}

继续获取 VisibleFile 对应的相关规范

c.license_cloud.license.VisibleFile = function() {functione(e) {if (this.configs = [], this.relationships = [], e) for (var t = Object.keys(e), r = 0; r < t.length; ++r) null != e[t[r]] && (this[t[r]] = e[t[r]])    }return e.prototype.sn = "",    e.prototype.customId = "",    e.prototype.customName = "",    e.prototype.licenseType = 0,    e.prototype.deployLimit = s.Long ? s.Long.fromBits(00, !1) : 0,    e.prototype.version = s.Long ? s.Long.fromBits(00, !1) : 0,    e.prototype.status = 0,    e.prototype.activateStatus = 0,    e.prototype.activateTime = null,    e.prototype.certificate = null,    e.prototype.productId = "",    e.prototype.productName = "",    e.prototype.configs = s.emptyArray,    e.prototype.modules = null,    e.prototype.relationships = s.emptyArray,    e.prototype.createAt = null,    e.prototype.coid = "",// ......    e.encode = function(e, t) {if (t || (t = i.create()),null != e.sn && Object.hasOwnProperty.call(e, "sn") && t.uint32(10).string(e.sn),null != e.customId && Object.hasOwnProperty.call(e, "customId") && t.uint32(18).string(e.customId),null != e.customName && Object.hasOwnProperty.call(e, "customName") && t.uint32(26).string(e.customName),null != e.licenseType && Object.hasOwnProperty.call(e, "licenseType") && t.uint32(32).int32(e.licenseType),null != e.deployLimit && Object.hasOwnProperty.call(e, "deployLimit") && t.uint32(40).int64(e.deployLimit),null != e.version && Object.hasOwnProperty.call(e, "version") && t.uint32(48).int64(e.version),null != e.status && Object.hasOwnProperty.call(e, "status") && t.uint32(56).int32(e.status),null != e.activateStatus && Object.hasOwnProperty.call(e, "activateStatus") && t.uint32(64).int32(e.activateStatus),null != e.activateTime && Object.hasOwnProperty.call(e, "activateTime") && c.google.protobuf.Timestamp.encode(e.activateTime, t.uint32(74).fork()).ldelim(),null != e.certificate && Object.hasOwnProperty.call(e, "certificate") && c.license_cloud.license.Certificate.encode(e.certificate, t.uint32(82).fork()).ldelim(),null != e.productId && Object.hasOwnProperty.call(e, "productId") && t.uint32(90).string(e.productId),null != e.productName && Object.hasOwnProperty.call(e, "productName") && t.uint32(98).string(e.productName),null != e.configs && e.configs.length)for (var r = 0; r < e.configs.length; ++r)                c.license_cloud.module.SimpleParam.encode(e.configs[r], t.uint32(162).fork()).ldelim();if (null != e.modules && Object.hasOwnProperty.call(e, "modules") && c.license_cloud.module.SimpleModule.encode(e.modules, t.uint32(170).fork()).ldelim(),// SimpleParam 是简单的参数结构message SimpleParam {    repeated string identifier = 1// 标识符    repeated string name = 2// 名称    repeated int32 component = 3// 组件类型    repeated string value = 4// 值    repeated Option option = 5// 选项    repeated Timestamp expiredTime = 6// 过期时间    repeated string unit = 7// 单位}// SimpleModule 是简单的模块结构message SimpleModule {    string name = 1// 名称    string identifier = 2// 标识符    repeated SimpleParam moduleParams = 3// 模块参数    repeated SimpleModule subModules = 4// 子模块}// Relationship 是关系结构message Relationship {    repeated string deployId = 1// 部署ID    repeated Timestamp createAt = 2// 创建时间    repeated AssetId assetId = 3// 资产ID}// Option 是选项结构message Option {    string label = 1// 标签    string value = 2// 值}// 定义 AssetId 消息message AssetId {  int64 oid = 1// 假设 oid 是一个 64 位整数  int64 id = 2;  // 假设 id 是一个 64 位整数}message Timestamp {  int64 seconds = 1;  int32 nanos = 2;}

备注:repeated 这个字段表示允许您在消息定义中指定一个字段可以重复任意次数,类似于数组或列表。懒得调试可以将消息的所有字段所有都加上,我这里是经调试部分字段是不需要重复的(毒点)。

联动 BP

BP 插件:autoDecoder

有了 proto 文件,通过该文件生成解密脚本了

cd protopython -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. .license.proto

编写脚本

import base64import loggingfrom loguru import loggerfrom urllib.parse import quote, unquotefrom flask import Flask, request, make_responsefrom proto import license_pb2app = Flask(__name__)log = logging.getLogger('werkzeug')log.disabled = True@app.route('/encode', methods=["POST"])defencrypt():    body = unquote(request.form.get('dataBody')).strip("n")    headers = request.form.get('dataHeaders')returnf"{headers}rnrnrnrn{body}"if headers else body@app.route('/decode', methods=["POST"])defdecrypt():    body = unquote(request.form.get('dataBody')).strip("n")    headers = request.form.get('dataHeaders')try:        decode_body = base64.b64decode(body)        grpc_obj = license_pb2.GetLicenseContentResponse()        grpc_obj.ParseFromString(decode_body[decode_body.find(b'x0a'):])        decrypt_body = str(grpc_obj)returnf"{headers}rnrnrnrn{decrypt_body}"if headers else decrypt_bodyexcept Exception as e:returnf"{headers}rnrnrnrn{body}"if headers else bodyif __name__ == '__main__':    app.run(debug=False, host='0.0.0.0', port=1664)

启动脚本

python app.py

插件配置一下解密接口链接

JS逆向 —— 基于 gRPC 的加解密对抗

最终效果如下:

JS逆向 —— 基于 gRPC 的加解密对抗

总结

综合以上,gRPC 在前端加密对抗的难点(复杂点)如下:

  1. 1. 从前端逆向分析到 proto 文件的创建
  2. 2. 每个请求和响应都需要建立一个 proto 文件

笔者水平有限,这种场景下目前解决方案只能一个接口一个接口的进行分析加解密,如果有更方便的解决方案的话欢迎补充

原文始发于微信公众号(A9 Team):JS逆向 —— 基于 gRPC 的加解密对抗

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月21日00:54:21
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   JS逆向 —— 基于 gRPC 的加解密对抗https://cn-sec.com/archives/3975093.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息