- 网络安全
【免费下载链接】sliver
Adversary Emulation Framework
本篇文章以 Sliver 仓库中 vendor 化的gopkg.in/jcmturner/aescts.v1包(README.md)为核心,系统讲解在 Go 中如何使用 AES CBC Ciphertext Stealing(CTS,密文窃取)模式对数据进行加密与解密:包括包的安装与导入、Encrypt/Decrypt两个核心函数的完整语义、三种明文长度场景下的算法分支,以及该包在 Kerberos 5(RFC 3962 / RFC 8009)AES 加密类型中的真实调用链。读完本文,你将理解 CTS 模式为何能免填充、其"交换末两块并截断"的具体实现,并能在自己的 Go 项目中正确使用该包。
一、CBC 密文窃取(Ciphertext Stealing)是什么
传统 CBC 模式要求明文长度必须是 16 字节(AES 块大小)的整数倍,否则需要使用 PKCS#7 等填充方案补齐最后一个块。而Ciphertext Stealing(CTS)是一种不用填充即可处理任意长度明文的 CBC 变体:它让最后两个块"互相借用"密文——倒数第二个密文块中截取所需字节补全最后一个块,加密完成后交换末两块的位置,最后再按原始明文长度截断输出。
该包的源码注释明确引用了这一设计(见 aescts.go):
为保持一致性,密文窃取总是作用于待加密数据的最后两个块。如果数据长度恰好是块大小的整数倍,则等价于普通 CBC 模式再将最后两个密文块交换;一次加密输出中"倒数第二块"(即最后一个明文块的加密结果)被用作下一次加密的初始向量。
这意味着 CTS 模式具有两个关键特性:输出密文与明文等长(无填充膨胀),以及可以自然地传递 next IV 用于连续分片的加密——这两点对 Kerberos 这类需要把加密数据嵌入协议消息的场景至关重要。
二、获取与导入 aescts 包
原文档给出了最直接的获取方式:
go get gopkg.in/jcmturner/aescts.v1在 Go Modules 模式下,更稳妥的做法是指定版本(当前仓库 vendor 的版本为 v1.0.1,见 implant/go-mod 第 42 行):
go get gopkg.in/jcmturner/aescts.v1@v1.0.1导入方式同样继承自原文档:
import "gopkg.in/jcmturner/aescts.v1"该包只暴露两个公开函数aescts.Encrypt与aescts.Decrypt,依赖 Go 标准库crypto/aes与crypto/cipher,无任何第三方依赖。
三、核心 API:Encrypt 与 Decrypt
Encrypt:加密并返回 next IV
func Encrypt(key, iv, plaintext []byte) ([]byte, []byte, error)参数与返回值(对应 aescts.go 的Encrypt实现):
| 项 | 说明 |
|---|---|
key | AES 密钥,必须是 16 / 24 / 32 字节(对应 AES-128/192/256),否则aes.NewCipher返回错误 |
iv | 初始向量,必须是 16 字节;函数内部不会修改传入的 iv |
plaintext | 明文,任意长度(含非块整数倍),允许为空调用则由内部zeroPad报错 |
| 返回值 1 | next IV:取自加密输出倒数第二块,供后续分片加密接力使用 |
| 返回值 2 | 密文:长度与明文严格一致(CTS 无填充膨胀) |
| 返回值 3 | 错误:密钥非法、明文为空等场景返回非 nil |
从源码看,Encrypt内部首先用aes.NewCipher(key)创建分组密码,再用cipher.NewCBCEncrypter(block, iv)创建 CBC 加密器;随后按明文长度分三种分支处理(详见下一节)。值得注意的是,函数内部对plaintext做了拷贝,因此不会破坏调用方的切片。
Decrypt:逆向恢复明文
func Decrypt(key, iv, ciphertext []byte) ([]byte, error)Decrypt的实现(aescts.go)有几点值得注意:
- 先拷贝密文:源码注释明确指出,即使以值传递切片,其底层数组仍可能被修改,因此内部先
copy一份,避免污染调用方数据; - 长度下限校验:密文长度小于一个块(16 字节)直接返回错误
"Ciphertext is not large enough..."; - 整数倍场景:只需把末两块交换回来,再用标准 CBC 解密;单个块(恰好 16 字节)时无法交换,直接按 CBC 解密;
- 非整数倍场景:需要从倒数第二块密文尾部"借"出
npb = BlockSize - len(ct) % BlockSize个字节拼接到最后一块上,再依次解密,最后按len(ct)截断输出。返回值只有明文与错误,长度与原始密文等长。
四、三种明文长度场景的算法分支
Encrypt依据l := len(plaintext)与aes.BlockSize(16)的关系走三条路径,理解这三条路径是掌握该包行为的关键:
场景 1:明文 ≤ 16 字节(单块)
m, _ = zeroPad(m, aes.BlockSize) mode.CryptBlocks(m, m) return m, m, nil先用零填充到 16 字节,做一次 CBC 加密,返回的密文是 16 字节(注意:此时输出比明文长,这是唯一"填充"的情形),next IV 即该密文本身。
场景 2:明文长度是块大小的整数倍
mode.CryptBlocks(m, m) iv = m[len(m)-aes.BlockSize:] rb, _ := swapLastTwoBlocks(m, aes.BlockSize) return iv, rb, nil整段按普通 CBC 加密,然后交换最后两个密文块;返回的 next IV 是交换前的最后一块密文。
场景 3:明文长度不是块大小的整数倍(真正的 CTS)
先zeroPad到块大小整数倍,再调用tailBlocks拆出"前半段 rb / 倒数第二块 pb / 最后一块 lb"三部分;前半段按 CBC 连续加密并滚动更新 iv,随后分别加密 pb 与 lb,最后将密文按lb + pb顺序拼接(交换末两块)并按原始明文长度ct[:l]截断。这正是维基百科"密文窃取"条目所描述的 CBC-CTS 标准做法(源码注释中直接引用了该参考,见 aescts.go)。
五、辅助函数:tailBlocks、swapLastTwoBlocks、zeroPad
包内还有三个未导出的辅助函数,共同支撑上述流程(aescts.go):
tailBlocks(b []byte, c int) ([]byte, []byte, []byte, error):把切片拆成"剩余部分 / 倒数第二块 / 最后一块"三块。最后一块的长度按len(b) % BlockSize计算,余数为 0 时整块为 16 字节;当数据不超过两个块时,返回的rb为 nil。数据不大于一个块时返回错误"bytes slice is not larger than one block so cannot tail"。swapLastTwoBlocks(b []byte, c int) ([]byte, error):在tailBlocks基础上重排为rb + lb + pb,实现末两块交换。加密(整数倍场景)与解密(交换回来)两侧都在使用它。zeroPad(b []byte, m int) ([]byte, error):用零字节把数据补到m的整数倍。块大小参数非法(m <= 0)或数据为空时返回错误。注意它与 PKCS#7 填充不同——只是补零,因为 CTS 最终会按原始长度截断,补零部分不进入最终输出。
六、仓库中的实战应用:Kerberos AES 加密链
这个看似只有两个函数的包,在仓库中的实际价值体现在它是Kerberos 5 AES 加密类型的内核。aescts 在 implant/go-mod 中被列为间接依赖(indirect),由gopkg.in/jcmturner/gokrb5.v7 v7.5.0(第 45 行)引入,并被打包进 Sliver 的 implant 端 vendor 目录。
RFC 3962:AES-CTS-HMAC-SHA1-96
rfc3962/encryption.go 中的EncryptData与DecryptData直接转调 aescts:
ivz := make([]byte, e.GetCypherBlockBitLength()/8) return aescts.Encrypt(key, ivz, data)这里 IV 是全零的 16 字节ivz。对应的加密类型在 aes128-cts-hmac-sha1-96.go 的文件头注释中列出:aes128-cts-hmac-sha1-96(etype 17,128 位密钥)与aes256-cts-hmac-sha1-96(etype 18,256 位密钥),加密/解密函数均为 "AES in CBC-CTS mode",校验和类型为hmac-sha1-96-aes128/256(96 位)。EncryptMessage在加密前还会拼接随机 confounder 并通过DeriveKey按 usage 派生会话密钥。
RFC 8009:AES-CTS-HMAC-SHA2 系列
rfc8009/encryption.go 的结构类似,区别在于AES256_CTS_HMAC_SHA384_192类型需要 32 字节密钥,且完整性哈希按HMAC(Ki, IV | C)方式计算(先验密再解密,见第 108-118 行注释)。两条路径最终都汇聚到同一个aescts.Encrypt/aescts.Decrypt。
上层链路:implant 的 SSH Kerberos 认证
在 Sliver 的 implant 端,implant/sliver/shell/ssh/ssh.go 的getClient函数在同时提供krb5conf、keytab、realm时走 Kerberos 认证分支,调用github.com/yiya1989/sshkrb5/krb5forssh.NewKrb5InitiatorClient(该库导入gokrb5.v7的client、spnego、keytab、messages等包,见 sshkrb5 client.go)。由此构成的依赖链是:
implant SSH 命令(Kerberos 认证) └─ github.com/yiya1989/sshkrb5(SPNEGO/KRB5 客户端) └─ gopkg.in/jcmturner/gokrb5.v7(RFC 3962 / RFC 8009 加密类型) └─ gopkg.in/jcmturner/aescts.v1(AES-CBC-CTS 加解密内核)也就是说,implant 通过 SSH 使用 Kerberos 票据认证时,票据与认证器消息中所有 AES 加密数据的机密性保障,最终都落在这个约 186 行的 aescts 包上。
七、使用注意事项
结合源码行为,使用该包时建议注意以下几点:
- 密钥与 IV 长度必须正确:
key必须是 16/24/32 字节,iv必须是 16 字节,否则要么aes.NewCipher返回错误,要么在加密过程中产生错误结果;Kerberos 场景中 IV 通常取全零向量。 - 务必保存并使用返回的 next IV:
Encrypt第一个返回值是供后续加密使用的 next IV(加密输出倒数第二块)。如果要分片加密长消息并保持 CBC 链式依赖,必须在下一片加密时传入它,而不是沿用原始 iv。 - 密文长度与明文等长:除单块场景(输出被补齐到 16 字节)外,CTS 输出长度恒等于明文长度;解密方必须知晓这个语义,避免把补零字节误当数据。
- Decrypt 会做长度校验:密文不足一个块会直接返回错误;整数倍与非整数倍两条解密路径不能互换,必须与加密侧保持一致。
- 在 Sliver 中它是间接依赖:日常开发一般无需直接调用 aescts,它通过 gokrb5 服务于 implant 的 Kerberos/SPNEGO 认证链路;如需独立使用,参考第二节的
go get与import方式即可。
结语
aescts.v1虽然是一个只有两个公开函数的小型加密包,但它以标准库crypto/aes与crypto/cipher为基础,完整实现了 CBC 密文窃取模式的免填充加密,并在本仓库中承担着 Kerberos 5 AES 加密类型(RFC 3962 / RFC 8009)的底层加解密职责。理解其"单块 / 整数倍 / 非整数倍"三分支逻辑与 next IV 语义,既能帮助你在自己的 Go 项目中正确引入 CTS 加密,也能更深入地读懂 Sliver implant 中 SSH Kerberos 认证这条完整依赖链。
- 网络安全
【免费下载链接】sliver
Adversary Emulation Framework
相关推荐
SeaTunnel FieldEncrypt 加密 Transform 插件实战指南:AES-GCM/AES-CBC 字段加解密原理与配置
SeaTunnel FieldEncrypt 加密 Transform 插件实战指南:AES GCM/AES CBC 字段加解密原理与配置 SeaTunnel
数据集成ETL大数据批处理流处理变更数据捕获AhabAssistantLimbusCompany:Limbus Company自动化助手终极指南
AhabAssistantLimbusCompany:Limbus Company自动化助手终极指南 AhabAssistantLimbusCompany(简称
桌面应用RPA计算机视觉MicroPython cryptolib 模块实战:AES 加密解密(ECB / CBC / CTR)完整指南
MicroPython cryptolib 模块实战:AES 加密解密(ECB / CBC / CTR)完整指南 本指南以 MicroPython 仓库中 do
嵌入式语言运行时编程语言解释器编译器物联网系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考