问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
FRP源码深度刨析
随着功能逐渐增多,FRP也愈发臃肿,越来越不适用红队项目了,现在客户端已经达到了`14M`,这是奔着产品去了。 红队项目需要短小精悍,体积小,只保留最核心的功能,其他能减则减。 所以学习一下FRP的优点,有机会开发出适合自己使用的工具。
一、什么是 frp? ---------- 下载地址:<https://github.com/fatedier/frp> frp 是一个快速反向代理,可让您将位于 NAT 或防火墙后面的本地服务器暴露到互联网。它目前支持**TCP**和**UDP**,以及**HTTP**和**HTTPS**协议,允许通过域名将请求转发到内部服务。 二、描述 ---- 随着功能逐渐增多,FRP也愈发臃肿,越来越不适用红队项目了,现在客户端已经达到了`14M`,这是奔着产品去了。 红队项目需要短小精悍,体积小,只保留最核心的功能,其他能减则减。 所以学习一下FRP的优点,有机会开发出适合自己使用的工具。 三、源码分析 ------ 先来熟悉一下源码  ### 3.1 整体流程 #### 3.1.1 服务端 先来看一下服务端,先从`cmd`目录开始,处理命令行,初始化配置文件  把配置文件传递给服务,并启动运行。  **`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` 之后启动一系列监听  #### 3.1.2 客户端 同样的先处理配置,遵循命令行配置文件优先,没有的就使用默认。  之后开始启动客户端服务。 先调用`client.NewService`初始化客户端服务  调用`svr.Run`启动服务 1. 先设置DNS服务 2. 根据`WebServer`确定是否开启web端 3. 登录客户端,就是根据配置在服务端新建一个连接。 4. 多线程启动保持控制连接`keepControllerWorking()`,保持代理一直处于工作状态 `keepControllerWorking` `wait.BackoffUntil`:函数用于使用指数退避策略重复执行一个函数,直到满足某个条件或超时。它接受四个参数: > 第一个参数:是一个匿名函数,也是重复执行的函数。 > > 第二个参数:创建一个新的退避管理器实例,并指定相关选项。 > > 第三个参数:一个布尔值,指示是否应立即执行退避,或在第一次失败后执行。 > > 第四个参数:一个通道,当与服务器(`svr`)关联的上下文被取消时关闭,表示应该中止操作。  ### 3.2 代理线程生命周期 #### 3.2.1 根据协议建立连接 先根据配置初始化连接器,接着打开与服务器的底层连接,之后再生成数据流进行数据传输。  ##### 3.2.1.1 连接器打开与底层的连接\[open()\] > 底层连接要么是TCP连接,要么是QUIC连接。 > 底层连接建立后,可以调用Connect()获取流。 > 如果未启用 TCPMux,则底层连接为零,每次调用 Connect() 时都会获得一个新的真实 TCP 连接。 > > 如果使用`Mux(多路复用)`,返回一个`session`进行后续的数据交互 **QUIC协议:**QUIC 是一种建立在 UDP 之上的新型多路复用传输。  建立连接的时候,主要需要 - 是否启用`TLS` - 使用`websocket`、`WSS`或者默认协议 - 配置选项:协议、超时时间、心跳时间、代理类型、服务端地址、认证信息  **WebSocket** > **设定协议:**WebSocket 是基于 TCP 的,所以协议被设置为 "tcp"。 > **添加 WebSocket 钩子:**处理连接时将其升级为 WebSocket 连接。 > **添加自定义 TLS 钩子:**根据配置进行 TLS 头部字节的处理(通常用于协议验证或调试)。 > **配置 TLS:**将 TLS 配置应用到连接中。 **WebSocket Secure (`WSS`)** 。`wss` 是 WebSocket 协议的加密版本,使用 TLS(类似于 HTTPS)。 > **设定协议**: 使用 TCP 作为基础协议。 > > **添加 TLS 配置钩子**: 优先处理 TLS 配置,确保连接加密。 > > **添加 WebSocket 钩子**: 在 TLS 连接成功建立后,再处理 WebSocket 连接的升级。   建立连接这里也称为`拨号(dial)`,如果设置了指定的拨号器(这里的自定义拨号器都是应用层协议),就使用对应的协议类型  如果没指定拨号器,默认使用`TCP`或`kcp` > KCP是一种快速而可靠的协议,可以达到平均延迟降低30%~40%,最大延迟降低3倍的传输效果,但代价是比TCP多浪费10%~20%的带宽。 > > KCP 模式使用 UDP 作为底层传输。  如果前边的流程都没有问题,最终`open()`把新生成的如下这样一个`Session对象`赋值给连接上下文对象的`muxSession`  ##### 3.2.1.2 生成交互流连接\[Connect()\] Connect 从底层连接返回一个流,如果未启用 TCPMux,则返回一个新的 TCP 连接。  #### 3.2.2 客户端代理认证登录 为了安全性,客户端和服务端正常工作是需要认证的,先初始化登录数据结构  用前边生成的网络流进行客户端认证,成功后跟新客户端代理ID  #### 3.2.3 代理控制器 登录成功之后,初始化客户端控制器  ##### 3.2.3.1 生成新的控制器\[NewControl()\] 根据客户端上下文和`Session`上下文初始化控制器  如果启用了加密,返回新的加密网络流调度器,未启用加密正常返回流调度器  加密算法如下,采用AES加密,key来自配置中的`Token`  接着注册消息处理程序,不同类型的消息由不同的处理器处理  传入发送调度器,生成消息发送器 生成新的代理管理器,指向控制器的pm 生成新的访客管理器,指向控制器的vm  ##### 3.2.3.2 运行控制器\[Run()\] **开始工作**  运行调度器,主要就是多线程启动发送池、接收池  **更新配置**  更新函数主要有两大块,一块是根据名字删除代理。另一块是添加新的代理,并运行检查。 del  Add  启动主要是启动工作检查和代理连通性检查  到这里就全部运行了。后续就是监控出问题后,结束控制器。 ### 3.3 数据交互过程 #### 3.3.1 请求工作连接 在通道控制器建立后,如果有数据,会通过调度器分配给对应的处理函数。直接定位`handleReqWorkConn()`方法。 先获取一个网络流,接着初始化一个工作连接结构体,这个结构体主要是商量每个连接的认证信息。通过网络流把该结构体对象发送到服务端,并且接收成功认证连接后,服务端返回的开始信息。然后把代理名、网络流、开始信息当作参数,初始化工作连接句柄  #### 3.3.2 代理分配 在代理管理器中,根据名字获取对应的代理包装器  在代理包装器中,分配给对应类型的代理  默认使用TCP协议处理  还有其他几种如下:  #### 3.3.3 TCP工作连接处理器 接着来看`TCP工作连接`的通用处理程序。 > 设置限制器 > > 设置加密器 > > 设置压缩器  > 构造插件信息  接着把插件信息传递给代理插件处理器  下边开始把处理两个网络流。生成本地网络流,远程网络流  #### 3.3.4 TCP协议数据交换 先创建一个匿名函数,接收四个参数: - number 排序、标识 - to 待写入数据 - from 待读取数据 - count 写入/读取大小 把数据从`from`复制到`to`。 然后创建两个多线程,调用该函数,to、from互换位置。这样随时在任意一端发送数据,同时不用等待即可发送。  到这里,数据交互的过程分析完了。但是好像又没分析完,感觉少点什么?通道也有了,数据交换也有了,少点什么那?插件好像没分析,插件是在那个步骤中起到作用了?想起来了吧,少了把数据写入通道的步骤。刚好这个步骤在插件中进行。 #### 3.3.5 插件写入、读取数据-socks5 插件是从`handle`调用开始的,里边有这么多。挑一个最常用的`socks5`进行后续分析。  先把`io读写器`和网络流包装到一块,调用`ServeConn()`函数进一步处理。  在`ServeConn()`函数中,先获取版本,对比是不是`Socks5`  接着创建一个新的请求对象,值从`bufConn`读取器中获取。数据结构如下: ```go request := &Request{ Version: socks5Version, Command: header[1], DestAddr: dest, bufConn: bufConn, } ``` 把新创建的`requests`对象和`conn`交给请求处理器处理  请求处理器`handleRequest()`,先获取需要的目的地址格式  大家应该都知道,`socks5`代理建立后,可以任意访问内网的主机。访问不同的主机就需要更换不同的目的地址。  有三个命令,实际作者公开源代码的版本只实现了建立连接命令,另外两个功能函数为空。这里建立连接命令就很好理解了,把代理使用者的请求数据发向目的地址。  在`handleConnect()`函数中,会先用`tcp协议`(socks5基于TCP)和目的地址建立连接。  启动两个线程,先把发过来的请求发送给目标机,再把目标机的响应发送给vps,这样就达到了高效数据交换的目的。  后边就是线程报错的相关处理了。至此SOCKS5代理分析完毕。 四、进行瘦身 ------ 瘦身也就是减小编译后的程序的体积,先来分析一下影响体积的因素有哪些? - 代码量 - 编译方法、编译程序 - 其他 ### 4.1 减少代码量 主要从一下几方面减少代码量:1、 去掉无用代码 2、去掉非必要功能 #### 4.1.1 去掉无用代码 比如注释符、和一些说明,在我们红队使用过程中,都属于无用代码。 这些是客户端使用到的代码  手动一个一个删很显然浪费时间,写了个脚本批量删除  再次编译后,和源文件对比,确实小了,但小的不多。  #### 4.1.2 去掉非必要功能 前边源码分析的时候,就发现了很多用不到的功能和占位的代码。现在把他们一一去除,看一下效果。 从头开始,这个功能很明显用不到,这行代码,连同函数所在的文件,一块删除。  去掉目录方式读取配置文件  去掉web UI  去掉用不到的插件  再次编译,这次少了`1M`  ### 4.2 编译方法 本地未作任何更改的情况下,和GitHub下载的`frpc.exe`做对比,大小也不一样。 ### 4.3 总结 前边几种方法下来,效果并不明显。这也跟语言特性有关系,go编译的本来就大,要想最大化缩小体积,用`C++`应该是最好的。 不过这样较小代码量也不是没用,应该可以起到免杀的效果。
发表于 2024-10-24 10:03:05
阅读 ( 2610 )
分类:
安全工具
0 推荐
收藏
0 条评论
请先
登录
后评论
Harper Scott
9 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!