密码学之对称加密算法:AES
对称加密算法很多AES(AES 加密又分为 ECB、CBC、CFB、OFB 等几种), DES,3DES,RC2,RC4,RC5等。下面从算法原理、应用和安全性三个方面来介绍AES加密算法。
1、AES算法原理
AES (Advanced Encryption Standard)加密算法是众多对称加密算法中的一种,它是用来替代之前的 DES 加密算法的。AES 加密算法的安全性要高于 DES 和 3DES, AES 已经成为了主要的对称加密算法。
1.1 基本介绍
AES加密算法的特点:分组密码体制、Padding、密钥、初始向量IV和四种加密模式。
-
采用分组密码体制
分组密码是将明文消息编码表示后的bit序列,按照固定长度进行分组,在同一密钥控制下用同一算法逐组进行加密,从而将各个明文分组变换成一个长度固定的密文分组序列。
AES中每段数据的分组必须要求是128位16字节,如果最后一组不够16字节就用Padding来将这个段数据填满,然后对每段分组数据进行加密,最后把每段加密后的数据拼起来形成最终的密文。
-
Padding
Padding就是用来填充不满16字节的分组用的。有三种模式PKCS5、PKCS7和NOPADDING。
- PKCS5:指分组数据缺少几个字节数据,就在数据末尾使用最后一个字节内容填充那几个剩余的几个字节。
- PKCS7:用0在末尾填充数据缺少的分组,如最后一个分组缺少7个字节,就用0来填充这7个字节
- NOPADDING:不需要填充。这需要数据发送方必须能保证最后一段数据正好是16个字节。
当密钥填充模式使用PKCS5/PKCS7时,最后一个分组的数据正好是16个A/16个0,这种情况下解密段就分不清楚 这16个字节中哪几个是填充数据,为解决这种情况带来的问题,PKCS5/PKCS7会自动的帮我们在整个数据的分组后再追加一个分组,使用16个A/16个0来填充。
解密端需要使用和加密端相同的Padding模式才能正确解密;开发通常采用PKCS7 Padding模式。
-
初始向量
初始向量IV的作用是使加密更加安全可靠。
使用AES加密时需要主动提供,且只需要提供一个。
初始向量规定为128位16字节
初始向量随机生成,后面每段数据加密向量都是前面一段的密文。
-
密钥
AES 要求密钥长度可以是128位16字节、192位24字节、256位32字节,位数越高,安全性越高,加密效率越低。
通常开发使用128位16字节的密钥。
AES只有一个密码,每个分组加密密钥都相同。
密钥的生成:随机生成初始密钥;密钥扩展(排列、置换、与轮常量异或、生成下一轮密钥序列)
密钥长度的不同影响加密轮次,密钥长度每增加64位,算法的循环次数就增加2轮。如128位加密10次、192位加密12次、256位加密14次。
-
四种加密模式:
AES 一共有四种加密模式,分别是ECB(电子密码本模式)、CBC(密码分组链接模式)、CFB(密文反馈模式)、OFB(输出反馈模式)。一般使用CBC模式。
- ECB:最基本的加密,比其他三种方式安全性稍弱。具体位:将明文切分成若干个128bit,分别加密。(已不被使用)
- CBC: 先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后(第一段明文使用初始向量进行加密),再与密钥进行加密。(用作认证码)
- CFB: 前一个密文分组会被送入密码算法的输入端,再将输出的结果与明文做异或。与ECB和CBC模式只能够加密块数据不同,CFB能够将块密文(Block Cipher)转换为流密文。(用于卫星通信)
- OFB: OFB又称输出反馈模式,前一组密码算法输出会输入到下一组密码算法输入。先用块加密器生成密钥流,然后再将密钥流与明文流异或得到密文流,解密是先用块加密器生成密钥流,再将密钥流与密文流异或得到明文,由于异或操作的对称性所以加密和解密的流程是完全一样的。(认证码)
改变一个明文分组对四种加密模式的影响:ECB和CFB只影响当前分组,CBC和OFB影响当前分组和后续每一个分组。
1.2 加密过程
AES加密过程通过四个步骤实现:字节替换、行移位、列混淆和轮密钥加。以AES-128为例,算法基本流程为:
- 密钥扩展得到每一轮的密钥,将输入复制到状态数组中
- 先进行初始密钥加
- 再进行9轮的字节代换、行移位、列混合、轮密钥加
- 最后一轮进行字节代换、列混合、轮密钥加(没有行移位)
- 将最后的结果复制到输出数组中

分组:按照字节的先后顺序从上到下、从左到右进行排列,作为输入。
-
明文进行分组,每个分组大小必须是128位(16字节,AES是按照字节进行加密的),分组不足128位时需要使用Padding进行填充。
-
密钥也需要进行分组,密钥长度有128位、192位、265位三种,不同密钥长度影响加密轮次。128位加密12轮、192位密钥加密12轮,256位密钥加密14轮。
轮密钥加
轮密钥加算法的输入是两个128位(16字节, 4*4矩阵),轮密钥与状态矩阵进行逐比特异或操作。
首先,初始轮是用输入的密钥作为key[0]进行轮密钥加运算。
后续,每一轮的轮密钥是由初始密钥按照一定算法得到的子密钥,具体为:子密钥生成以列为单位进行,初始的密钥记为W0-W3,称为种子密钥,由种子密钥扩展生成扩展密钥,轮密钥由扩展密钥中抽取生成。
-
当生成列i不等于4的倍数,即生成子密钥的2/3/4列时,根据公式w[i]=w[i-4]⊕w[i-1]生成。
-
当生成列i等于4的倍数时,根据公式w[i]=w[i-4]⊕w’[i]⊕Rcon[j]生成,具体位以下三个步骤:
-
将w[i-1]循环左移一个字节
-
分别对左移后的每个字节进行S盒替换
-
前两步的结果同轮常量Rcon[j]进行异或,每一轮Rcon如下(密钥为128位)
j 1 2 3 4 5 Rcon[j] 01 00 00 00 02 00 00 00 04 00 00 00 08 00 00 00 10 00 00 00 j 6 7 8 9 10 Rcon[j] 20 00 00 00 40 00 00 00 80 00 00 00 1B 00 00 00 36 00 00 00
-
字节替换
对每个分组通过S盒进行分线性变换。替换规律:每个分组中按照一个字节分高4位作为列向量,低4位作为行向量在S盒中查找并替换。例如在某个分组中某字节为0x14,那么前四位的16进制为1,后四位的16进制为4,去查找s盒中的第1行第4列的值,可以看出为0xfa,就把原先的字节0x14替换为0xfa。
解密过程与此相同,唯一就是采用的是逆S盒。

行移位
对一个4*4矩阵,行移位具体为:第一行不变,第二行左移1位、第二行左移2位、第三行左移3位。
列混淆
列混淆是AES算法中最为复杂的部分,属于扩散层,列混淆操作是AES算法中主要的扩散元素,它混淆了输入矩阵的每一列,使输入的每个字节都会影响到4个输出字节。
在加密的正向列混淆中,我们要将输入的44矩阵左乘一个给定的44矩阵。而它们之间的加法、乘法都在扩展域GF(28)中进行(扩展域中加法操作等同于异或运算,而乘法操作需要一个特殊的方式进行处理),不可约多项式为x8+x4+x2+x+1。简单而言就是:
- x * 01,为x本身
- x * 02,x的二进制左移一位(右边补0),如果溢出(即如果x的二进制最高位为1),那么再异或上 1B
- x * 03,结果为 (x * 02) +(这里+指异或,不是简单位相加) x,即,先乘02再异或本身,计算方法和上面一样。
2、Go语言实践
2.1 CBC加密模式
加密程序
func Encrypto(data, key string) string {
//1. 将输入字符串和加密的key转换成字符数组,其中key必须符合密钥格式要求(即只能使用16、24、32字节密钥)
originData := []byte(data)
en_key := []byte(key)
//2. 创建一个cipher.Block接口, 参数key为密钥
block, err := aes.NewCipher(en_key)
if err != nil {
fmt.Printf("NewCipher Error: %v\n", block)
return "error"
}
blockSize := block.BlockSize()
//3.自定义Padding函数对明文进行填充数据
originData = PCKS7Padding(originData, blockSize)
//4.调用NewCBCEncrypter函数来得到一个BlockMode,然后用 BlockMode 来进行加密数据
//NewCBCEncrypter函数的第二个参数即用来设置初始向量
//这里使用了密钥前16字节数据作为初始向量
blockModle := cipher.NewCBCEncrypter(block, en_key[:blockSize]) //cipher实现了标准的块加密模式(接口)
//5. 创建保存加密结果的数组
crypef := make([]byte, len(originData))
//6. 调用BlockMode.CryptBlocks函数加密数据
blockModle.CryptBlocks(crypef, originData)
return base64.StdEncoding.EncodeToString(crypef)
}
//对明文数据进行填充:输入参数为明文字符数组,分组块大小
//
func PCKS7Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
解密程序:
func Decrypto(endata string, enkey string) string {
cyrptedByte, _ := base64.StdEncoding.DecodeString(endata)
key := []byte(enkey)
block, _ := aes.NewCipher(key)
blockSize := block.BlockSize()
blockModle := cipher.NewCBCDecrypter(block, key[:blockSize])
origin := make([]byte, len(cyrptedByte))
blockModle.CryptBlocks(origin, cyrptedByte)
origin = PCKS7UnPadding(origin)
return string(origin)
}
func PCKS7UnPadding(origin []byte) []byte {
length := len(origin)
unpadding := int(origin[length-1])
fmt.Println(length, unpadding)
return origin[:(length - unpadding)]
}
测试程序
func main() {
originData := "hello the world of gopher!"
key := "12345678123456781234567869696969" //必须是16、24、32字节
fmt.Printf("origin data: %v, %v\n", originData, key)
//encrypt
data := Encrypto(originData, key)
fmt.Printf("encrypted data: %v\n", data)
//decrypt
deData := Decrypto(data, key)
fmt.Printf("decrypted data: %v\n", deData)
}
================测试结果==================
origin data: hello the world of gopher!, 12345678123456781234567869696969
encrypted data: BtgnBQgN/LspBbtyPCTLVxcmi/3aQEcAoFAsGKxOu64=
decrypted data: hello the world of gopher!
2.2 CFB 加解密
package cfbmodule
import (
"crypto/aes"
"crypto/cipher"
"fmt"
)
//初始向量
var commIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}
func CfbTest(data, key string) {
oridata := []byte(data)
enkey := []byte(key)
c, err := aes.NewCipher(enkey)
if err != nil {
fmt.Printf("Error: NewCipher %s\n", err)
return
}
//cfb加密
cfb := cipher.NewCFBEncrypter(c, commIV)
ciphertext := make([]byte, len(oridata))
cfb.XORKeyStream(ciphertext, oridata)
fmt.Printf("original data: %s\n", oridata)
fmt.Printf("encode data: %x\n", ciphertext)
//cfb解密
cfbdec := cipher.NewCFBDecrypter(c, commIV)
plaintext := make([]byte, len(ciphertext))
cfbdec.XORKeyStream(plaintext, ciphertext)
fmt.Printf("decode data: %s\n", plaintext)
}
测试程序
func TestCFB() {
data := "hello the world of gopher!"
key := "12345678123456781234567869696969" //必须是16、24、32字节
cfb.CfbTest(data, key)
}
===========测试结果=============
original data: hello the world of gopher!
encode data: a41a812b50f522df3f9dba995553117fc1638b562ef1158463b3
decode data: hello the world of gopher!
3、AES算法安全问题
3.1 AES算法攻击面
CBC Bit翻转攻击
在cbc模式下,前一个分组的密文与当前分组的明文进行异或操作后再加密,这样做增加了破解强度。
在cbc模式的密文中,在不知道密钥的情况下,如果有一组明密文,就可以通过修改密文来使密文解密出来特定位置的字符变成我们想要的字符。
CBC选择密文攻击
通过CBC模式选择密文攻击,可以很快恢复出AES的向量IV。CBC模式下,明文每次加密前都会与IV异或。每组IV都会更新为上一组的密文。如果构造两个相同的C,即带解密的密文为C|C时,通过计算可以得到IV。
CBC Padding oracle攻击
Padding oracle攻击是利用服务器通过对Padding检查时的不同回显进行的。这是一种侧信道攻击。利用服务器对padding的检查,可以从末位开始逐位爆破明文。攻击效果是在不清楚key和IV时,对任意密文进行解密。但是这种攻击需要满足以下两个条件:
- 加密时采用了PCKS5填充,即填充的内容为填充的字符个数。
- 攻击者可以和服务器进行交互,可以获取密文,服务器会返回信息告知客户端padding是否正常。
Feistle结构攻击
在Feistle结构中,如果右边的加密是线性的话,那么可以实现已知明文攻击。
攻击伪随机数发生器
序列密码的设计思想是将种子密钥通过密钥流生成器产生的伪随机序列与明文简单的结合生成密文。将与明文结合的元素称为密钥流,将生成密钥流的部件称为密钥流发生器。序列密码方案是否具有很高的安全性主要取决于密钥流生成器的设计。
对于分组密码,还有复杂的差分攻击、积分攻击等攻击方式,对于序列密码有快速相关攻击等方式。
-----<<CTF 特训营>>---
3.2 AES算法逆向
逆向识别标准的AES算法最关键的是找到置换表s_box,在AES算法中该置换表为固定的256元素数字。
利用PEiD的插件KANAL识别,可以识别到MD5和AES的S盒、逆S盒。

ida pro中的Findcrypt插件:https://github.com/polymorf/findcrypt-yara
Locky勒索软件中的AES算法识别
背景知识:2010年以后,Intel公司生产的大部分CPU包含了一些列用于处理AES加密和解密的指令,这些指令包括生成轮密钥的AESKEYGENASSIST,用于加密的轮函数AESENC,用于最后一轮加密的AESENCLAST等等。这些指令可以极大的提高该CPU处理AES算法的速率。而在Visual Studio中,其Visual C++编译器以内联函数的形式支持使用这些用于AES加解密相关的指令。通过MSDN可以了解到,在wmmintrin.h头文件中_mm_aeskeygenassist函数、_si128_mm_aesenc_si128函数以及_mm_aesenclast_si128函数可以用于使用上述提到的AESKEYGENASSIST、AESENC以及AESENCLAST指令。
Locky AES加密代码片段

TeslaCrypt勒索软件中的AES算法识别
背景知识: 而在实际运用中,更多的是使用查表法进行AES加密的算法。用查表法实现AES算法可以以一种较快的速度完成AES加密和解密,是一种以存储空间兑换消耗时间的方法。根据AES算法的轮函数,可以建立4张置换表,通过查询这4张表,并与该轮的密钥进行异或,可以直接得到该轮加密的结果。
在TeslaCrypt勒索软件中,就建立了如下的一张表:

资料来源:逆向分析及识别恶意代码中的AES算法

浙公网安备 33010602011771号