2025年12月21日

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


0. 背景:流量特征分析与合规预警

在企业级网络策略中,标准版 frp 流量因协议指纹明显,容易被防火墙或 DPI(深度包检测)识别并阻断。常见特征有:

  1. TLS 指纹:固定的 ClientHello 首字节(0x17)及特定加密套件顺序。

  2. 明文特征:控制信道中包含 privilege_key 等明文 JSON 字段。

  3. 行为特征:固定频率(默认 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/utls

2.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
修改点

  1. 立即启动:连接建立后立即发送首个心跳。

  2. 随机抖动:心跳间隔增加 ±20% 随机值。

  3. 垃圾填充:在 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/frps
go 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/frpc

5.2 配置文件关键项

配置文件参数项建议值说明
frps.initls_onlytrue强制走修改后的 TLS 逻辑
frps.iniheartbeat_timeout90放宽超时时间以适应随机心跳
frpc.initls_enabletrue客户端显式开启 TLS
frpc.iniheartbeat_interval30设置基准值触发随机逻辑

6. 验证:流量表现与局限性

完成修改并部署后,预期效果如下:

  • Wireshark 抓包

    • 连接初期显示为标准的 HTTP GET Host: www.baidu.com200 OK

    • 后续 TLS ClientHello 与 Chrome 浏览器完全一致(无 0x17 前缀)。

    • 载荷全加密,无明文 JSON 关键字。

  • 流量统计:心跳包大小动态变化,时间间隔无明显周期性。

局限性说明
尽管上述手段消除了协议指纹,但在高安全等级环境中(如 H800 计算集群),高级的 UEBA(用户实体行为分析) 仍可能通过以下特征发现异常:

  1. 非工作时间的持续长连接。

  2. 单 IP 出现异常的大流量吞吐。

  3. 目标服务器 IP 信誉度低(如家用宽带 IP)。