一、RPC 技术详解
1. RPC 的基本概念
RPC(Remote Procedure Call,远程过程调用) 是一种极具创新性的技术,它打破了传统本地调用的局限,允许应用程序通过网络无缝调用远程服务端的方法。对于开发者而言,就仿佛在调用本地函数一样自然流畅,底层复杂的网络通信细节被巧妙地隐藏起来,极大地提升了开发效率。
其主要特点体现在以下两个关键方面:
-
透明性
当开发者进行远程接口调用时,无需为底层的序列化操作、传输协议的选择以及网络异常的处理等繁琐问题而烦恼。所有这些复杂的细节都由强大的 RPC 框架精心封装,让开发者能够专注于业务逻辑的实现。 -
封装性
客户端和服务端通过接口契约(IDL: Interface Definition Language) 进行紧密协作。在开发之初,双方就需要明确约定好数据格式和方法签名。目前,常用的格式包括 Protocol Buffers 和 JSON,其中 Protocol Buffers 以其更高的性能和更小的数据体积脱颖而出,成为众多开发者的首选。
2. RPC 的工作流程
RPC 调用的流程可以细致地拆解为以下几个关键步骤:
-
客户端调用本地代理(Stub)
当我们在代码中调用如userService.getUser(id)
这样的方法时,Stub 会迅速发挥作用。它会将方法名和参数进行序列化处理,常见的序列化格式包括二进制或 JSON 格式。经过序列化后的数据将被交由网络传输模块,为后续的网络传输做好准备。 -
网络传输
序列化后的数据通过 TCP、HTTP 或其他高效的传输协议,跨越网络的界限,被准确无误地发送到远程服务器。在这个过程中,网络传输协议的选择会直接影响数据传输的效率和稳定性。 -
服务端处理请求
服务端成功接收到数据后,会立即进行反序列化操作,将数据还原为原始的格式。然后,通过精确的识别机制,确定具体调用的服务和方法。接着,执行相应的业务逻辑,并将执行结果再次进行序列化处理,最终将结果返回给客户端。 -
客户端接收结果
客户端接收到响应数据后,会再次进行反序列化操作,将数据转换为可处理的格式。最后,将结果传递给原始调用者,以便继续后续的业务逻辑处理。
3. 典型组件介绍
RPC 框架通常由以下几个重要的组成部分协同工作:
|
|
---|---|
客户端代理(Stub) |
|
序列化协议 |
|
网络通信层 |
|
服务端框架 |
|
4. RPC 的应用场景与优缺点
应用场景
-
微服务架构内部通信
在微服务架构中,各个微服务之间可以通过 RPC 实现高效、跨语言的数据交换。这种方式能够显著降低系统间的耦合度,提高系统的可维护性和可扩展性。例如,订单服务可以通过 RPC 调用支付服务,实现订单支付的功能。 -
高性能分布式系统
在实时数据处理或高频交易系统中,对系统的性能要求极高。采用二进制序列化协议(如 Protobuf)能够显著降低传输延迟,从而满足高性能需求。例如,高频交易系统需要在极短的时间内处理大量的交易数据,RPC 技术可以确保数据的快速传输和处理。 -
跨平台系统集成
RPC 能够巧妙地解决老旧系统和新开发应用间的数据交互问题,即使它们使用不同的编程语言和平台。例如,一个旧的 C++ 系统可以通过 RPC 与新开发的 Node.js 微服务进行无缝对接,实现系统的升级和扩展。
优缺点
优点:
-
高开发效率:使用类似本地函数调用的方式,大大简化了远程调用逻辑。开发者无需编写复杂的网络请求代码,只需专注于业务逻辑的实现,从而提高了开发效率。 -
卓越性能:采用二进制协议(如 Protobuf),传输数据量小、效率高;长连接机制也能减少握手时间,进一步提升系统的性能。例如,在大数据量的传输场景下,Protobuf 能够显著减少传输时间和带宽占用。 -
跨语言支持:通过统一的接口定义,轻松实现不同编程语言之间的互调。这使得开发者可以根据项目的需求选择最合适的编程语言,而无需担心语言之间的兼容性问题。
缺点:
-
调试难度较高:网络延迟、异常处理及超时重试等问题需要开发者额外关注。在调试过程中,由于涉及到网络通信,问题的定位和解决相对复杂,需要开发者具备丰富的网络知识和调试经验。 -
接口耦合风险:接口升级时要求客户端和服务端严格同步,否则可能出现调用异常。在系统的升级和维护过程中,需要谨慎处理接口的变化,确保客户端和服务端的兼容性。 -
技术复杂性:需要引入并学习额外的框架和序列化机制,初期学习曲线较陡峭。对于初学者来说,掌握 RPC 技术需要花费一定的时间和精力。
二、准备阶段与环境搭建
在本实例中,我们将精心选用前后端不同的技术栈来实现一个功能强大的键盘记录器。前端使用 JavaScript 结合 grpc-web
库,后端则采用 Go 语言搭配 Protocol Buffers。接下来,我们将详细介绍技术栈的选择、依赖的安装以及代码的生成过程。
1. 技术栈
-
前端:JavaScript 与 grpc-web
库。JavaScript 作为前端开发的主流语言,具有广泛的应用场景和丰富的生态系统。grpc-web
库则为前端与后端的 gRPC 服务提供了无缝的连接。 -
后端:Go 语言及 Protocol Buffers。Go 语言以其高效的性能和简洁的语法,成为后端开发的理想选择。Protocol Buffers 则作为一种高效的序列化协议,能够确保数据在传输过程中的高效性和准确性。 -
辅助工具:Node.js、npm、protoc 编译器。Node.js 为前端开发提供了强大的运行环境,npm 则是 JavaScript 包管理工具,方便我们安装和管理各种依赖。protoc 编译器则用于生成 Protocol Buffers 代码。
2. 安装依赖
首先,我们需要确保 Node.js 环境已安装。为了方便管理 Node.js 版本,推荐使用 nvm(Node Version Manager):
# 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# 重启终端后安装最新 LTS 版本 Node.js
nvm install --lts
安装完成 Node.js 后,我们需要安装 gRPC 和 Protobuf 相关插件:
# 安装 protoc 和插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
npm install -g protoc-gen-grpc-web
npm install --global protoc-gen-js
安装完成后,我们可以通过以下命令进行验证:
# 确认已安装所有必要插件
protoc-gen-go --version # 需要 v1.28+
protoc-gen-go-grpc --version # 需要 v1.2+
protoc-gen-grpc-web --version # 需要 1.5.0+
protoc-gen-js --version # 无版本信息,报错参数不存在说明安装成功
3. 生成代码
接下来,我们将编写 keylogger.proto
定义文件,并使用 protoc
编译器生成对应的 Go 和 JavaScript 代码。
keylogger.proto (路径:proto/keylogger.proto
)
syntax = "proto3";
package keylogger;
// 指定 Go 包路径:模块名/目录;包名
option go_package = "grpcKeyboardRecord/proto/keylogger;keylogger";
serviceKeyLogger{
rpc SendKey (KeyRequest) returns (KeyResponse);
}
messageKeyRequest{
string key = 1;
}
messageKeyResponse{
string status = 1;
}
利用以下命令生成代码:
# 生成 Go 和 gRPC-Web 代码
protoc # 调用 Protocol Buffers 编译器
-I=./proto # 指定 .proto 文件的搜索路径
--go_out=./proto/gen/go # Go 代码输出路径(消息结构)
--go_opt=paths=source_relative # Go 代码的路径映射规则
--go-grpc_out=./proto/gen/go # Go gRPC 服务代码输出路径
--go-grpc_opt=paths=source_relative # Go gRPC 代码路径映射规则
--js_out=import_style=commonjs:./proto/gen/js # JS 代码输出路径和模块风格
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./proto/gen/js # gRPC-Web 代码配置
proto/keylogger.proto # 输入的原型文件
# 根据当前需求修改命令
protoc
-I=./proto
--go_out=./proto/keylogger
--go_opt=paths=source_relative
--go-grpc_out=./proto/keylogger
--go-grpc_opt=paths=source_relative
--js_out=import_style=commonjs:./proto
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./proto
proto/keylogger.proto
代码生成详解
在生成代码的过程中,路径映射选项起着至关重要的作用。常见的两种路径映射模式为:
-
import 模式:根据 go_package
生成路径,输出会依照模块名称构造目录结构。这种模式适用于需要将生成的代码集成到大型项目中的场景,能够确保代码的目录结构与项目的整体架构相匹配。 -
source_relative 模式:生成的代码与 .proto
文件的相对路径保持一致,便于管理和更新。这种模式使得生成的代码能够直接对应原始文件的位置,从而简化了后续项目的代码维护工作。
执行上述命令后,将在以下目录中生成相应的文件:
-
在 proto
目录下生成:keylogger_grpc_web_pb.js
和keylogger_pb.js
-
在 proto/keylogger
目录下生成:keylogger.pb.go
和keylogger_grpc.pb.go
三、后端代码实现
接下来,我们将进入后端代码的实现阶段。我们将使用 Go 语言编写一个强大的 gRPC 服务,并借助 grpc-web 库实现 HTTP 网关,使得前端能够通过浏览器方便地访问 gRPC 服务。
下面是 main.go
的完整代码及详细注释说明:
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"path/filepath"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"github.com/improbable-eng/grpc-web/go/grpcweb"
pb "grpcKeyboardRecord/proto"
)
// 定义服务实现结构体,嵌入生成的未实现接口
type server struct {
pb.UnimplementedKeyLoggerServer
}
// SendKey 方法:接收前端传入的按键,并打印,同时返回处理状态
func(s *server)SendKey(ctx context.Context, req *pb.KeyRequest)(*pb.KeyResponse, error) {
fmt.Printf("收到按键:%sn", req.GetKey())
return &pb.KeyResponse{Status: "接收成功"}, nil
}
funcmain() {
// 实例化 gRPC 服务器
grpcServer := grpc.NewServer()
// 注册自定义服务实现
pb.RegisterKeyLoggerServer(grpcServer, &server{})
// 注册服务反射,用于调试和客户端自动生成调用信息
reflection.Register(grpcServer)
// 使用 grpc-web 包将 gRPC 服务器包装为支持 HTTP 请求的服务
grpcWebServer := grpcweb.WrapServer(grpcServer)
// 设置静态文件服务,分别托管 public 和 dist 下的文件
publicFS := http.FileServer(http.Dir("public"))
distFS := http.FileServer(http.Dir("dist"))
// 自定义 HTTP 路由处理器
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 首先判断是否为 gRPC-Web 请求,如是则交给 grpcWebServer 处理
if grpcWebServer.IsGrpcWebRequest(r) || grpcWebServer.IsAcceptableGrpcCorsRequest(r) {
grpcWebServer.ServeHTTP(w, r)
return
}
// 对 URL 以 /dist/ 开头的请求进行特殊处理
if strings.HasPrefix(r.URL.Path, "/dist/") {
// 使用 StripPrefix 去掉 URL 中的 /dist/ 部分,再交由 distFS 处理
http.StripPrefix("/dist/", distFS).ServeHTTP(w, r)
return
}
// 对于 "/" 或没有文件后缀的请求(SPA 应用场景),返回 index.html
if r.URL.Path == "/" || filepath.Ext(r.URL.Path) == "" {
http.ServeFile(w, r, "public/index.html")
return
}
// 其他静态资源请求默认交由 publicFS 处理
publicFS.ServeHTTP(w, r)
})
// 在指定端口启动服务器
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
log.Println("服务启动在 http://localhost:8080")
if err := http.Serve(listener, handler); err != nil {
log.Fatalf("服务失败: %v", err)
}
}
代码详解
-
gRPC 服务器的初始化与注册
代码中首先创建了一个 gRPC 服务器实例,并将自定义的KeyLogger
服务注册到该服务器中。通过反射(reflection)功能,还可以在调试时使用 gRPC 客户端工具自动获取服务描述。这使得开发和调试过程更加高效和便捷。 -
grpc-web 的支持
利用grpcweb.WrapServer
方法,gRPC 服务器被包装,使其支持来自浏览器的 HTTP 请求。这样,前端开发者不必直接处理复杂的 gRPC 协议细节,只需要通过 HTTP 请求即可与后端服务进行交互。 -
静态文件的路由配置
自定义 HTTP 处理函数中,对请求 URL 进行细致的判断:-
若为 gRPC-Web 请求则交由 grpc-web 服务处理,确保 gRPC 服务的正常运行。 -
若 URL 包含 /dist/
则使用http.StripPrefix
来映射到静态资源目录,方便前端获取静态资源。 -
对于根路径或没有后缀的请求,返回单页面应用的入口文件 index.html
,确保单页面应用的正常访问。 -
其余请求则使用 public 目录下的资源,提供统一的静态资源服务。
-
四、前端代码实现
前端部分我们将使用 JavaScript 编写,通过 grpc-web
实现与后端服务的高效交互,并借助 webpack 进行打包优化。下面详细介绍每个文件的作用和实现逻辑。
1. 环境搭建
先安装 webpack 及开发服务器:
npm install webpack -g
npm install webpack-dev-server -g
2. index.html
—— 页面入口文件
此文件用于展示页面内容,并引用打包后的 JavaScript 文件:
<!DOCTYPE html>
<html>
<head>
<metacharset="UTF-8" />
<title>gRPC Keylogger</title>
</head>
<body>
<h1>键盘记录器(Webpack 构建)</h1>
<p>请尝试按键,会发送到后端</p>
<scriptsrc="/dist/bundle.js"></script>
</body>
3. index.js
—— js 入口文件
import { KeyLoggerClient } from"../proto/keylogger_grpc_web_pb";
import { KeyRequest } from"../proto/keylogger_pb";
const client = new KeyLoggerClient("http://localhost:8080");
document.addEventListener("keydown", (event) => {
const request = new KeyRequest();
request.setKey(event.key);
client.sendKey(request, {}, (err, response) => {
if (err) {
console.error("Error:", err.message);
} else {
console.log("Server acknowledged:", response.getMessage());
}
});
});
4. webpack.config.js
—— webpack 配置文件
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
mode: "development",
devServer: {
static: "./public",
port: 3000,
},
resolve: {
fallback: {
buffer: require.resolve("buffer"),
},
},
};
执行
-
在根目录下执行 npx webpack
将src/index.js
文件构建打包到dist/bundle.js
。 -
在 public/index.html
中引用为绝对路径<script src="/dist/bundle.js"></script>
-
启动 服务器 go run main.go
,使用浏览器进行访问http://localhost:8080
RPC(含 gRPC)技术在网络安全领域的应用及优缺点
一、WebSocket 与 gRPC/RPC 在红队攻击中的对比
在红队攻击和工具开发里,WebSocket 和 RPC(如 gRPC)优势与劣势各异,对比情况如下:
|
|
|
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
典型场景选型建议
-
优先选 WebSocket:快速 PoC 开发、兼容 Web 基础设施、低权限环境。 -
优先选 gRPC/RPC:APT 长期潜伏、跨平台武器化、数据复杂传输。
混合攻击架构示例
高级 C2 架构
├── 入口层(WebSocket over WSS)
│ ├── 伪装为合法 Web 应用
│ └── 前端 JS 植入
├── 中继层(gRPC over HTTP/2)
│ ├── 内部微服务通信
│ └── 与云原生组件混合
└── 植入层
├── 轻量级 WebSocket 探针
└── 高隐蔽 gRPC 后门
二、AI 增强型防火墙 / WAF 对 gRPC C2 的对抗
即便防火墙 / WAF 引入 AI 技术,gRPC C2 通信仍可维持隐蔽性。
AI 防御检测维度
从元数据层(调用频率、服务 / 方法名语义、请求 - 响应时间)、行为层(流量周期性、数据包大小分布)、内容层(Protobuf 语法校验、二进制熵值分析)分析流量。
gRPC C2 对抗策略
-
元数据层:复用目标企业服务定义,动态化方法名。 -
行为层:基于正常流量特征生成负载,混入伪随机数据块。 -
内容层:分层加密,利用 Trailing Metadata 传递指令。
利用 AI 模型缺陷
数据污染攻击、对抗样本生成、模型逆向工程。
现实案例与未来方向
现实中,如 APT29 动态端口跳变和协议嫁接,Lazarus 组织上下文感知心跳和流量镜像。未来,防御方会引入新技术,攻击方则开发新架构和优化性能。
三、使用 gRPC 实现键盘记录器说明
使用 gRPC 技术实现键盘记录器,主要是为介绍该技术,未充分发挥其在网络安全领域优势,gRPC 更适合构建复杂分布式安全系统和工具。
原文始发于微信公众号(D1TASec):gRPC+Proto 实现键盘记录器 —— 深度实战解析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论