实战指南:FRP 流量特征深度伪装与隐蔽通信改造

0. 背景:流量特征分析与合规预警
在企业级网络策略中,标准版 frp 流量因协议指纹明显,容易被防火墙或 DPI(深度包检测)识别并阻断。常见特征有:
-
TLS 指纹:固定的 ClientHello 首字节(0x17)及特定加密套件顺序。
-
明文特征:控制信道中包含
privilege_key等明文 JSON 字段。 -
行为特征:固定频率(默认 30s)且包大小恒定的心跳包。
本文基于 frp v0.65.0 源码,从协议头注入、TLS 指纹模拟、载荷混淆、心跳随机化四个方向改造,让 frp 流量更像普通 HTTPS 浏览器访问。
核心警告
不兼容性:本文涉及的修改会导致客户端(frpc)与原版服务端(frps)无法互通,必须两端同时编译部署。
法律风险:此类修改属于网络防御规避(Defense Evasion)。在未经授权的网络环境中使用可能违反法律或公司合规政策,仅用于安全测试或科研环境。
1. 协议头注入:伪装 HTTP 握手
原理:在 TCP 连接建立后、TLS 握手前,插入 HTTP 请求交互。防火墙会先识别到合法的 HTTP 流量(模拟访问百度),从而放行后续流量。
1.1 客户端修改
文件路径:client/connector.go
逻辑:在 realConnect 方法中,TCP 连接建立后立即发送 HTTP GET 请求,并校验服务端返回的 200 OK。
if protocol == "tcp" { // 1. 发送伪造 HTTP 请求头 (模拟访问白名单域名) fakeHeader := "GET / HTTP/1.1\r\nHost: www.baidu.com\r\nUser-Agent: Mozilla/5.0\r\nAccept: */*\r\n\r\n" if _, err := conn.Write([]byte(fakeHeader)); err != nil { conn.Close() return nil, fmt.Errorf("write fake header error: %v", err) }
// 2. 读取并校验服务端响应 buf := make([]byte, 1024) conn.SetReadDeadline(time.Now().Add(5 * time.Second)) n, err := conn.Read(buf) conn.SetReadDeadline(time.Time{}) // 重置超时
if err != nil { conn.Close() return nil, fmt.Errorf("read fake response error: %v", err) }
// 必须包含 200 OK 才能继续 if !strings.Contains(string(buf[:n]), "200 OK") { conn.Close() return nil, fmt.Errorf("invalid fake response") } // ... 后续标准 TLS 握手逻辑 ...}1.2 服务端修改
文件路径:server/service.go
逻辑:引入中间层 FakeHandshakeListener,拦截并“吞掉”伪造的 HTTP 流量,仅将后续真实流量透传给 frp 逻辑。
// 自定义 Listener 包装器type FakeHandshakeListener struct { net.Listener}
func (l *FakeHandshakeListener) Accept() (net.Conn, error) { c, err := l.Listener.Accept() if err != nil { return nil, err } return &FakeHandshakeConn{Conn: c}, nil}
// 自定义 Conn 包装器type FakeHandshakeConn struct { net.Conn handshakeDone bool}
func (c *FakeHandshakeConn) Read(b []byte) (n int, err error) { // 握手完成后直接透传 if c.handshakeDone { return c.Conn.Read(b) }
// 1. 读取客户端伪造头 buf := make([]byte, 1024) n, err = c.Conn.Read(buf) if err != nil { return 0, err } data := string(buf[:n])
// 2. 校验特征 (Host: www.baidu.com) if strings.Contains(data, "Host: www.baidu.com") { // 3. 回复伪造 200 OK _, _ = c.Conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")) c.handshakeDone = true return 0, nil // 返回 0 字节,向上层隐藏此次交互 }
c.Conn.Close() return 0, fmt.Errorf("invalid handshake")}2. TLS 指纹伪装:模拟 Chrome
原理:Go 标准库 crypto/tls 的指纹特征固定。使用 utls 库完全模拟 Chrome 浏览器的 JA3 指纹,并移除 frp 特有的魔数。
2.1 引入 utls 依赖
go get github.com/refraction-networking/utls2.2 客户端集成 utls
文件路径:client/connector.go
import utls "github.com/refraction-networking/utls"
// ... 在 realConnect 中替换原有 TLS 逻辑 ...
tlsEnable := true // 强制开启 TLS
if tlsConfig != nil { uConfig := &utls.Config{ ServerName: tlsConfig.ServerName, InsecureSkipVerify: tlsConfig.InsecureSkipVerify, RootCAs: tlsConfig.RootCAs, NextProtos: tlsConfig.NextProtos, }
// 关键:使用 HelloChrome_Auto 模拟 Chrome 指纹 uConn := utls.UClient(conn, uConfig, utls.HelloChrome_Auto) if err := uConn.Handshake(); err != nil { conn.Close() return nil, err } conn = uConn // 替换连接对象}2.3 移除特征字节
文件路径:pkg/util/net/tls.go
逻辑:FRP 默认在 TLS 前发送 0x17,这是极强特征,需修改为标准 TLS ClientHello 首字节 0x16。
// var FRPTLSHeadByte = 0x17 // 原版var FRPTLSHeadByte = 0x16 // 修改后注:需同步注释掉 pkg/util/net/dial.go 中的 DialHookCustomTLSHeadByte 写入逻辑。
3. 载荷特征混淆:JSON 字段简化
原理:缩短并混淆 JSON 字段名,规避针对 privilege_key 等关键词的正则匹配。
文件路径:pkg/msg/msg.go
type Login struct { Version string `json:"v,omitempty"` // version -> v Hostname string `json:"h,omitempty"` // hostname -> h PrivilegeKey string `json:"pk,omitempty"` // privilege_key -> pk Timestamp int64 `json:"ts,omitempty"` // timestamp -> ts RunID string `json:"ri,omitempty"` // run_id -> ri}
type Ping struct { PrivilegeKey string `json:"pk,omitempty"` Timestamp int64 `json:"ts,omitempty"` Padding string `json:"p,omitempty"` // 新增填充字段}4. 心跳逻辑重构:随机化与抗分析
原理:打破固定时间间隔和包大小的统计特征。
文件路径:client/control.go
修改点:
-
立即启动:连接建立后立即发送首个心跳。
-
随机抖动:心跳间隔增加 ±20% 随机值。
-
垃圾填充:在
Padding字段填充随机长度字符。
go func() { // 1. 立即发送首个心跳 if _, err := sendHeartBeat(); err != nil { xl.Warnf("send first heartbeat error: %v", err) }
for { // 2. 计算基准时间 + 随机抖动 (30s ± 20%) baseInterval := time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatInterval) * time.Second jitter := time.Duration(rand.Intn(int(baseInterval)/5*2) - int(baseInterval)/5)
select { case <-time.After(baseInterval + jitter): // 3. 发送带 Padding 的心跳 if _, err := sendHeartBeat(); err != nil { xl.Warnf("send heartbeat error: %v", err) } case <-ctl.doneCh: return } }}()5. 构建与部署:编译配置
5.1 编译命令 (PowerShell)
必须禁用 CGO 以确保静态链接。
# Linux 服务端/客户端$env:CGO_ENABLED="0"; $env:GOOS="linux"; $env:GOARCH="amd64"go build -ldflags "-s -w" -o frps ./cmd/frpsgo build -ldflags "-s -w" -o frpc_linux ./cmd/frpc
# Windows 客户端$env:CGO_ENABLED="0"; $env:GOOS="windows"; $env:GOARCH="amd64"go build -ldflags "-s -w" -o frpc.exe ./cmd/frpc5.2 配置文件关键项
| 配置文件 | 参数项 | 建议值 | 说明 |
|---|---|---|---|
| frps.ini | tls_only | true | 强制走修改后的 TLS 逻辑 |
| frps.ini | heartbeat_timeout | 90 | 放宽超时时间以适应随机心跳 |
| frpc.ini | tls_enable | true | 客户端显式开启 TLS |
| frpc.ini | heartbeat_interval | 30 | 设置基准值触发随机逻辑 |
6. 验证:流量表现与局限性
完成修改并部署后,预期效果如下:
-
Wireshark 抓包:
-
连接初期显示为标准的
HTTP GET Host: www.baidu.com及200 OK。 -
后续 TLS ClientHello 与 Chrome 浏览器完全一致(无
0x17前缀)。 -
载荷全加密,无明文 JSON 关键字。
-
-
流量统计:心跳包大小动态变化,时间间隔无明显周期性。
局限性说明:
尽管上述手段消除了协议指纹,但在高安全等级环境中(如 H800 计算集群),高级的 UEBA(用户实体行为分析) 仍可能通过以下特征发现异常:
-
非工作时间的持续长连接。
-
单 IP 出现异常的大流量吞吐。
-
目标服务器 IP 信誉度低(如家用宽带 IP)。