Chisel-1.9.1逆向分析
Chisel-1.9.1逆向分析
前言
为了写针对Chisel的内网穿透工具的流量报警,这里对该工具进行流量分析和逆向分析
逆向
解析命令行
1 | version := flag.Bool("version", false, "") |
这段代码用于处理命flag
包的作用是帮助Go程序处理命令行参数和选项。
flag
包的作用是帮助Go程序处理命令行参数和选项。它允许程序员定义各种命令行标志,然后在运行程序时根据传递的参数来设置这些标志的值。这有助于使命令行工具更加灵活,并且能够接受用户提供的参数来控制程序的行为。
一旦使用flag.Parse()
解析了命令行参数,程序就可以根据这些参数来执行不同的逻辑。
1 | args := flag.Args() |
这段代码用于获取在命令行参数之后的非标志(non-flag)参数列表。在Go的flag
包中,命令行参数通常分为标志(flags)和非标志参数。
- 标志(flags)是那些以
-
或--
开头的参数,通常用于传递配置选项和参数值。例如,-version
或--verbose
。 - 非标志参数是那些没有标志前缀的参数,它们通常用于传递操作对象或其他非配置性参数。这些参数按照它们在命令行中出现的顺序保存在
flag.Args()
返回的字符串切片中。
1 | subcmd := "" |
这段代码首先对第一个参数进行判断是否为server
或者client
,如果是则将后面的命令行参数当成参数传递给对应的函数,否则打印help
字符
server函数
解析命令行
首先解析传入的参数
1 | flags := flag.NewFlagSet("server", flag.ContinueOnError) |
将结构体里面的一些变量与命令行参数进行连接,和本地的一些局部变量进行连接,最后通过flags.parse(arfs)
进行解析,Config结构体定义了服务器的一些设置,结构体如下
1 | // Config is the configuration for the chisel service |
密钥初始化和解析其他选项
1 | if *keyGen != "" { |
这段代码的作用是检查用户是否请求生成密钥文件,如果是,则生成密钥文件并退出程序。这通常用于初始化或更新程序所需的密钥文件。
更据后面的输出消息可以知道选项 --key
已弃用,并将在 chisel 的未来版本中删除。请使用chisel server --keygen /file/path
,后跟chisel server --keyfile /file/path
指定SSH私钥
1 | if *host == "" { |
设置端口和IP,如果不设置,则默认为0.0.0.0:8080。
1 | if config.KeyFile == "" { |
总结起来,这段代码的目的是在用户没有在命令行参数中指定密钥文件路径 (KeyFile
) 或密钥的种子 (KeySeed
) 的情况下,尝试从环境变量中获取这些值作为默认值,以便程序在没有显式配置这些参数的情况下仍然能够正常运行。如果环境变量中也没有定义这些值,那么这些字段仍然会保持为空。
然后调用NewServer函数
去根据config结构体
初始化服务器,最后调用StartContext
函数
1 | func generatePidFile() { |
总的来说,这段代码的目的是启动Chisel服务器并管理它的运行。它可以处理调试模式、生成PID文件、收集性能统计信息以及处理服务器的启动和关闭。
NewServer函数
1 | server := &Server{ |
总的来说,这段代码用于初始化Chisel服务器的配置,包括日志记录、用户管理和身份验证配置。它确保服务器在启动时具有必要的配置信息,以便能够接受连接并验证用户身份。
首先判断是否有AuthFile
,Auth
这两个参数,如果有则解析对应的Authfile文件或者Auth所携带的name
和pass
。
server := &Server{
config: c,
httpServer: cnet.NewHTTPServer(),
Logger: cio.NewLogger("server"),
sessions: settings.NewUsers(),
}
这段代码表示实现一个server服务器实例,然后对对应服务进行初始化
server := &Server{...}
:创建一个名为server
的Server
结构体实例。这是Chisel服务器的主要配置和运行对象。server.config = c
:将传递给函数的配置c
赋值给server
结构体的config
字段,以便后续使用。server.httpServer = cnet.NewHTTPServer()
:创建一个新的 HTTP 服务器实例,并将其分配给server
结构体的httpServer
字段。这将用于处理HTTP连接。server.Logger = cio.NewLogger("server")
:创建一个名为 “server” 的日志记录器,并将其分配给server
结构体的Logger
字段。这用于记录服务器的日志消息。server.sessions = settings.NewUsers()
:创建一个新的用户集合实例,并将其分配给server
结构体的sessions
字段。这用于跟踪与服务器建立的会话。server.Info = true
:设置server
结构体的Info
字段为true
,表示服务器应该记录详细的信息日志。server.users = settings.NewUserIndex(server.Logger)
:创建一个新的用户索引实例,并将其分配给server
结构体的users
字段。这用于管理服务器允许连接的用户。
配置ssh密钥
1 | var pemBytes []byte |
这段代码的主要目的是获取Chisel服务器所需的私钥,并确保它以PEM编码形式可用。如果密钥文件已经包含了PEM编码的密钥,它会直接使用,否则会根据种子生成新的私钥并进行PEM编码。无论哪种情况,最终的私钥都以PEM编码形式存储在 pemBytes
变量中,供服务器使用。
1 | //convert into ssh.PrivateKey |
总之,这段代码负责配置Chisel服务器的SSH密钥和(如果已配置)反向代理。密钥用于SSH连接的身份验证,而反向代理用于转发传入的连接到其他主机。
private, err := ssh.ParsePrivateKey(pemBytes)
:将之前生成的PEM编码形式的密钥pemBytes
解析为SSH私钥。如果解析失败,将记录错误并终止程序。server.fingerprint = ccrypto.FingerprintKey(private.PublicKey())
:计算SSH密钥的指纹(fingerprint)并将其存储在服务器结构体的fingerprint
字段中。指纹通常用于验证密钥的唯一性。server.sshConfig = &ssh.ServerConfig{ ... }
:创建SSH服务器配置,其中包括以下设置:ServerVersion
:指定SSH服务器的版本信息,这里使用Chisel的协议版本。PasswordCallback
:指定一个回调函数server.authUser
,用于验证SSH用户的用户名和密码组合。AddHostKey(private)
:将之前解析的私钥private
添加为SSH服务器的主机密钥,以用于SSH连接。
if c.Proxy != "" { ... }
:检查是否已配置代理。如果已配置代理,执行以下操作:u, err := url.Parse(c.Proxy)
:解析代理URL,将其存储在u
变量中。如果解析失败,将记录错误并终止程序。if u.Host == "" { ... }
:检查代理URL是否包含主机信息,如果不包含,将返回一个错误消息。server.reverseProxy = httputil.NewSingleHostReverseProxy(u)
:创建一个反向代理,将传入的HTTP请求代理到指定的URLu
。server.reverseProxy.Director
:配置反向代理的Director函数,用于处理传入请求。此函数将强制设置请求的URL方案和主机,以确保请求正确代理到目标主机。
user-password callback函数
上面出现了校验user和pass的函数
1 | func (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { |
这个函数的作用是验证SSH用户的身份和密码组合。如果用户列表为空,表示用户认证未启用,允许所有连接。否则,它会检查用户名和密码是否匹配,并在成功认证时将用户信息存储在会话映射中。
果用户成功认证,将用户信息插入到用户会话映射中。这个映射用于跟踪用户会话。
1 | //print when reverse tunnelling is enabled |
如果有Reverse这个选项,直接打印就行
StartContext函数
接下来看startContext
函数
1 | ctx := cos.InterruptContext() |
这三段代码一起协同工作,用于启动和运行服务器以提供服务。以下是它们的详细解释:
第一段代码
cos.InterruptContext()
创建了一个用于处理中断信号(例如Ctrl+C)的上下文对象ctx
,这样服务器可以在接收到中断信号时优雅地关闭。s.StartContext(ctx, *host, *port)
调用了Server
结构体的StartContext
方法,传递了上下文ctx
、主机host
和端口port
作为参数,开始启动服务器。- 如果启动服务器时出现错误,
log.Fatal(err)
将记录错误信息并终止程序。
第二段代码
- 这个方法首先记录服务器的指纹信息和是否启用了用户认证和反向代理。
- 然后,它通过调用
s.listener(host, port)
创建监听器,该监听器绑定到指定的主机和端口。 - 接下来,它创建一个HTTP请求处理程序,将请求传递给
handleClientHandler
方法进行处理。如果服务器的调试模式已启用,将包装处理程序以记录请求和响应日志。 - 最后,它调用
s.httpServer.GoServe(ctx, l, h)
来启动HTTP服务器,使用传递的上下文ctx
、监听器l
和HTTP处理程序h
。这将使服务器开始监听传入的连接,处理HTTP请求。
第三段代码
s.httpServer.Wait()
是等待HTTP服务器关闭的操作。当HTTP服务器启动后,它将一直运行并监听传入连接,直到某个条件满足,例如通过调用服务器的关闭方法或发生错误。s.httpServer.Wait()
返回一个错误,表示等待过程中发生的任何错误。如果没有发生错误,它将返回nil
,否则返回一个描述错误的错误对象。
GoServer函数
1 | func (h *HTTPServer) GoServe(ctx context.Context, l net.Listener, handler http.Handler) error { |
总的来说,这段代码的作用是启动HTTP服务器,同时处理HTTP请求,并使用上下文对象来支持启动和关闭操作的管理。它使用了 errgroup
来跟踪HTTP服务器的运行状态和错误。
if ctx == nil { return errors.New("ctx must be set") }
:首先检查传入的上下文对象ctx
是否为空,如果为空则返回一个错误,要求上下文对象必须设置。h.waiterMux.Lock()
和defer h.waiterMux.Unlock()
:这两行代码用于加锁和解锁h.waiterMux
,这是一个互斥锁,用于保护多个goroutine同时访问HTTPServer
结构体的相关字段。h.Handler = handler
:将传入的handler
赋值给HTTPServer
结构体的Handler
字段,以指定处理HTTP请求的处理程序。h.waiter, ctx = errgroup.WithContext(ctx)
:使用errgroup.WithContext
函数创建一个错误组,并将传入的上下文对象ctx
分配给h.waiter
,以便在启动HTTP服务器时跟踪错误。h.waiter.Go(func() error { return h.Serve(l) })
:使用errgroup
的Go
方法启动一个新的goroutine,该goroutine将调用HTTPServer
结构体的Serve
方法来启动HTTP服务器,并将l
作为参数传递给Serve
方法。go func() { <-ctx.Done(); h.Close() }()
:在新的goroutine中,启动一个匿名函数,该函数在ctx.Done()
通道关闭时调用h.Close()
方法来关闭HTTP服务器。return nil
:最后,返回nil表示成功启动HTTP服务器。
handleClientHandler函数
1 | // handleClientHandler is the main http websocket handler for the chisel server |
由注释可以看出handleClientHandler
是chisel
服务器的主要http websocket处理程序
这段代码的作用是根据 HTTP 请求的内容和头部信息来处理不同类型的请求,包括 WebSocket 连接、反向代理、健康检查和版本信息。
该处理程序负责处理传入的 HTTP 请求,并根据请求的不同内容执行不同的操作。
func (s *Server) handleClientHandler(w http.ResponseWriter, r *http.Request)
:这是Server
结构体中的一个方法,用于处理传入的 HTTP 请求。它接受两个参数,w
表示 HTTP 响应写入器,r
表示 HTTP 请求对象。upgrade := strings.ToLower(r.Header.Get("Upgrade"))
和protocol := r.Header.Get("Sec-WebSocket-Protocol")
:这两行代码分别从 HTTP 请求头中获取Upgrade
和Sec-WebSocket-Protocol
字段的值,并将它们转换为小写。这些字段通常用于检测是否存在 WebSocket 连接和 WebSocket 协议版本。if upgrade == "websocket"
:这是一个条件语句,用于检查是否存在 WebSocket 升级请求。if protocol == chshare.ProtocolVersion
:这是另一个条件语句,用于检查 WebSocket 协议版本是否与预期的版本(chshare.ProtocolVersion
)匹配。s.handleWebsocket(w, r)
:如果升级请求是 WebSocket 且协议版本匹配,那么调用s.handleWebsocket
方法来处理 WebSocket 连接。s.Infof("ignored client connection using protocol '%s', expected '%s'", protocol, chshare.ProtocolVersion)
:如果 WebSocket 协议版本不匹配,记录一条信息到服务器日志,指示客户端连接使用了不匹配的协议版本。if s.reverseProxy != nil
:这是另一个条件语句,用于检查是否存在反向代理配置。s.reverseProxy.ServeHTTP(w, r)
:如果存在反向代理配置,则将请求转发给反向代理处理。switch r.URL.Path
:这是一个switch
语句,用于根据请求的 URL 路径执行不同的操作。case "/health":
:如果请求的路径是 “/health”,则返回 “OK\n” 表示健康检查通过。case "/version":
:如果请求的路径是 “/version”,则返回 chisel 的版本号。default:
:如果请求的路径不匹配任何已知路径,返回状态码 404 表示未找到资源,并返回 “Not found”。
handleWebsocket函数
在上面的handleClientHandler函数中对于websocket的数据包交给了这个函数进行处理,从函数的注释中也可以看出来。
1 | //handleWebsocket负责处理websocket连接 |
这段代码的主要作用是将 WebSocket 连接升级为 SSH 连接,并根据客户端请求的配置信息进行验证和隧道传输的设置,以便在客户端和服务器之间进行安全通信。
func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request)
:这是Server
结构体中的一个方法,用于处理 WebSocket 连接。它接受两个参数,w
表示 HTTP 响应写入器,req
表示 HTTP 请求对象。id := atomic.AddInt32(&s.sessCount, 1)
:这一行代码用于为每个 WebSocket 连接生成一个唯一的会话标识符。会话计数器s.sessCount
是一个原子计数器,每次调用AddInt32
都会增加其值。l := s.Fork("session#%d", id)
:这一行代码创建一个新的日志记录器(Logger),用于记录与当前 WebSocket 会话相关的日志。Fork
方法会根据提供的前缀和会话标识符创建一个新的日志记录器。wsConn, err := upgrader.Upgrade(w, req, nil)
:这一行代码使用upgrader
将 HTTP 连接升级为 WebSocket 连接,并返回一个 WebSocket 连接对象wsConn
。如果升级失败,将会返回一个错误。conn := cnet.NewWebSocketConn(wsConn)
:这一行代码创建一个WebSocketConn
对象,用于封装 WebSocket 连接。l.Debugf("Handshaking with %s...", req.RemoteAddr)
:这一行代码记录一个调试级别的日志,表示正在进行 WebSocket 连接的握手。sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)
:这一行代码使用 SSH 包中的NewServerConn
函数,将 WebSocket 连接升级为 SSH 连接,并返回 SSH 连接对象sshConn
、通道(channel)和请求(request)。var user *settings.User
:这一行代码声明了一个变量user
,用于存储连接的用户信息。if s.users.Len() > 0
:这是一个条件语句,用于检查是否存在用户验证功能。sid := string(sshConn.SessionID())
:这一行代码获取 SSH 连接的会话 ID,用于从会话映射中获取用户信息。u, ok := s.sessions.Get(sid)
:这一行代码从会话映射中获取与会话 ID 相关联的用户信息。user = u
:如果找到与会话 ID 相关联的用户信息,则将其分配给变量user
。s.sessions.Del(sid)
:从会话映射中删除已经使用的会话信息。select
语句:这个select
语句用于等待 SSH 客户端发送的请求,包括配置请求和其他类型的请求。case r = <-reqs:
:如果收到了一个请求,将其分配给变量r
。case <-time.After(settings.EnvDuration("CONFIG_TIMEOUT", 10*time.Second)):
:如果在指定的时间内未收到请求,将会触发超时。failed := func(err error)
:这是一个匿名函数,用于记录连接失败的详细信息,并回复请求。if r.Type != "config"
:这是一个条件语句,用于检查请求的类型是否为配置请求。c, err := settings.DecodeConfig(r.Payload)
:这一行代码用于解码客户端发送的配置信息,并将其存储在变量c
中。if r.Type != "config"
和c, err := settings.DecodeConfig(r.Payload)
:如果请求类型不是配置请求或配置信息无效,则触发失败函数,记录失败信息。l.Infof("Client version (%s) differs from server version (%s)", v, chshare.BuildVersion)
:如果客户端版本与服务器版本不匹配,记录一条信息到服务器日志,指示版本不一致。for _, r := range c.Remotes
:这是一个循环,用于迭代客户端发送的所有远程配置项。addr := r.UserAddr()
:获取远程配置项的地址信息。if !user.HasAccess(addr)
:如果存在用户验证并且用户没有访问权限,触发失败函数,拒绝访问。if r.Reverse && !s.config.Reverse
:如果客户端请求了反向端口转发,但服务器没有启用反向模式,则触发失败函数,拒绝请求。if r.Reverse && !r.CanListen()
:如果客户端请求的反向端口转发无法在服务器上监听,则触发失败函数,拒绝请求。r.Reply(true, nil)
:如果所有验证都通过,回复客户端请求,表示验证成功。tunnel := tunnel.New(tunnel.Config{...})
:创建一个名为tunnel
的隧道对象,用于处理 SSH 隧道的传输。eg, ctx := errgroup.WithContext(req.Context())
:创建一个errgroup
对象,用于管理多个并发任务,并传入请求的上下文。eg.Go(func() error { ... })
:使用errgroup
启动一个协程,用于处理 SSH 连接的绑定和隧道的传输。eg.Go(func() error { ... })
:启动另一个协程,用于处理反向隧道的绑定(如果存在)。err = eg.Wait()
:等待所有并发任务完成,如果出现错误(除了正常关闭连接时的 “EOF” 错误),则记录错误日志。
client函数
解析命令行
首先解析传入的参数
1 | flags := flag.NewFlagSet("client", flag.ContinueOnError) |
和server一样的,将结构体里面的一些变量与命令行参数进行连接,和本地的一些局部变量进行连接,最后通过flags.parse(arfs)
进行解析,Config结构体定义了服务器的一些设置,但是结构体不一样,结构体如下
1 | // Config represents a client configuration |
启动客户端初始化
1 | c, err := chclient.NewClient(&config) |
这段代码用与启动一个client客户端的初始化,根据Config
结构体去初始化客户端,然后运行Start函数
NewClient函数
1 | // NewClient creates a new client instance |
最终,函数返回一个新的Client
实例作为结果,如果在创建过程中发生任何错误,将返回相应的错误信息。这个函数的主要作用是根据配置信息创建一个客户端实例,并进行一些配置参数的处理和验证。
func NewClient(c *Config) (*Client, error) { ... }
:这是一个名为NewClient
的函数,它接受一个*Config
类型的参数c
,并返回一个指向Client
类型的指针和一个错误。这个函数用于创建一个新的客户端实例。if !strings.HasPrefix(c.Server, "http") { ... }
:这个条件语句检查配置中的服务器地址c.Server
是否以”http”或”https”开头。如果不是的话,它会自动在地址前添加”http://“,以确保服务器地址的正确格式。if c.MaxRetryInterval < time.Second { ... }
:这个条件语句检查配置中的最大重试间隔c.MaxRetryInterval
是否小于1秒。如果小于1秒,它将最大重试间隔设置为5分钟,以确保不会太频繁地进行重试。u, err := url.Parse(c.Server)
:这一行代码使用Go标准库中的url.Parse
函数来解析服务器地址c.Server
,并将结果存储在变量u
中。同时,它也会检查是否有解析错误,并将错误存储在变量err
中。u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1)
:这一行代码将解析得到的URL的协议方案从”http”替换为”ws”,以便后续使用WebSocket连接。if !regexp.MustCompile(
:\d+$).MatchString(u.Host) { ... }
:这个条件语句检查解析得到的URL的主机部分是否包含端口号。如果不包含端口号,则根据协议方案添加默认端口号(80或443)。hasReverse := false
、hasSocks := false
、hasStdio := false
:这些变量用于标记配置中是否包含反向隧道、Socks代理或标准输入/输出通道。- 创建一个
Client
结构体实例,并设置了各种字段,包括日志记录器、配置信息、服务器地址、TLS配置等。 for _, s := range c.Remotes { ... }
:这个循环迭代配置中的远程设置,对每个远程设置进行解码和验证,并根据其类型设置相关标志。if p := c.Proxy; p != "" { ... }
:如果配置中指定了代理服务器地址,则解析代理URL并存储在client.proxyURL
中。user, pass := settings.ParseAuth(c.Auth)
:这一行代码解析配置中的身份验证信息,将用户名和密码提取出来,然后设置SSH客户端配置。- 创建一个
Client
结构体实例的tunnel
字段,用于处理隧道相关的配置。
start函数
1 | // Start client and does not block |
总的来说,这段代码的作用是启动客户端并尝试连接到chisel服务器,同时监听本地的Socket连接(如果需要)。它使用goroutines来并发执行这些操作,不会阻塞当前线程,并可以在需要时取消。连接和监听操作的详细细节在匿名函数中执行,需要查看匿名函数内部的代码来了解具体的实现。
func (c *Client) Start(ctx context.Context) error { ... }
:这是一个Client
结构体的方法,用于启动客户端并连接到服务器,但不会阻塞当前线程。它接受一个context.Context
类型的参数ctx
,并返回一个错误。ctx, cancel := context.WithCancel(ctx)
:这一行代码创建了一个新的上下文ctx
,并返回一个cancel
函数,用于在需要时取消这个上下文。c.stop = cancel
:这一行代码将cancel
函数赋值给c
结构体的stop
字段,以便稍后可以使用它来停止客户端。eg, ctx := errgroup.WithContext(ctx)
:这一行代码使用errgroup
包创建一个新的errgroup.Group
实例,同时基于传入的上下文ctx
创建一个新的上下文ctx
。errgroup
是一个用于处理一组goroutine的包,可以等待它们全部完成或任何一个失败。via := ""
、if c.proxyURL != nil { ... }
:这些行代码用于创建一个字符串via
,其中包含代理服务器的信息,如果c.proxyURL
不为nil
,则将代理服务器的URL添加到via
字符串中。c.Infof("Connecting to %s%s\n", c.server, via)
:这一行代码使用c
结构体的日志记录器打印连接服务器的信息,包括服务器地址和(如果存在)代理服务器信息。eg.Go(func() error { ... })
:这一行代码启动一个goroutine,其中包含一个匿名函数,用于执行连接到chisel服务器的操作。这个匿名函数返回连接操作的错误(如果有)。eg.Go(func() error { ... })
:这一行代码启动另一个goroutine,其中包含一个匿名函数,用于监听本地的Socket连接。它会检查客户端配置中是否有需要监听的Socket连接,如果有的话,会将这些连接绑定到chisel服务器上。如果没有需要监听的Socket连接,则不执行任何操作。return nil
:最后,这个方法返回nil
,表示启动过程不会返回错误。这意味着该方法会立即返回,而不会阻塞。
connectionLoop函数
1 | func (c *Client) connectionLoop(ctx context.Context) error { |
这段代码是Client
结构体中的一个方法,名为connectionLoop
。它的主要作用是在一个循环中尝试连接到chisel服务器,如果连接失败,根据一定策略进行重试,同时记录连接尝试的次数和错误信息。
b := &backoff.Backoff{Max: c.config.MaxRetryInterval}
:创建一个backoff.Backoff
的实例b
,并设置最大的重试间隔为c.config.MaxRetryInterval
,这是在客户端配置中定义的最大重试间隔。for { ... }
:这是一个无限循环,表示不断尝试连接到chisel服务器。connected, err := c.connectionOnce(ctx)
:调用c.connectionOnce(ctx)
方法,该方法尝试一次连接到chisel服务器,并返回两个值,connected
表示是否成功连接,err
表示连接中出现的错误。if connected { ... }
:如果成功连接到服务器,重置backoff
实例b
,以便在下一次连接尝试时使用最小的重试间隔。attempt := int(b.Attempt())
:获取当前的连接尝试次数,这是backoff
实例的属性。maxAttempt := c.config.MaxRetryCount
:从客户端配置中获取最大重试次数。- 如果
err
表示了关闭网络连接的错误(”use of closed network connection”),则将err
重置为io.EOF
,以避免打印此类错误。 - 如果
err
不为空且不是io.EOF
,则打印连接错误消息,包括错误信息和连接尝试次数。 - 如果达到了最大连接尝试次数,并且
maxAttempt
大于等于0(表示有限次数的重试),则打印”Give up”消息,表示放弃连接,并退出循环。 - 计算下一次重试的等待时间
d
,使用backoff
实例的Duration
方法来获取。 - 打印”Retrying in %s…”消息,其中
%s
会被替换为等待时间d
的字符串表示。 - 使用
select
语句等待以下两种事件中的任何一个发生:- 当前goroutine等待
d
时间后继续执行,表示进行下一次连接尝试。 - 当前上下文
ctx
被取消(可能是由于外部要求取消连接),则打印”Cancelled”消息,并返回nil
表示取消连接尝试,然后退出循环。
- 当前goroutine等待
- 最后,如果退出循环,调用
c.Close()
方法来关闭客户端的连接,然后返回nil
。
connectionOnce函数
1 | // connectionOnce connects to the chisel server and blocks |
总的来说,这个方法的作用是进行一次连接到chisel服务器的尝试,包括WebSocket握手、SSH握手和与服务器的通信。如果连接成功,返回true
和nil
;如果连接失败,返回false
和相应的错误信息。这个方法通常由connectionLoop
方法调用,用于进行连接的具体实现。
数据传输部分
创建连接
BindSSH函数
1 | //BindSSH provides an active SSH for use for tunnelling |
总的来说,这段代码的主要作用是为SSH隧道提供一个活跃的SSH连接,通过启动多个goroutines来处理SSH请求和通道,以及定期发送ping请求以保持SSH连接的活跃状态。连接关闭后,返回连接关闭时的错误。这个方法用于建立和维护SSH连接,以便进行数据传输。
func (t *Tunnel) BindSSH(ctx context.Context, c ssh.Conn, reqs <-chan *ssh.Request, chans <-chan ssh.NewChannel) error { ... }
:这是一个Tunnel
结构体的方法,用于为SSH隧道提供一个活跃的SSH连接,接受三个参数:上下文ctx
、SSH连接c
、SSH请求通道reqs
和SSH新通道通道chans
。go func() { ... }()
:这是一个goroutine,用于将上下文ctx
与SSH连接c
关联起来。当上下文ctx
被取消时,它会尝试关闭SSH连接c
,并通知调试信息表明SSH连接被取消。同时,它调用t.activatingConn.DoneAll()
来减少激活连接计数,表示连接已完成激活。t.activeConnMut.Lock()
:获取Tunnel
结构体中的activeConnMut
互斥锁,用于保护对activeConn
字段的并发访问。if t.activeConn != nil { ... }
:检查activeConn
字段是否已经被设置,如果已经设置,则抛出panic
,表示不应该出现多次绑定SSH连接。t.activeConn = c
:将SSH连接c
设置为activeConn
字段,表示当前SSH连接是活跃的。t.activeConnMut.Unlock()
:释放activeConnMut
互斥锁。t.activatingConn.Done()
:通知t.activatingConn
计数减少一个,表示SSH连接已完成激活。- 如果配置中指定了保持活动的时间间隔
t.Config.KeepAlive > 0
,则启动一个goroutine,调用t.keepAliveLoop(c)
来定期发送ping请求以保持SSH连接的活跃状态。 - 启动两个goroutines,一个用于处理SSH请求通道
reqs
,另一个用于处理SSH新通道通道chans
。这两个goroutines将负责处理SSH连接的请求和通道创建。 - 打印调试信息表示SSH连接已连接成功。
err := c.Wait()
:阻塞等待SSH连接的关闭,一旦连接关闭,将返回一个错误(如果有错误的话)。- 打印调试信息表示SSH连接已断开。
- 重新增加
t.activatingConn
计数,表示SSH连接已完成激活。 - 获取
activeConnMut
互斥锁,将activeConn
字段重置为nil
,表示SSH连接不再活跃。 - 返回连接关闭时的错误(如果有错误的话)。
BindRemotes函数
1 | //BindRemotes converts the given remotes into proxies, and blocks |
总的来说,这段代码的主要作用是将给定的远程配置转换为代理对象,并在后台运行这些代理对象,用于处理入站连接请求。它会等待所有代理运行结束,如果有任何代理出现错误,则返回其中一个代理的错误。这个方法用于管理和运行代理,以便允许入站连接请求并将它们转发到相应的远程目标。
func (t *Tunnel) BindRemotes(ctx context.Context, remotes []*settings.Remote) error { ... }
:这是一个Tunnel
结构体的方法,用于将给定的远程配置转换为代理并阻塞等待代理的运行,接受两个参数:上下文ctx
和一个包含远程配置的切片remotes
。if len(remotes) == 0 { ... }
:检查remotes
切片是否为空,如果没有配置远程代理,返回一个错误,表示没有远程配置可用。if !t.Inbound { ... }
:检查t.Inbound
字段,如果设置为false
,表示入站连接被阻止,返回一个错误,表示无法创建入站连接。- 创建一个长度等于
remotes
切片长度的切片proxies
,用于存储代理对象。 - 使用循环遍历
remotes
切片,并为每个远程配置创建一个代理对象。这些代理对象会存储在proxies
切片中。同时,会为每个代理对象分配一个唯一的标识t.proxyCount
,用于标识代理。 - 使用
errgroup
包创建一个错误组eg
,用于管理并发运行的代理对象。 - 针对每个代理对象,启动一个goroutine,调用
p.Run(ctx)
来运行代理。这些代理会在后台运行,并等待入站连接请求。 - 打印调试信息表示代理已经成功创建。
- 使用
eg.Wait()
等待所有代理对象的运行结束。如果任何一个代理对象返回错误,eg.Wait()
会返回其中的一个错误。 - 打印调试信息表示代理已经全部解绑(即代理运行结束)。
- 返回代理运行结束后的错误(如果有错误的话)。
handleSSHChannels函数
1 | func (t *Tunnel) handleSSHChannels(chans <-chan ssh.NewChannel) { |
总的来说,这段代码的主要作用是根据SSH通道的类型和目标来将数据流量路由到相应的处理函数,并记录连接的信息以及处理过程中的调试信息。根据不同的协议类型,会调用不同的处理函数来处理数据流量,例如处理SOCKS代理请求、UDP流量或TCP流量。这有助于在SSH隧道中根据不同的通道类型进行灵活的数据传输。
func (t *Tunnel) handleSSHChannel(ch ssh.NewChannel) { ... }
:这是一个Tunnel
结构体的方法,用于处理SSH通道。if !t.Config.Outbound { ... }
:检查t.Config.Outbound
字段是否为false
,如果是,则表示出站连接被禁止,打印调试信息并拒绝通道,返回ssh.Prohibited
错误。remote := string(ch.ExtraData())
:从通道的额外数据中获取远程地址信息,该额外数据通常包含了目标地址信息。- 解析远程地址信息,提取出主机和端口以及协议类型(TCP或UDP),同时检查是否为SOCKS代理请求。
udp
表示协议类型为UDP,socks
表示是SOCKS代理请求。 - 如果是SOCKS代理请求,并且SOCKS代理服务器未启用(
t.socksServer == nil
),则打印调试信息并拒绝通道,返回ssh.Prohibited
错误。 - 通过
ch.Accept()
接受SSH通道,获取一个SSH通道和请求通道。如果无法接受通道,打印调试信息并返回。 - 创建一个
stream
,用于表示与SSH通道相关联的读写流。这个流将用于传输数据。 - 启动一个goroutine,通过
ssh.DiscardRequests(reqs)
来处理SSH请求通道,这里的请求通道主要是用于处理SSH的远程请求(例如端口转发)。 - 创建一个记录器(Logger)
l
,用于记录通道相关的调试信息。然后,通过t.connStats.Open()
表示新建一个连接,打印连接信息。 - 根据通道的类型(SOCKS、UDP或TCP),分别调用相应的处理函数来处理数据流量。
- 如果是SOCKS代理请求,调用
t.handleSocks(stream)
来处理SOCKS代理请求。 - 如果是UDP协议,调用
t.handleUDP(l, stream, hostPort)
来处理UDP流量。 - 如果是TCP协议,调用
t.handleTCP(l, stream, hostPort)
来处理TCP流量。
- 如果是SOCKS代理请求,调用
- 在处理完数据流后,关闭连接计数(
t.connStats.Close()
),并在连接关闭时打印相应的调试信息,包括连接状态和错误信息(如果有错误)。
数据包转发
TCP
1 | func (t *Tunnel) handleTCP(l *cio.Logger, src io.ReadWriteCloser, hostPort string) error { |
总的来说,这段代码用于将TCP流量从SSH通道的数据流传输到目标主机和端口的TCP连接,并记录传输的数据量。这种方式可以用于在SSH隧道中转发TCP流量。、
dst, err := net.Dial("tcp", hostPort)
:使用Go标准库的net.Dial
函数建立一个TCP连接到指定的目标主机和端口(hostPort
)。如果建立连接过程中出现错误,将返回错误信息。s, r := cio.Pipe(src, dst)
:使用自定义的cio.Pipe
函数,将源数据流(src
,来自SSH通道)和目标TCP连接(dst
)连接起来,创建一个数据管道,允许数据从源传输到目标。s
和r
分别代表已发送和已接收的数据量。l.Debugf("sent %s received %s", sizestr.ToString(s), sizestr.ToString(r))
:使用记录器的Debugf
方法,记录已发送和已接收的数据量,以便在调试时查看连接的数据流量。- 最后,函数返回
nil
,表示成功处理TCP连接。
UDP
1 | func (t *Tunnel) handleUDP(l *cio.Logger, rwc io.ReadWriteCloser, hostPort string) error { |
总的来说,这段代码的主要作用是建立一个UDP数据包处理器,不断接收UDP数据包并将它们转发到目标主机和端口。这种方式可以用于在SSH隧道中转发UDP流量。UDP数据包的收发和转发是通过udpHandler
和相关的结构体来管理的。
- 创建一个
udpConns
结构体,用于管理UDP连接,包括记录每个UDP连接的状态。 - 在函数末尾使用
defer conns.closeAll()
确保在函数返回时关闭所有UDP连接。 - 创建一个
udpHandler
结构体,该结构体用于处理UDP数据包的收发和转发。它包括以下属性:Logger
:记录器,用于记录调试信息。hostPort
:目标主机和端口,表示要将UDP数据包转发到的目标地址。udpChannel
:UDP数据包的通信通道,包括读取器、写入器和通道本身。udpConns
:管理UDP连接的结构体,用于跟踪每个连接的状态。maxMTU
:UDP数据包的最大传输单元大小,可以通过环境变量配置,默认为9012字节。
- 打印调试信息,显示UDP最大数据包大小。
- 进入一个无限循环,用于不断接收和处理UDP数据包。
- 在循环中,创建一个空的
udpPacket
结构体,用于表示UDP数据包。 - 调用
h.handleWrite(&p)
来处理接收到的UDP数据包,其中&p
表示要处理的数据包的引用。 - 如果处理UDP数据包过程中出现错误,函数将返回错误信息。
SOCKS
1 | func (t *Tunnel) handleSocks(src io.ReadWriteCloser) error { |
总的来说,这段代码的主要作用是处理 SOCKS 代理的连接请求,以及提供了一个用于创建网络连接的函数。它允许将io.ReadWriteCloser
类型的对象转换为net.Conn
类型的对象,以便在网络通信中使用。在 SOCKS 代理情境下,handleSocks
函数将 SSH 通道的数据流转发给 SOCKS 代理服务器进行处理,而NewRWCConn
函数用于创建符合net.Conn
接口的对象。
func (t *Tunnel) handleSocks(src io.ReadWriteCloser) error { ... }
:- 这个函数的主要作用是处理 SOCKS 代理的连接。
- 它接受一个实现了
io.ReadWriteCloser
接口的参数src
,通常来自SSH通道的数据流。 - 函数通过调用
t.socksServer.ServeConn
将src
转换为net.Conn
类型并传递给 SOCKS 代理服务器来处理。 - 这个函数的实际作用是将 SOCKS 代理的连接请求转发给 SOCKS 代理服务器进行处理。
func NewRWCConn(rwc io.ReadWriteCloser) net.Conn { ... }
:- 这个函数用于创建一个网络连接,返回一个
net.Conn
接口类型的对象。 - 它接受一个实现了
io.ReadWriteCloser
接口的参数rwc
,通常是一个包含读写方法的对象,例如SSH通道的数据流。 - 在函数内部,它创建了一个
rwcConn
结构体,并将参数rwc
赋值给ReadWriteCloser
字段,然后返回这个rwcConn
对象。 rwcConn
是一个自定义的类型,实现了net.Conn
接口,用于包装io.ReadWriteCloser
对象,以便它可以被当作net.Conn
类型使用。
- 这个函数用于创建一个网络连接,返回一个
wireshark抓包数据分析
服务器
1)
1 | wsConn, err := upgrader.Upgrade(w, req, nil) |
wsConn, err := upgrader.Upgrade(w, req, nil)
:这一行代码使用upgrader
将 HTTP 连接升级为 WebSocket 连接,并返回一个 WebSocket 连接对象wsConn
。如果升级失败,将会返回一个错误。
2)
1 | sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig) |
server_hander.go里存在这段代码
sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)
:这一行代码使用 SSH 包中的NewServerConn
函数,将 WebSocket 连接升级为 SSH 连接,并返回 SSH 连接对象sshConn
、通道(channel)和请求(request)。
1 | s.clientVersion, err = exchangeVersions(s.sshConn.conn, s.serverVersion) |
ssh/server.go里面有这段代码,会将serverVersion传递给客户端
1 | server.sshConfig = &ssh.ServerConfig{ |
main.go里面将version 定义为了SSH-chisel-v3-server
,所以在传递的时候会和传递这个数据
3)
一段乱糟糟的数据,执行完
sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)
这段函数后发送的,但是这个函数是ssh库的函数产生的,不具备特殊性
这段数据可以看出来是ssh的密钥交换算法的一部分,ssh key-exchange-algorithms,也就是ssh支持的算法什么的
客户端
1)
客户端发送一个websocket的升级请求,chisel的数据特征和普通的特征的区别是websocket的子协议存在chisel
字段
2)
客户端发送的数据都是带有掩码异或的,但是服务端发送的数据不带有,所以从服务端的数据更能看出数据特征,这段数据便是经过掩码加密的,下图可以看到
3)
这段数据也是掩码加密的,实际数据如下
4)
这段数据经过掩码解密后的数据是乱码,很明显是经过加密的数据,不具备参考意义
5)
后面的数据是在ssh登录的时候传递的数据,很明显无论是客户端还是服务端都不具备参考意义
suricata规则
所以可以选择的特征如下