Sliver 植入体 Pivot 链传输客户端源码解析:pivotclients 包架构、密钥交换与隧道协商机制
2026/9/24 7:03:33 网站建设 项目流程
  • 网络安全

【免费下载链接】sliver

Adversary Emulation Framework

项目地址:https://gitcode.com/gh_mirrors/sl/sliver
点击查看免费下载

导读

本文以 Sliver 对抗仿真框架中implant/sliver/transports/pivotclients包为核心,深入剖析植入体(Implant)在通过 Pivot 链(pivot chain)拨号上行时使用的传输客户端实现。该包是 Sliver 多层跳板通信的"最后一公里":它实现了嵌套客户端行为(nested client behaviors)与隧道协商(tunnel negotiation),并为 TCP 与 Windows 命名管道(Named Pipe)两种 pivot 监听器类型提供对等连接能力。读完本文,你将掌握NetConnPivotClient的核心抽象、两级密钥交换流程、长度前缀帧协议与防滥用保护、C2 URL 选项解析规则,以及上层传输路由(tcppivot:///namedpipe://)如何与之衔接。


一、包定位:Pivot 链上的"客户端侧"传输组件

在 Sliver 的架构中,Pivot 是指植入体之间相互中继流量的机制:一台已经受控的机器(中间节点)运行 pivot 监听器,其他植入体(后续节点)通过它间接连接 C2 服务器,从而形成多级跳板链。整个链路涉及两个方向的能力:

  • 监听器侧(listener side):由implant/sliver/pivots包维护PivotListener的创建、注册与生命周期管理;
  • 客户端侧(client side):即本包pivotclients,负责作为"后续植入体"主动拨号到上游 peer,协商会话并复用其链路向服务器注册。

根据 implant/sliver/transports/pivotclients/README.md 的说明,本包"实现了嵌套客户端行为与隧道协商,运行时组件覆盖 namedpipe、namedpipe generic、namedpipe windows 以及 pivotclient 四类植入体侧特性"。包内共有 5 个 Go 源文件与 1 个测试文件:

文件职责
pivotclient.go核心 Pivot 客户端抽象,协调嵌套传输(长度前缀帧、读写、两级加密、会话关闭)
tcp.go基于 TCP 的 Pivot 客户端实现与 C2 URL 选项解析
namedpipe.go命名管道 Pivot 的公共选项定义与解析(平台无关)
namedpipe_windows.goWindows 命名管道传输实现(构建条件:IncludeNamePipe
namedpipe_generic.go非 Windows 平台的命名管道客户端桩(返回unsupported platform
pivotclient_test.go会话 ID 解析与帧长度保护的单元测试

所有源文件均位于 implant/sliver/transports/pivotclients/,其中namedpipe_generic.go顶部带有//go:build !windows构建约束,而namedpipe_windows.go同时受 Sliver 模板系统{{if .Config.IncludeNamePipe}}条件编译控制。


二、核心抽象:NetConnPivotClient

NetConnPivotClient(定义于 pivotclient.go)是本包一切功能的载体。它把任何net.Conn(TCP socket 或 Windows named pipe)包装成"已协商好的 pivot 会话连接",对外只暴露四个方法:

type NetConnPivotClient struct { pivotSessionID []byte // 与上游服务器协商出的 pivot 会话 UUID conn net.Conn // 底层传输(TCP / named pipe) readMutex *sync.Mutex // 串行化读取 writeMutex *sync.Mutex // 串行化写入 peerCipherCtx *cryptography.CipherContext // 与直接 peer 之间的加密上下文 serverCipherCtx *cryptography.CipherContext // 与上游 C2 服务器之间的加密上下文 readDeadline time.Duration // 读写超时 writeDeadline time.Duration }

关键设计点:

  1. 双层加密上下文peerCipherCtx加密"本节点到直接上游 peer"这一段链路;serverCipherCtx加密"本节点到 C2 服务器"端到端这一段。数据在 peer 上被解密后再重加密转发,因此中间节点无法读取会话内容,这实现了"嵌套客户端行为"(每一跳只对相邻一跳可见)。
  2. 读写互斥readMutex/writeMutex保证在多 goroutine 场景(如并发发送信封与后台 ping)下帧边界不被破坏。
  3. 通用性:TCP 与 named pipe 两种StartSession都返回同一个*NetConnPivotClient,上层传输(见第五节)完全不需要关心底层是 socket 还是管道。

此外包内定义了哨兵错误errInvalidPivotSessionID(pivotclient.go),并在 pivotclient_test.go 中用TestParsePivotSessionID验证:只有恰好 16 字节的数据才能被解析为 UUID,nil、15 字节与 17 字节输入都必须返回该错误。


三、两级密钥交换:从 Peer 到 Server 的会话建立

KeyExchange()(pivotclient.go)是建立会话的入口,分为peerKeyExchangeserverKeyExchange两个阶段,全程对底层连接设置 deadline(默认见第四节),防止死锁。

3.1 第一阶段:与直接 Peer 交换密钥

流程如下(对应peerKeyExchange,pivotclient.go):

  1. 本节点构造pb.PivotHello,携带cryptography.PeerAgePublicKey(本节点的 Age 公钥)、PublicKeySignature以及本节点 Peer IDpivots.MyPeerID,序列化后写入连接;
  2. 读取对端返回的PivotHello,用cryptography.AgeDecryptFromPeer解密出会话密钥并校验签名,要求会话密钥恰好 32 字节;
  3. 用该 32 字节密钥构造peerCipherCtx,完成 peer 段加密上下文的初始化。

这里的pivots.MyPeerID定义于 implant/sliver/pivots/pivots.go,是"每次进程执行都会重新生成的实例级 ID":它通过crypto/rand读取 8 字节生成 64 位整数,随机数不可用时回退到time.Now().UnixNano()(generatePeerID)。Peer ID 是链路中每条消息路由的依据。

3.2 第二阶段:经由 Peer 与上游服务器交换密钥

serverKeyExchange(pivotclient.go)实现"嵌套"的精髓:

  1. 本节点用cryptography.RandomSymmetricKey()生成随机会话密钥,cryptography.AgeKeyExToServer加密后放入pb.PivotServerKeyExchange,并附上OriginID: pivots.MyPeerID
  2. 将该消息封装为pb.PivotPeerEnvelope(记录经过的 peer 列表[{MyPeerID, SliverName}]),再套一层pb.Envelope{Type: MsgPivotPeerEnvelope},最后用peerCipherCtx加密写入连接——即"借道 peer 把密钥交换请求递交给上游服务器";
  3. 读取响应信封,校验类型必须为MsgPivotServerKeyExchange,解密得到服务器的SessionKey,解析为 16 字节 UUID 作为pivotSessionID保存,此后所有上行数据都携带该会话 ID。

值得注意的工程细节:响应阶段的读 deadline 被放宽到5 分钟(pivotclient.go),源码注释明确解释了原因——"该请求需要往返服务器,如果上游植入体使用慢速协议,来回可能需要较长时间"。这说明 Sliver 在实现上对"多层慢速中继"场景做了显式容错。


四、帧协议:长度前缀 + 双层信封 + 防滥用保护

4.1 写入路径

write(pivotclient.go)采用经典的 4 字节小端长度前缀 + 负载的帧格式:

  • 先用lengthOf把消息长度写成 4 字节 little-endian(lengthOf定义见 pivotclient.go);
  • 若长度前缀未能一次写完 4 字节,直接返回ErrFailedWrite(定义于 tcp.go);
  • 负载部分使用循环Write直至写满,容忍部分写入。

4.2 读取路径与安全上限

read(pivotclient.go)用io.ReadFull依次读取 4 字节长度与负载,并做两层防护:

  • 长度 ≤ 0 时返回zero data length错误;
  • 长度超过pivots.MaxFrameLength时返回pivots.ErrFrameTooLarge

MaxFrameLength定义为512 MiB(implant/sliver/pivots/pivots.go),源码注释给出了明确的防护动机:"损坏或失步的长度前缀可能被解析为接近 4 GiB 的值,从而触发足以压垮植入体的大内存分配(参见 BishopFox/sliver#1452)。512 MiB 远高于任何真实中继消息的体积,又远低于致命分配阈值。"

对应的测试TestNetConnPivotClientReadRejectsOversizedFrame(pivotclient_test.go)用net.Pipe模拟对端写入MaxFrameLength+1的长度前缀,断言读取在 3 秒内以pivots.ErrFrameTooLarge失败;TestNetConnPivotClientReadAcceptsValidFrame则验证合法负载可被完整、原样读出。这两条用例共同构成了帧解析的"拒绝越界、接受合法"边界。

4.3 信封封装规则

WriteEnvelope(pivotclient.go)和ReadEnvelope(pivotclient.go)是上层与传输之间的消息接口,封装规则非常精细:

  • 上行:普通消息先用serverCipherCtx加密,包进PivotPeerEnvelope(携带PivotSessionID与自己的 PeerID)再整体套MsgPivotPeerEnvelope,最后用peerCipherCtx加密;而MsgPivotPeerPing与已存在的 peer envelope 属于"不属于本节点起源"的控制消息,用服务器密钥加密(pivotclient.go);
  • 下行:先解 peer 层密文,若收到的不是MsgPivotPeerEnvelope则报invalid message type;随后检查 peer 列表首位是否为pivots.MyPeerID——"如果不是我们,这个 peer envelope 就不是发给我们的",直接原样返回由上层转发(实现链式中继);只有确认自己是收件人时才用serverCipherCtx解密出真正的业务信封。

这条路径完整实现了"每一跳只解密 peer 段、端到端加密仍保持"的嵌套传输语义,正是 README 所述"nested client behaviors and tunnel negotiation"的代码落地。


五、两种底层传输实现与 C2 URL 选项

5.1 TCP Pivot 客户端(tcp.go

TCPPivotStartSession(tcp.go)是本包最直接的会话入口:

func TCPPivotStartSession(peer string, opts *TCPPivotOptions) (*NetConnPivotClient, error) { conn, err := net.Dial("tcp", peer) if err != nil { return nil, err } pivot := &NetConnPivotClient{ conn: conn, readMutex: &sync.Mutex{}, writeMutex: &sync.Mutex{}, readDeadline: opts.ReadDeadline, writeDeadline: opts.WriteDeadline, } err = pivot.KeyExchange() if err != nil { conn.Close() return nil, err } return pivot, nil }

拨号成功即执行第二节所述的两级密钥交换,任何一步失败都会关闭连接并返回错误。

5.2 NamedPipe Pivot 客户端

Windows 实现NamedPipePivotStartSession(namedpipe_windows.go)先把 C2 URL 转换成本机管道路径再拨号:

address := "\\\\" + uri.Hostname() + strings.Replace(uri.Path, "/", "\\", -1) conn, err := winio.DialPipe(address, nil)

\\主机名\路径的形式,然后以与 TCP 完全相同的NetConnPivotClient构造流程完成密钥交换。它依赖github.com/lesnuages/go-winio库,并被{{if .Config.IncludeNamePipe}}模板包裹——只有在生成植入体时启用了 named pipe 支持才会编译进二进制。

非 Windows 平台的NamedPipePivotStartSession(namedpipe_generic.go)直接返回errors.New("unsupported platform"),通过//go:build !windows保证两个同名函数永远不会同时编译。

5.3 C2 URL 选项解析(默认值与来源)

两个Parse*Options函数从拨号 URL 的查询参数中解析超时配置,解析失败或参数缺失时回退到默认值:

参数TCP 默认值(tcp.go)NamedPipe 默认值(namedpipe.go)含义
read-deadline10s(defaultDeadline10s(defaultNamedPipeDeadline单次读取操作的超时
write-deadline10s(defaultDeadline10s(defaultNamedPipeDeadline单次写入操作的超时
timeout10s(defaultNamedPipeDeadlineNamedPipe 专属的连接建立超时

ParseTCPPivotOptionsParseNamedPipePivotOptions均使用time.ParseDuration解析,因此支持10s500ms1m等 Go duration 字符串;NamedPipe 版本在参数存在但解析失败时还会输出调试日志(仅 Debug 构建启用),便于定位配置错误。


六、上层接入:传输路由与心跳维持

pivotclients 包并不自行决策协议,而是被植入体的传输层按 URL scheme 分发调用:

  • tcppivot://:在 implant/sliver/transports/session.go 中被路由到tcpPivotConnect(uri),该函数(session.go)调用ParseTCPPivotOptions(uri)TCPPivotStartSession(uri.Host, opts)完成拨号;
  • namedpipe://:在 implant/sliver/transports/transports_windows.go 的namedPipeConnect中被路由到ParseNamedPipePivotOptions(uri)NamedPipePivotStartSession(uri, opts)

拨号成功后,连接层会为 pivot 会话启动一个双路心跳 goroutine(每分钟一次,见 session.go 与 transports_windows.go):向直接 peer 发送MsgPivotPeerPing(携带纳秒级 Nonce),同时向服务器发送MsgPivotServerPing,分别验证"上一跳存活"与"整条链路可达"。这两类 ping 在WriteEnvelope中走"不加密 server 段"的特殊分支,与控制消息通道解耦,避免 ping 在高延迟链路上排队超时。

pivotclient.go中与协议相关的消息类型全部来自protobuf/sliverpbMsgPivotPeerEnvelopeMsgPivotServerKeyExchangeMsgPivotPeerPingMsgPivotSessionEnvelope等),proto 定义可参考 protobuf/sliverpb/sliver.proto。


七、会话生命周期与关闭

会话结束后调用CloseSession()(pivotclient.go),它直接关闭底层net.Conn,从而触发对端read返回错误并层层退出。上层Connectioncleanup回调(发送ctrl/pingCtrl信号)会终止心跳 goroutine 并清理隧道状态,详见 session.go。此外,会话的Stop()cleanup()语义在nextSendEnvelope(session.go)的注释中有明确说明:生产者通道在清理后保持打开,传输发送方必须依赖连接生命周期而非range发送通道来判断连接是否终结——这避免了 pivot 会话断开时发送方永久阻塞。

从调用链可以推断,CloseSession通常由上层 transport 连接在检测到io.EOF、密钥交换失败或连续错误超限(opts.MaxErrors)时触发,与 pivots.go 中RemoveListener对监听器侧的生命周期管理形成对偶关系。


八、小结

pivotclients包虽小,却是 Sliver 多级跳板通信中承上启下的关键一环:

  • 抽象层NetConnPivotClient统一了 TCP 与 Windows NamedPipe 两种底层传输,让上层只面对"已协商好的加密连接";
  • 安全层:peer/server 两级密钥交换与双层信封封装,保证中间节点只可转发、不可窃读;
  • 健壮层:4 字节长度前缀帧 + 512 MiB 上限 + 双向 deadline,配合 pivotclient_test.go 的回归测试,防止失步帧导致的内存崩溃;
  • 接入层tcppivot://namedpipe://两种 scheme 在 session.go 与 transports_windows.go 中完成路由,心跳机制维持链路健康。

对于希望深入 Sliver 内部或自行实现 pivot 协议的读者,建议按以下顺序阅读源码:先通读本包全部 6 个文件,再对照 implant/sliver/pivots/pivots.go 理解监听器侧与MyPeerIDMaxFrameLength等共享常量,最后回到 implant/sliver/transports/session.go 查看传输层如何组合这些能力——三者合在一起,才能看到一条完整 pivot 链路从拨号、协商、加密中继到心跳保活的全貌。

  • 网络安全

【免费下载链接】sliver

Adversary Emulation Framework

项目地址:https://gitcode.com/gh_mirrors/sl/sliver
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询