CBC字节翻转

又有半年没更博客233,主要是懒,笔记和文还是有写的,只不过我开了一个私密的语雀,文都放到语雀上了。现在从语雀那扒拉几篇文,丢到博客上吧。

概念

简单来说CBC是一种加密模式(我也没学太深),全称 cipher block chaining。即块加密链。

加密时,明文会按照约定的块大小,分割成各个小块,并依次加解密。如下图的加解密过程所示。图源 CBC byte flipping attack—101 approach

简单来说。在CBC模式中,加密一段明文,需要四要素:

  • 明文
  • 固定块大小
  • 加密key,长度为块大小的长度
  • iv - 随机生成的一串字节,长度也是块大小的长度

块大小是可以指定的,常用的是16字节

加密算法如下,以块为单位(偷的CBC byte flipping attack—101 approach

1
2
Ciphertext-0 = Encrypt(Plaintext XOR IV)
Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1)

第一段加密的块,加密时的参数是 明文块 异或 iv

第二段及之后的块,加密时的参数是 明文块 异或 前一段加密块。即把前一段加密块当iv使。

解密算法如下,同样也是以块为单位

1
2
3
Plaintext-0 = Decrypt(Ciphertext) XOR IV

Plaintext-N= Decrypt(Ciphertext) XOR Ciphertext-N-1

第一段解密的块,解密时的参数是 密文块 异或 iv

第二段及之后的块,解密时的参数是 密文块 异或 前一段加密块。即把前一段加密块当iv使。

注意解密时异或的依然还是加密块。

攻击方式

由于在第二段密文块及之后的密文块中,都是依据前一个密文块来做解密的。若修改了中间一个密文块的值,则解密出来的明文当然也会改变。我们便可凭借这种方式,修改前一块密文,控制下一块的密文的解密值。

Demo

我们来写个简单的AES CBC加密的demo。用的块长度是16(key和iv长度保持为16即可):

ps: 由于AES/CBC是前后端密文强关联,所以要求加密密文16字节对齐。Demo中用的加密模式是”NoPadding”,即不自动进行字节填充对齐,就需要我们手动将密文填充成16字节对齐的形式。”NoPadding”模式不会对字节自动填充,初学应该好理解一点。CBC字节翻转是可以对CBC的其他模式进行攻击的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static void main(String[] args) throws Exception{
// Generate PlainText, Cipher key and iv
String plainText = "Hello World! How Are You?";
byte[] key = "bbbbbbbbbbbbbbbb".getBytes();
byte[] iv = "aaaaaaaaaaaaaaaa".getBytes();
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
System.out.print("PlainText bytes: ");
for (byte aByte : plainText.getBytes()) {
System.out.print(aByte + " ");
}
System.out.println();

//encrypt
Cipher cipherEncode = Cipher.getInstance("AES/CBC/NoPadding");
cipherEncode.init(1, secretKeySpec, ivParameterSpec); //1 is encode mode
byte[] bytesEncode = cipherEncode.doFinal(plainText.getBytes());
System.out.print("EnCrypt bytes: ");
for (byte b : bytesEncode) {
System.out.print(b + " ");
}
System.out.println();

//decrypt
Cipher cipherDecode = Cipher.getInstance("AES/CBC/NoPadding");
cipherDecode.init(2, secretKeySpec, ivParameterSpec); //2 is decode mode
System.out.print("ReDeCrypt bytes: ");
byte[] bytesDecode = cipherDecode.doFinal(bytesEncode);
for (byte b : bytesDecode) {
System.out.print(b + " ");
}
System.out.println();
System.out.print(new String(bytesDecode));
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PlainText bytes: 72 101 108 108 111 32 87 111 114 108 100 33 32 72 111 119 32 65 114 101 32 89 111 117 63 
PlatinText String: Hello World! How Are You?
EnCrypt bytes: 90 40 -75 -65 -48 52 123 46 116 66 -13 -54 103 -120 5 -118 -16 97 -40 -40 -117 82 -23 31 80 7 -100 76 -78 66 -1 113
ReDeCrypt bytes: 72 101 108 108 111 32 87 111 114 108 100 33 32 72 111 119 32 65 114 101 32 89 111 117 63 65 65 65 65 65 65 65

----按16字节分割
PlainText bytes:
72 101 108 108 111 32 87 111 114 108 100 33 32 72 111 119
32 65 114 101 32 89 111 117 63 65 65 65 65 65 65 65

EnCrypt bytes:
90 40 -75 -65 -48 52 123 46 116 66 -13 -54 103 -120 5 -118
29 69 117 22 -89 50 112 -109 9 18 -81 -116 -7 -54 -85 52

ReDeCrypt bytes:
72 101 108 108 111 32 87 111 114 108 100 33 32 72 111 119
32 65 114 101 32 89 111 117 63 65 65 65 65 65 65 65

值得注意的是,密文字节将会按照块长度进行对齐。原文总长25,密文总长32。按16字节对齐。

根据CBC加密模式下的加密流程,第一段密文由于是受iv控制的,还原时第一段不可控。只有第二段及之后的密文段才可以在解密时控制。

捋一捋这一段Demo中,明文第2段第0字节的字符32的加解密流程:

  • 加密: 用公式Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1),得到29=encrypt(32^90)
  • 解密: 用公式Plaintext-N= Decrypt(Ciphertext) XOR Ciphertext-N-1,得到32=decrypt(29)^90
    • 解密时,忽略decrypt()函数,可以拆分成这样: 32=(32^90)^90
    • 90^90=0,则32^0就能还原回明文32

在解密过程中,decrypt()调用完后会返回32^90。即第2段明文第0字节异或加密时1段密文第0字节。由于这个操作读取的是加密时数据,作为攻击者角度无法控制;但后头异或的90,即执行解密时的第1段密文第0字节,是我们可控的,我们只需要修改传入解密函数的密文即可。

这里思考一个问题: 我们如何使得32^90^XXX的结果控制为任意输出呢?

我们来看个异或中的一个特性:相同字节异或,结果是0

1
2
3
4
byte a = 10;
byte b = 22;
byte c = (byte)(a^b); //28
byte d = (byte)(a^b^c); //0 //我们把a^b当作一个整体,就好理解了。a^b其实就是c,c^c自然就是0

既然x^x返回的结果是0。那如果我们控制上文这个XXX的值为32^90,不就能控制这一个字节的解密值为0了嘛?其中3290这两个字节都是可预测的,一个是原始明文,一个是密文。

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws Exception{
//Generate PlainText, Cipher key and iv
.....

//encrypt
Cipher cipherEncode = Cipher.getInstance("AES/CBC/NoPadding");
cipherEncode.init(1, secretKeySpec, ivParameterSpec); //1 is encode mode
byte[] bytesEncode = cipherEncode.doFinal(plainText.getBytes());

//Change the "1" pos of second plain block byte to "0". In the whole plainText the pos is "17"
bytesEncode[1] = (byte)(bytesEncode[1] ^ plainTextBytes[17]); //[!]

System.out.print("EnCrypt bytes: ");
for (byte b : bytesEncode) {
System.out.print(b + " ");
}
System.out.println();

//decrypt
......
}

OUTPUT:

PlainText bytes: 72 101 108 108 111 32 87 111 114 108 100 33 32 72 111 119 **32 **65 114 101 32 89 111 117 63 65 65 65 65 65 65 65
EnCrypt bytes: 90 105 -75 -65 -48 52 123 46 116 66 -13 -54 103 -120 5 -118 29 69 117 22 -89 50 112 -109 9 18 -81 -116 -7 -54 -85 52
ReDeCrypt bytes: 27 -126 -93 121 65 -48 81 -89 -14 15 62 53 -106 41 -44 86 32 0 114 101 32 89 111 117 63 65 65 65 65 65 65 65

看到[!]这一段。我们预期修改明文下标为17的值为0。根据前面的推测可知,最终解密还原时是通过异或明文字节加密时前一段密文字节解密时前一段密文字节得到的。我们将解密时前一段密文字节设置为”明文字节异或加密时前一段密文字节“,即可让解密结果为0

接下再看到异或中的一个小知识点:0异或任何数,结果都是那个数:

1
2
3
4
byte a = 10;
byte b = 22;
byte c = (byte)(0^a); //10
byte d = (byte)(0^b); //22

既然上文我们都能控制解密结果为0了,为何不能跟进一步,让解密结果为任意数值呢?很简单,只要我们在0的基础上再异或多一个数值即可。

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws Exception{
//Generate PlainText, Cipher key and iv
.....

//encrypt
Cipher cipherEncode = Cipher.getInstance("AES/CBC/NoPadding");
cipherEncode.init(1, secretKeySpec, ivParameterSpec); //1 is encode mode
byte[] bytesEncode = cipherEncode.doFinal(plainText.getBytes());

//Change the "1" pos of second plain block byte to "0". In the whole plainText the pos is "17"
//修改
//"bytesEncode[1] ^ plainTextBytes[17]" returns '0'. "'0' ^ 'x'" returns 'x' so that we can control the decrypt result.
bytesEncode[1] = (byte)(bytesEncode[1] ^ plainTextBytes[17] ^ 'x'); //[!]

System.out.print("EnCrypt bytes: ");
for (byte b : bytesEncode) {
System.out.print(b + " ");
}
System.out.println();

//decrypt
......
}

OUTPUT:

PlainText bytes: 72 101 108 108 111 32 87 111 114 108 100 33 32 72 111 119 32 65 114 101 32 89 111 117 63 65 65 65 65 65 65 65
EnCrypt bytes: 90 105 -75 -65 -48 52 123 46 116 66 -13 -54 103 -120 5 -118 29 69 117 22 -89 50 112 -109 9 18 -81 -116 -7 -54 -85 52
ReDeCrypt bytes: -87 -95 -80 -82 25 -34 17 -119 -112 -56 69 103 21 -28 117 -65 32 120 114 101 32 89 111 117 63 65 65 65 65 65 65 65

可以看到,第17字节在解密时已经被成功控制为字符'x'

作用

  • 鉴权绕过 - CTF用的多
  • 和Padding Oracle攻击配合,修改或增加密文
  • ……

Reference

CBC byte flipping attack—101 approach

https://wooyun.js.org/drops/CBC%E5%AD%97%E8%8A%82%E7%BF%BB%E8%BD%AC%E6%94%BB%E5%87%BB-101Approach.html