FRP源码深度刨析

随着功能逐渐增多,FRP也愈发臃肿,越来越不适用红队项目了,现在客户端已经达到了`14M`,这是奔着产品去了。 红队项目需要短小精悍,体积小,只保留最核心的功能,其他能减则减。 所以学习一下FRP的优点,有机会开发出适合自己使用的工具。

一、什么是 frp?

下载地址:https://github.com/fatedier/frp

frp 是一个快速反向代理,可让您将位于 NAT 或防火墙后面的本地服务器暴露到互联网。它目前支持TCPUDP,以及HTTPHTTPS协议,允许通过域名将请求转发到内部服务。

二、描述

随着功能逐渐增多,FRP也愈发臃肿,越来越不适用红队项目了,现在客户端已经达到了14M,这是奔着产品去了。

红队项目需要短小精悍,体积小,只保留最核心的功能,其他能减则减。

所以学习一下FRP的优点,有机会开发出适合自己使用的工具。

三、源码分析

先来熟悉一下源码

image-20240825200021480.png

3.1 整体流程

3.1.1 服务端

先来看一下服务端,先从cmd目录开始,处理命令行,初始化配置文件

image-20240825204647187.png

把配置文件传递给服务,并启动运行。

image-20240825204745242.png

**server.NewService**主要是把配置初始化给服务端的各个组件,并返回一个新的服务对象。那么新建服务的时候,都做了什么?

  1. 提取TLS的配置
  2. 如果配置了web服务端口,传入配置,并新建一个http服务。
  3. 初始化Server对象
  4. 如果配置了TCPMuxHTTPConnectPort端口,新建tcpmux.NewHTTPConnectTCPMuxer服务
  5. 遍历并初始化所有HTTPPlugins,使用svr.pluginManager.Register()初始化
  6. 初始化TCP组控制器
  7. 初始化HTTP组控制器
  8. 初始化TCPMux组控制器
  9. 启动TCP监听
  10. 启动KCP监听
  11. 如果QUICBindPort设置,启动quic监听
  12. 如果SSHTunnelGateway设置,启动SSHTunnelGateway监听
  13. 启动websocket监听
  14. 如果VhostHTTPPort设置,启动http反向代理
  15. 如果VhostHTTPSPort设置,启动https反向代理
  16. 启动tls监听
  17. 初始化nat hole服务

之后调用自身的Run方法启动运行。

根据参数多线程启动web GUI

之后启动一系列监听

image-20240826170417271.png

3.1.2 客户端

同样的先处理配置,遵循命令行配置文件优先,没有的就使用默认。

image-20240826170821460.png

之后开始启动客户端服务。

先调用client.NewService初始化客户端服务

image-20240826171544956.png

调用svr.Run启动服务

  1. 先设置DNS服务
  2. 根据WebServer确定是否开启web端
  3. 登录客户端,就是根据配置在服务端新建一个连接。
  4. 多线程启动保持控制连接keepControllerWorking(),保持代理一直处于工作状态

keepControllerWorking

wait.BackoffUntil:函数用于使用指数退避策略重复执行一个函数,直到满足某个条件或超时。它接受四个参数:

第一个参数:是一个匿名函数,也是重复执行的函数。

第二个参数:创建一个新的退避管理器实例,并指定相关选项。

第三个参数:一个布尔值,指示是否应立即执行退避,或在第一次失败后执行。

第四个参数:一个通道,当与服务器(svr)关联的上下文被取消时关闭,表示应该中止操作。

image-20240828201634711.png

3.2 代理线程生命周期

3.2.1 根据协议建立连接

先根据配置初始化连接器,接着打开与服务器的底层连接,之后再生成数据流进行数据传输。

image-20240827194121280.png

3.2.1.1 连接器打开与底层的连接[open()]

底层连接要么是TCP连接,要么是QUIC连接。
底层连接建立后,可以调用Connect()获取流。
如果未启用 TCPMux,则底层连接为零,每次调用 Connect() 时都会获得一个新的真实 TCP 连接。

如果使用Mux(多路复用),返回一个session进行后续的数据交互

**QUIC协议:**QUIC 是一种建立在 UDP 之上的新型多路复用传输。

image-20240827201151200.png

建立连接的时候,主要需要

  • 是否启用TLS
  • 使用websocketWSS或者默认协议
  • 配置选项:协议、超时时间、心跳时间、代理类型、服务端地址、认证信息

image-20240827202050220.png

WebSocket

**设定协议:**WebSocket 是基于 TCP 的,所以协议被设置为 "tcp"。
**添加 WebSocket 钩子:**处理连接时将其升级为 WebSocket 连接。
**添加自定义 TLS 钩子:**根据配置进行 TLS 头部字节的处理(通常用于协议验证或调试)。
**配置 TLS:**将 TLS 配置应用到连接中。

WebSocket Secure (WSS)wss 是 WebSocket 协议的加密版本,使用 TLS(类似于 HTTPS)。

设定协议: 使用 TCP 作为基础协议。

添加 TLS 配置钩子: 优先处理 TLS 配置,确保连接加密。

添加 WebSocket 钩子: 在 TLS 连接成功建立后,再处理 WebSocket 连接的升级。

image-20240828110021676.png

image-20240827202128918.png

建立连接这里也称为拨号(dial),如果设置了指定的拨号器(这里的自定义拨号器都是应用层协议),就使用对应的协议类型

image-20240827203016849.png

如果没指定拨号器,默认使用TCPkcp

KCP是一种快速而可靠的协议,可以达到平均延迟降低30%~40%,最大延迟降低3倍的传输效果,但代价是比TCP多浪费10%~20%的带宽。

KCP 模式使用 UDP 作为底层传输。

image-20240828101904746.png

如果前边的流程都没有问题,最终open()把新生成的如下这样一个Session对象赋值给连接上下文对象的muxSession

image-20240828114536708.png

3.2.1.2 生成交互流连接[Connect()]

Connect 从底层连接返回一个流,如果未启用 TCPMux,则返回一个新的 TCP 连接。

image-20240828150232429.png

3.2.2 客户端代理认证登录

为了安全性,客户端和服务端正常工作是需要认证的,先初始化登录数据结构

image-20240828152653447.png

用前边生成的网络流进行客户端认证,成功后跟新客户端代理ID

image-20240828153559485.png

3.2.3 代理控制器

登录成功之后,初始化客户端控制器

image-20240828155818420.png

3.2.3.1 生成新的控制器[NewControl()]

根据客户端上下文和Session上下文初始化控制器

image-20240828164050758.png

如果启用了加密,返回新的加密网络流调度器,未启用加密正常返回流调度器

image-20240828164843339.png

加密算法如下,采用AES加密,key来自配置中的Token

image-20240828165503088.png

接着注册消息处理程序,不同类型的消息由不同的处理器处理

image-20240828174320310.png

传入发送调度器,生成消息发送器

生成新的代理管理器,指向控制器的pm

生成新的访客管理器,指向控制器的vm

image-20240828175115533.png

3.2.3.2 运行控制器[Run()]

开始工作

image-20240828194517459.png

运行调度器,主要就是多线程启动发送池、接收池

image-20240828194902574.png

更新配置

image-20240828182649169.png

更新函数主要有两大块,一块是根据名字删除代理。另一块是添加新的代理,并运行检查。

del

image-20240828190318535.png

Add

image-20240828190520970.png

启动主要是启动工作检查和代理连通性检查

image-20240828193023664.png

到这里就全部运行了。后续就是监控出问题后,结束控制器。

3.3 数据交互过程

3.3.1 请求工作连接

在通道控制器建立后,如果有数据,会通过调度器分配给对应的处理函数。直接定位handleReqWorkConn()方法。

先获取一个网络流,接着初始化一个工作连接结构体,这个结构体主要是商量每个连接的认证信息。通过网络流把该结构体对象发送到服务端,并且接收成功认证连接后,服务端返回的开始信息。然后把代理名、网络流、开始信息当作参数,初始化工作连接句柄

image-20240829103728503.png

3.3.2 代理分配

在代理管理器中,根据名字获取对应的代理包装器

image-20240829104532636.png

在代理包装器中,分配给对应类型的代理

image-20240829104822792.png

默认使用TCP协议处理

image-20240829105033546.png

还有其他几种如下:

image-20240829105308402.png

3.3.3 TCP工作连接处理器

接着来看TCP工作连接的通用处理程序。

设置限制器

设置加密器

设置压缩器

image-20240829110317141.png

构造插件信息

image-20240829124112918.png

接着把插件信息传递给代理插件处理器

image-20240829140427530.png

下边开始把处理两个网络流。生成本地网络流,远程网络流

image-20240829140809471.png

3.3.4 TCP协议数据交换

先创建一个匿名函数,接收四个参数:

  • number 排序、标识
  • to 待写入数据
  • from 待读取数据
  • count 写入/读取大小

把数据从from复制到to

然后创建两个多线程,调用该函数,to、from互换位置。这样随时在任意一端发送数据,同时不用等待即可发送。

image-20240829141636011.png

到这里,数据交互的过程分析完了。但是好像又没分析完,感觉少点什么?通道也有了,数据交换也有了,少点什么那?插件好像没分析,插件是在那个步骤中起到作用了?想起来了吧,少了把数据写入通道的步骤。刚好这个步骤在插件中进行。

3.3.5 插件写入、读取数据-socks5

插件是从handle调用开始的,里边有这么多。挑一个最常用的socks5进行后续分析。

image-20240829162916435.png

先把io读写器和网络流包装到一块,调用ServeConn()函数进一步处理。

image-20240829163347110.png

ServeConn()函数中,先获取版本,对比是不是Socks5

image-20240829164554734.png

接着创建一个新的请求对象,值从bufConn读取器中获取。数据结构如下:

request := &Request{
    Version:  socks5Version,
    Command:  header[1],
    DestAddr: dest,
    bufConn:  bufConn,
}

把新创建的requests对象和conn交给请求处理器处理

image-20240829165138173.png

请求处理器handleRequest(),先获取需要的目的地址格式

image-20240902154007355.png

大家应该都知道,socks5代理建立后,可以任意访问内网的主机。访问不同的主机就需要更换不同的目的地址。

image-20240902154822374.png

有三个命令,实际作者公开源代码的版本只实现了建立连接命令,另外两个功能函数为空。这里建立连接命令就很好理解了,把代理使用者的请求数据发向目的地址。

image-20240902155205879.png

handleConnect()函数中,会先用tcp协议(socks5基于TCP)和目的地址建立连接。

image-20240902160138766.png

启动两个线程,先把发过来的请求发送给目标机,再把目标机的响应发送给vps,这样就达到了高效数据交换的目的。

image-20240902160859623.png

后边就是线程报错的相关处理了。至此SOCKS5代理分析完毕。

四、进行瘦身

瘦身也就是减小编译后的程序的体积,先来分析一下影响体积的因素有哪些?

  • 代码量
  • 编译方法、编译程序
  • 其他

4.1 减少代码量

主要从一下几方面减少代码量:1、 去掉无用代码 2、去掉非必要功能

4.1.1 去掉无用代码

比如注释符、和一些说明,在我们红队使用过程中,都属于无用代码。image-20240903104724244.png

这些是客户端使用到的代码

image-20240903105121387.png

手动一个一个删很显然浪费时间,写了个脚本批量删除

image-20240903110342606.png

再次编译后,和源文件对比,确实小了,但小的不多。

image-20240903110647475.png

4.1.2 去掉非必要功能

前边源码分析的时候,就发现了很多用不到的功能和占位的代码。现在把他们一一去除,看一下效果。

从头开始,这个功能很明显用不到,这行代码,连同函数所在的文件,一块删除。

image-20240903111025167.png

去掉目录方式读取配置文件

image-20240903111455937.png

去掉web UI

image-20240903112132643.png

去掉用不到的插件

image-20240903115217104.png

再次编译,这次少了1M

image-20240903115505648.png

4.2 编译方法

本地未作任何更改的情况下,和GitHub下载的frpc.exe做对比,大小也不一样。

4.3 总结

前边几种方法下来,效果并不明显。这也跟语言特性有关系,go编译的本来就大,要想最大化缩小体积,用C++应该是最好的。

不过这样较小代码量也不是没用,应该可以起到免杀的效果。

  • 发表于 2024-10-24 10:03:05
  • 阅读 ( 2985 )
  • 分类:安全工具

0 条评论

请先 登录 后评论
la0gke
la0gke

9 篇文章

站长统计