- 网络安全
【免费下载链接】sliver
Adversary Emulation Framework
导读
本文以 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.go | Windows 命名管道传输实现(构建条件: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 }关键设计点:
- 双层加密上下文:
peerCipherCtx加密"本节点到直接上游 peer"这一段链路;serverCipherCtx加密"本节点到 C2 服务器"端到端这一段。数据在 peer 上被解密后再重加密转发,因此中间节点无法读取会话内容,这实现了"嵌套客户端行为"(每一跳只对相邻一跳可见)。 - 读写互斥:
readMutex/writeMutex保证在多 goroutine 场景(如并发发送信封与后台 ping)下帧边界不被破坏。 - 通用性:TCP 与 named pipe 两种
StartSession都返回同一个*NetConnPivotClient,上层传输(见第五节)完全不需要关心底层是 socket 还是管道。
此外包内定义了哨兵错误errInvalidPivotSessionID(pivotclient.go),并在 pivotclient_test.go 中用TestParsePivotSessionID验证:只有恰好 16 字节的数据才能被解析为 UUID,nil、15 字节与 17 字节输入都必须返回该错误。
三、两级密钥交换:从 Peer 到 Server 的会话建立
KeyExchange()(pivotclient.go)是建立会话的入口,分为peerKeyExchange与serverKeyExchange两个阶段,全程对底层连接设置 deadline(默认见第四节),防止死锁。
3.1 第一阶段:与直接 Peer 交换密钥
流程如下(对应peerKeyExchange,pivotclient.go):
- 本节点构造
pb.PivotHello,携带cryptography.PeerAgePublicKey(本节点的 Age 公钥)、PublicKeySignature以及本节点 Peer IDpivots.MyPeerID,序列化后写入连接; - 读取对端返回的
PivotHello,用cryptography.AgeDecryptFromPeer解密出会话密钥并校验签名,要求会话密钥恰好 32 字节; - 用该 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)实现"嵌套"的精髓:
- 本节点用
cryptography.RandomSymmetricKey()生成随机会话密钥,cryptography.AgeKeyExToServer加密后放入pb.PivotServerKeyExchange,并附上OriginID: pivots.MyPeerID; - 将该消息封装为
pb.PivotPeerEnvelope(记录经过的 peer 列表[{MyPeerID, SliverName}]),再套一层pb.Envelope{Type: MsgPivotPeerEnvelope},最后用peerCipherCtx加密写入连接——即"借道 peer 把密钥交换请求递交给上游服务器"; - 读取响应信封,校验类型必须为
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-deadline | 10s(defaultDeadline) | 10s(defaultNamedPipeDeadline) | 单次读取操作的超时 |
write-deadline | 10s(defaultDeadline) | 10s(defaultNamedPipeDeadline) | 单次写入操作的超时 |
timeout | — | 10s(defaultNamedPipeDeadline) | NamedPipe 专属的连接建立超时 |
ParseTCPPivotOptions与ParseNamedPipePivotOptions均使用time.ParseDuration解析,因此支持10s、500ms、1m等 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/sliverpb(MsgPivotPeerEnvelope、MsgPivotServerKeyExchange、MsgPivotPeerPing、MsgPivotSessionEnvelope等),proto 定义可参考 protobuf/sliverpb/sliver.proto。
七、会话生命周期与关闭
会话结束后调用CloseSession()(pivotclient.go),它直接关闭底层net.Conn,从而触发对端read返回错误并层层退出。上层Connection的cleanup回调(发送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 理解监听器侧与MyPeerID、MaxFrameLength等共享常量,最后回到 implant/sliver/transports/session.go 查看传输层如何组合这些能力——三者合在一起,才能看到一条完整 pivot 链路从拨号、协商、加密中继到心跳保活的全貌。
- 网络安全
【免费下载链接】sliver
Adversary Emulation Framework
相关推荐
Sliver 植入体隧道处理器(tunnel_handlers)源码级解析:Shell / SOCKS / 端口转发 / WASM 的隧道流协调机制
Sliver 植入体隧道处理器(tunnel_handlers)源码级解析:Shell / SOCKS / 端口转发 / WASM 的隧道流协调机制 导读 im
网络安全Sliver 客户端核心包 client/core 深度解析:会话状态、隧道编排与 RPC 协调机制
Sliver 客户端核心包 client/core 深度解析:会话状态、隧道编排与 RPC 协调机制 导读 client/core 是 Sliver(Adver
网络安全Sliver 植入物 Pivot 通道管理深入解析:横向移动、命名管道与 Pivot 图构建
Sliver 植入物 Pivot 通道管理深入解析:横向移动、命名管道与 Pivot 图构建 本文聚焦 Sliver Adversary Emulation F
网络安全
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考