27

有关AES的CBC模式加解密安全漏洞的实践

 3 years ago
source link: https://segmentfault.com/a/1190000024528620
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

(1)CBC模式的原理

这种模式是先将明文切分成若干小段,然后每一小段与初始块(IV向量)或者上一段的密文段进行异或运算后,再与密钥进行加密。第一个数据块进行加密之前需要用初始化向量IV进行异或操作。
![image](/img/bVbO5aB)
(此图和以上原理参考于网络)

(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)以解密方的密钥和向量为正确的标准,加密方的密钥正确,但向量不正确

RNZfIv2.png!mobile

2)以加密方的密钥和向量为正确的标准,解密方的密钥正确,但向量不正确

zAvi6jA.png!mobile

以上两种异常情况,使用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的明文就完全可控了。
![image](/img/bVbO5a2)

(此图和以上方法参考于网络)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK