Go 中的 AES-CBC-Ciphertext Stealing(密文窃取)加解密:aescts 包原理与在 Kerberos 加密链中的实战应用
2026/9/23 18:50:56 网站建设 项目流程
  • 网络安全

【免费下载链接】sliver

Adversary Emulation Framework

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

本篇文章以 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.Encryptaescts.Decrypt,依赖 Go 标准库crypto/aescrypto/cipher,无任何第三方依赖。

三、核心 API:Encrypt 与 Decrypt

Encrypt:加密并返回 next IV

func Encrypt(key, iv, plaintext []byte) ([]byte, []byte, error)

参数与返回值(对应 aescts.go 的Encrypt实现):

说明
keyAES 密钥,必须是 16 / 24 / 32 字节(对应 AES-128/192/256),否则aes.NewCipher返回错误
iv初始向量,必须是 16 字节;函数内部不会修改传入的 iv
plaintext明文,任意长度(含非块整数倍),允许为空调用则由内部zeroPad报错
返回值 1next 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)有几点值得注意:

  1. 先拷贝密文:源码注释明确指出,即使以值传递切片,其底层数组仍可能被修改,因此内部先copy一份,避免污染调用方数据;
  2. 长度下限校验:密文长度小于一个块(16 字节)直接返回错误"Ciphertext is not large enough..."
  3. 整数倍场景:只需把末两块交换回来,再用标准 CBC 解密;单个块(恰好 16 字节)时无法交换,直接按 CBC 解密;
  4. 非整数倍场景:需要从倒数第二块密文尾部"借"出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 中的EncryptDataDecryptData直接转调 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函数在同时提供krb5confkeytabrealm时走 Kerberos 认证分支,调用github.com/yiya1989/sshkrb5/krb5forssh.NewKrb5InitiatorClient(该库导入gokrb5.v7clientspnegokeytabmessages等包,见 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 包上。

七、使用注意事项

结合源码行为,使用该包时建议注意以下几点:

  1. 密钥与 IV 长度必须正确key必须是 16/24/32 字节,iv必须是 16 字节,否则要么aes.NewCipher返回错误,要么在加密过程中产生错误结果;Kerberos 场景中 IV 通常取全零向量。
  2. 务必保存并使用返回的 next IVEncrypt第一个返回值是供后续加密使用的 next IV(加密输出倒数第二块)。如果要分片加密长消息并保持 CBC 链式依赖,必须在下一片加密时传入它,而不是沿用原始 iv。
  3. 密文长度与明文等长:除单块场景(输出被补齐到 16 字节)外,CTS 输出长度恒等于明文长度;解密方必须知晓这个语义,避免把补零字节误当数据。
  4. Decrypt 会做长度校验:密文不足一个块会直接返回错误;整数倍与非整数倍两条解密路径不能互换,必须与加密侧保持一致。
  5. 在 Sliver 中它是间接依赖:日常开发一般无需直接调用 aescts,它通过 gokrb5 服务于 implant 的 Kerberos/SPNEGO 认证链路;如需独立使用,参考第二节的go getimport方式即可。

结语

aescts.v1虽然是一个只有两个公开函数的小型加密包,但它以标准库crypto/aescrypto/cipher为基础,完整实现了 CBC 密文窃取模式的免填充加密,并在本仓库中承担着 Kerberos 5 AES 加密类型(RFC 3962 / RFC 8009)的底层加解密职责。理解其"单块 / 整数倍 / 非整数倍"三分支逻辑与 next IV 语义,既能帮助你在自己的 Go 项目中正确引入 CTS 加密,也能更深入地读懂 Sliver implant 中 SSH Kerberos 认证这条完整依赖链。

  • 网络安全

【免费下载链接】sliver

Adversary Emulation Framework

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

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

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

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

立即咨询