(1)CBC模式的原理
这种模式是先将明文切分成若干小段,然后每一小段与初始块(IV向量)或者上一段的密文段进行异或运算后,再与密钥进行加密,第一个数据块进行加密之前需要用初始化向量IV进行异或操作。
(此图和以上原理参考于网络)
(2)漏洞复现
鄙人在实际开发API服务中用到了cbc模式的加密算法,但测试过程中却发现了此算法有安全漏洞,而且比较容易复现!下面是用于复现的测试代码:
cbc.go(填充模式PKCS5和PKCS7都适用)
package cbc
import (
"bytes"
"crypto/aes" "crypto/cipher" "encoding/base64" "fmt")
func encryptCBC(text []byte, aesKey, aesIV string) (encryptData string, err error) {
fmt.Println("encryptCBC-Key:", aesKey)
block, err := aes.NewCipher([]byte(aesKey))
if err != nil {
fmt.Println("encryptCBC NewCipher ERR:", err.Error())
return "", err
}
blockSize := block.BlockSize()
originData := pad(text, blockSize)
fmt.Println("encryptCBC-IV:", aesIV)
blockMode := cipher.NewCBCEncrypter(block, []byte(aesIV))
encrypt := make([]byte, len(originData))
blockMode.CryptBlocks(encrypt, originData)
encryptData = base64.StdEncoding.EncodeToString(encrypt)
return
}
func pad(cipherText []byte, blockSize int) []byte {
padding := blockSize - len(cipherText)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(cipherText, padText...)
}
func decryptCBC(text, aesKey, aesIV string) (decryptData string, err error) {
decodeData, err := base64.StdEncoding.DecodeString(text)
if err != nil {
return "", err
}
fmt.Println("decryptCBC-Key:", aesKey)
block, err := aes.NewCipher([]byte(aesKey))
if err != nil {
fmt.Println("decryptCBC Err:", err)
return "", err
}
fmt.Println("decryptCBC-IV:", aesIV)
blockMode := cipher.NewCBCDecrypter(block, []byte(aesIV))
originData := make([]byte, len(decodeData))
blockMode.CryptBlocks(originData, decodeData)
decryptData = string(unPad(originData))
return
}
func unPad(cipherText []byte) []byte {
length := len(cipherText)
unPadding := int(cipherText[length-1])
return cipherText[:(length - unPadding)]
}
cbc_test.go
package cbc
import "testing"
func TestEncryptCBC(t *testing.T) {
type args struct {
text []byte
aesKey string
aesIV string
}
tests := []struct {
name string
args args
wantEncryptData string
wantErr bool
}{
// 和解密的时候用的密钥和向量一样
{name: testing.CoverMode(), args: args{text: []byte("Tokyo"), aesKey: "DFA84B10B7ACDD25", aesIV: "DFA84B10B7ACDD25"}, wantEncryptData: "Tokyo", wantErr: false},
// 和解密的时候用的密钥相同, 向量不同, 这个地方产生了安全漏洞, 因为解密方可以解密成功!
{name: testing.CoverMode(), args: args{text: []byte("Tokyo"), aesKey: "DFA84B10B7ACDD25", aesIV: "DFA84B00B7ACDD25"}, wantEncryptData: "Tokyo", wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotEncryptData, err := encryptCBC(tt.args.text, tt.args.aesKey, tt.args.aesIV)
if (err != nil) != tt.wantErr {
t.Errorf("encryptCBC() error = %v, wantErr %v", err, tt.wantErr)
return
}
decryptKey, decryptIV := "DFA84B10B7ACDD25", "DFA84B10B7ACDD25"
decryptData, err := decryptCBC(gotEncryptData, decryptKey, decryptIV)
if decryptData != tt.wantEncryptData {
t.Errorf("encryptCBC() gotEncryptData = %v, want %v", decryptData, tt.wantEncryptData)
} else {
t.Logf("Successfully, encryptCBC() gotEncryptData = %v, want %v", decryptData, tt.wantEncryptData)
}
})
}
}
func TestDecryptCBC(t *testing.T) {
encryptKey, encryptIV := "DFA84B10B7ACDD25", "DFA84B10B7ACDD25"
gotEncryptData, err := encryptCBC([]byte("Tokyo"), encryptKey, encryptIV)
if err != nil {
t.Fatal("encryptCBC ERR:", err)
}
type args struct {
text string
aesKey string
aesIV string
}
tests := []struct {
name string
args args
wantDecryptData string
wantErr bool
}{
// 和加密时候用的密钥和向量一样
{name: testing.CoverMode(), args: args{text: gotEncryptData, aesKey: "DFA84B10B7ACDD25", aesIV: "DFA84B10B7ACDD25"}, wantDecryptData: "Tokyo", wantErr: false},
// 和加密时候用的密钥一样, 但向量不同, 这个地方产生了安全漏洞, 向量不同却可以解密成功!
{name: testing.CoverMode(), args: args{text: gotEncryptData, aesKey: "DFA84B10B7ACDD25", aesIV: "DFA84B00B7ACDD25"}, wantDecryptData: "Tokyo", wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotDecryptData, err := decryptCBC(tt.args.text, tt.args.aesKey, tt.args.aesIV)
if (err != nil) != tt.wantErr {
t.Errorf("decryptCBC() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotDecryptData != tt.wantDecryptData {
t.Errorf("decryptCBC() gotDecryptData = %v, want %v", gotDecryptData, tt.wantDecryptData)
} else {
t.Logf("Successfully, decryptCBC() gotDecryptData = %v, want %v", gotDecryptData, tt.wantDecryptData)
}
})
}
}
测试结果如下:
1)以解密方的密钥和向量为正确的标准,加密方的密钥正确,但向量不正确
2)以加密方的密钥和向量为正确的标准,解密方的密钥正确,但向量不正确
以上两种异常情况,使用CBC模式都可以得到正确的解密后的结果,还有其他更多的异常情况等,在这里不再叙述,这不是go语言的缺陷,而是AES算法本身的缺陷,其他诸如Java,Python等都有此漏洞,也无法通过语言层面来改进这个漏洞,所以使用AES算法要多加小心!
(3)CBC字节反转攻击法(常用的破解cbc模式的攻击方法)
问题出在异或加密这里,在讲解字节反转攻击前先了解下异或加密。
异或 xor 符号表示为 ^ ,计算机中 两个数字异或,相同为0,不同为1。 1^1=0 0^1=1
如果是字母异或加密,a^b,那么首先转化为ascii编码,然后二进制,对每一位进行异或得到的结果转为十进制,在ascii编码出来。
异或有一个特性,任意值与自己本身做异或运算的结果都是0,任意值与0做异或运算的结果都是自己。本身a^b=乱七八糟,a^a则为空,但是a^a^任意字母=任意字母。
在CBC解密中,如图A是第一组的密文,B是第二组被解密的密文(未异或),C是明文。C=A^B。那么B=C^A,且A^B^C=0。如果我们更改A,A为我们可控的密文,C=A^B,如果我们使A=B^X,B=C^A,所以A=C^A^X,C=C^A^X^B=B^X^B=X。这里X是我们需要的任意字符,这便是CBC字节反转攻击的核心,这样一来C的明文就完全可控了。
(此图和以上方法参考于网络)
有疑问加站长微信联系(非本文作者)