“A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践。”
基于 gRPC 的加解密对抗
前置知识
gRPC 介绍
gRPC 是一个由 Google 开发的开源高性能远程过程调用(RPC)框架,它基于 HTTP/2 协议和 Protocol Buffers 数据序列化格式。gRPC 支持多种编程语言,包括 C++、Java、Python、Go 和 JavaScript 等,是一种跨语言、模块化且高效的网络通信解决方案。
gRPC 的主要特点包括:
-
1. 高性能:使用二进制协议传输数据,比 JSON 等文本协议更高效。 -
2. 跨语言支持:允许不同语言编写的客户端和服务端进行通信。 -
3. 流式传输:支持单向流和双向流,适用于实时数据传输场景。 -
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 通信
我们只做对响应包的解密,所以我们只看 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(0, 0, !1) : 0, e.prototype.version = s.Long ? s.Long.fromBits(0, 0, !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
插件配置一下解密接口链接
最终效果如下:
总结
综合以上,gRPC 在前端加密对抗的难点(复杂点)如下:
-
1. 从前端逆向分析到 proto 文件的创建 -
2. 每个请求和响应都需要建立一个 proto 文件
笔者水平有限,这种场景下目前解决方案只能一个接口一个接口的进行分析加解密,如果有更方便的解决方案的话欢迎补充
原文始发于微信公众号(A9 Team):JS逆向 —— 基于 gRPC 的加解密对抗
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论