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

simamengde · · 642 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

(1)CBC模式的原理

 这种模式是先将明文切分成若干小段,然后每一小段与初始块(IV向量)或者上一段的密文段进行异或运算后,再与密钥进行加密,第一个数据块进行加密之前需要用初始化向量IV进行异或操作。  

image

(此图和以上原理参考于网络)

(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)以解密方的密钥和向量为正确的标准,加密方的密钥正确,但向量不正确
image
2)以加密方的密钥和向量为正确的标准,解密方的密钥正确,但向量不正确
image
以上两种异常情况,使用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
(此图和以上方法参考于网络)


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:simamengde

查看原文:有关AES的CBC模式加解密安全漏洞的实践

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

642 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传