gRPC+Proto 实现键盘记录器 —— 深度实战解析

在当今的分布式系统开发领域,RPC(Remote Procedure Call,远程过程调用) 技术犹如一颗璀璨的明星,凭借其强大的透明性和卓越的高性能,在微服务架构中占据着举足轻重的地位。本文将全方位、深入地剖析 RPC 的基本原理、显著优势以及潜在的局限性。同时,我们将以键盘记录器项目为例,详细演示如何巧妙运用 gRPC 与 Protocol Buffers 实现跨语言通信。通过丰富的代码讲解和直观的图示,助力你迅速掌握从环境搭建到前后端代码实现的每一个核心细节。


一、RPC 技术详解

1. RPC 的基本概念

RPC(Remote Procedure Call,远程过程调用) 是一种极具创新性的技术,它打破了传统本地调用的局限,允许应用程序通过网络无缝调用远程服务端的方法。对于开发者而言,就仿佛在调用本地函数一样自然流畅,底层复杂的网络通信细节被巧妙地隐藏起来,极大地提升了开发效率。

其主要特点体现在以下两个关键方面:

  • 透明性

    当开发者进行远程接口调用时,无需为底层的序列化操作、传输协议的选择以及网络异常的处理等繁琐问题而烦恼。所有这些复杂的细节都由强大的 RPC 框架精心封装,让开发者能够专注于业务逻辑的实现。

  • 封装性

    客户端和服务端通过接口契约(IDL: Interface Definition Language) 进行紧密协作。在开发之初,双方就需要明确约定好数据格式和方法签名。目前,常用的格式包括 Protocol Buffers 和 JSON,其中 Protocol Buffers 以其更高的性能和更小的数据体积脱颖而出,成为众多开发者的首选。

2. RPC 的工作流程

RPC 调用的流程可以细致地拆解为以下几个关键步骤:

  1. 客户端调用本地代理(Stub)

    当我们在代码中调用如 userService.getUser(id) 这样的方法时,Stub 会迅速发挥作用。它会将方法名和参数进行序列化处理,常见的序列化格式包括二进制或 JSON 格式。经过序列化后的数据将被交由网络传输模块,为后续的网络传输做好准备。

  2. 网络传输

    序列化后的数据通过 TCP、HTTP 或其他高效的传输协议,跨越网络的界限,被准确无误地发送到远程服务器。在这个过程中,网络传输协议的选择会直接影响数据传输的效率和稳定性。

  3. 服务端处理请求

    服务端成功接收到数据后,会立即进行反序列化操作,将数据还原为原始的格式。然后,通过精确的识别机制,确定具体调用的服务和方法。接着,执行相应的业务逻辑,并将执行结果再次进行序列化处理,最终将结果返回给客户端。

  4. 客户端接收结果

    客户端接收到响应数据后,会再次进行反序列化操作,将数据转换为可处理的格式。最后,将结果传递给原始调用者,以便继续后续的业务逻辑处理。

3. 典型组件介绍

RPC 框架通常由以下几个重要的组成部分协同工作:

组件 作用
客户端代理(Stub) 它的主要职责是封装复杂的网络请求,模拟本地调用的方式,让开发者无需关注网络细节,从而实现对远程服务的透明调用。
序列化协议 负责将对象转换为二进制或文本数据格式,常见的如 Protobuf。序列化协议的选择直接影响数据传输的效率和数据的安全性。
网络通信层 作为数据传输的桥梁,负责将序列化后的数据准确无误地传输到目标服务器。常见的协议包括 TCP、UDP 和 HTTP2 等,不同的协议适用于不同的应用场景。
服务端框架 承担着接收客户端请求的重任,通过精确的路由机制,将请求分发到具体的服务实现中,并将执行结果返回给客户端。

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):

bash 复制代码
# 安装 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 相关插件:

shell 复制代码
# 安装 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

安装完成后,我们可以通过以下命令进行验证:

sh 复制代码
# 确认已安装所有必要插件
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

protobuf 复制代码
syntax = "proto3";  
  
package keylogger;  
  
// 指定 Go 包路径:模块名/目录;包名  
option go_package = "grpcKeyboardRecord/proto/keylogger;keylogger";  
  
service KeyLogger {  
  rpc SendKey (KeyRequest) returns (KeyResponse);  
}  
  
message KeyRequest {  
  string key = 1;  
}  
  
message KeyResponse {  
  string status = 1;  
}

利用以下命令生成代码:

bash 复制代码
# 生成 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.jskeylogger_pb.js
  • proto/keylogger 目录下生成:keylogger.pb.gokeylogger_grpc.pb.go

三、后端代码实现

接下来,我们将进入后端代码的实现阶段。我们将使用 Go 语言编写一个强大的 gRPC 服务,并借助 grpc-web 库实现 HTTP 网关,使得前端能够通过浏览器方便地访问 gRPC 服务。

下面是 main.go 的完整代码及详细注释说明:

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("收到按键:%s\n", req.GetKey())  
    return &pb.KeyResponse{Status: "接收成功"}, nil  
}  
  
func main() {  
    // 实例化 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 及开发服务器:

bash 复制代码
npm install webpack -g
npm install webpack-dev-server -g

2. index.html ------ 页面入口文件

此文件用于展示页面内容,并引用打包后的 JavaScript 文件:

html 复制代码
<!DOCTYPE html>  
<html>  
<head>  
    <meta charset="UTF-8" />  
    <title>gRPC Keylogger</title>  
</head>  
<body>  
<h1>键盘记录器(Webpack 构建)</h1>  
<p>请尝试按键,会发送到后端</p>  
<script src="/dist/bundle.js"></script>  
</body>  

3. index.js ------ 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配置文件

js 复制代码
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"),  
        },  
    },  
	}; 

执行

  1. 在根目录下执行 npx webpacksrc/index.js 文件构建打包到 dist/bundle.js

  2. public/index.html 中引用为绝对路径 <script src="/dist/bundle.js"></script>

  3. 启动 服务器 go run main.go ,使用浏览器进行访问 http://localhost:8080



RPC(含 gRPC)技术在网络安全领域的应用及优缺点

一、WebSocket 与 gRPC/RPC 在红队攻击中的对比

在红队攻击和工具开发里,WebSocket 和 RPC(如 gRPC)优势与劣势各异,对比情况如下:

对比维度 WebSocket gRPC/RPC
协议基础 基于 HTTP/1.1 Upgrade 机制,支持文本或二进制帧,持久化全双工通信,加密可选 基于 HTTP/2(gRPC)或自定义协议,强制二进制,多路复用短连接,gRPC 强制 TLS
隐蔽通信能力 握手头明显,文本模式易被记录,长连接易被检测 使用标准 HTTP/2 流,Protobuf 二进制编码难解析,多路复用流量可伪装
工具开发效率 主流语言有成熟库,但需手动设计消息格式和分帧 gRPC 支持 11+ 语言,Protobuf IDL 自动生成代码,自动分帧
防御绕过能力 WAF 检测规则成熟,时序特征明显,但可通过代理和域名前置 二进制编码绕过率高,多路复用流量难区分,但 gRPC-Web 需特化代理,域名前置需定制策略

典型场景选型建议

  • 优先选 WebSocket:快速 PoC 开发、兼容 Web 基础设施、低权限环境。
  • 优先选 gRPC/RPC:APT 长期潜伏、跨平台武器化、数据复杂传输。

混合攻击架构示例

text 复制代码
高级 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 更适合构建复杂分布式安全系统和工具。

相关推荐
半路_出家ren2 小时前
动态路由, RIP路由协议,RIPv1,RIPv2
网络·网络安全·rip·路由协议·动态路由·ripv1·ripv2
☆firefly☆8 小时前
[MRCTF2020]ezpop wp
web安全·网络安全
世界尽头与你2 天前
MacOS红队常用攻击命令
安全·macos·网络安全
游戏开发爱好者82 天前
使用克魔助手查看iOS 应用程序使用历史记录和耗能历史记录
websocket·网络协议·tcp/ip·http·网络安全·https·udp
jingshaoyou3 天前
【11】Strongswan processor 详解1
网络·网络安全
拾柒SHY3 天前
WEB安全-CTF中的PHP反序列化漏洞
web安全·网络安全
白猫a٩3 天前
记一次某网络安全比赛三阶段webserver应急响应解题过程
安全·web安全·网络安全
浩浩测试一下3 天前
网络安全中信息收集需要收集哪些信息了?汇总
安全·web安全·网络安全·oracle·sqlite·系统安全·可信计算技术
一口一个橘子3 天前
[ctfshow web入门] web40
前端·web安全·网络安全
Alfadi联盟 萧瑶3 天前
文件上传、读取与包含漏洞解析及防御实战
网络安全