这个是现代密码学课的作业,因为还是挺好玩的而且我还把内容给忘掉了,所以说重写一遍记一下,针对 AES 不同模式的攻击还要分上好几次来写。

# AES ECB mode

众所周知,例如 AES-128 是通过 128 bit 的密钥,加密 128 bit 的明文数据生成 128 bit 的密文块。但是这并不能应用,因为明文长度并不一定是 128bit,实际的明文长度可能会有各种不同的情况。

因此,我们需要一些不同的方式来利用 AES 算法,使得 AES 能够加密任意长度的明文。其中的一种方法是 ECB(Electronic Code Book)模式,即将明文分成若干 128bit 的分组,并对各个分组采用密钥分别进行加密。即如图所示。

这种方式属于是比较简单、容易实现的,但是相应的,这种方式也有比较明显的风险。

对于相同的两段明文m1=m2m_1=m_2,很显然的,加密之后的结果EECB(k,m1)=EECB(k,m2)E_{ECB}(k, m_1)=E_{ECB}(k, m_2),这就带来了泄漏明文消息的风险,对有较多重复内容的明文来说尤其严重,例如图像中常有的纯色色块、音频中的静音等,都会在密文中表现为重复的同样的数据块。一个潜在的攻击者对密文进行分析,即使他无法对密文进行解密,也能够分析出其中一些潜在信息。如图所示,尽管图片经过了加密,但是仍然可以看出其大致轮廓。

很明显,ECB 模式对于选择明文攻击(CPA)来说是并不安全的。

# ECB Byte-at-a-time Attack

ECB Byte-at-a-time Attack 实际上是一种选择明文攻击的手段,我们下面来说明这个攻击的原理。

首先是一些和 ECB 具体实现相关的内容。

# PKCS #7 Padding

前面我们提到了,AES 的加密块长度为 128 比特,那么对于一块实际长度不足 128 比特的明文块,我们就需要通过某种方式将其转换为长度为 128 比特。

PKCS #7 是 Cryptographic Message Syntax Standard ,其中指定了块密码的填充方式,即使用需要填充的长度 paddingSizeASCII 值重复填充,如果块的长度刚好为 128 比特(16bytes),那么就填充 16 个 0x16

例如对于长度为 13 的消息 123456789abcd ,通过 PKCS #7 填充的结果就是 123456789abcd\x03\x03\x03 。对于长度为 16 的消息 0123456789abcdef ,填充的结果是 0123456789abcdef\x10\x10\x10...\x10 ,其中重复 16 个 \x10

PKCS #7 目前广泛应用在各种密码协议中,例如 OpenSSL 中默认也就使用了 PKCS #7.

而对于已经 padding 的数据,只需要检查后面 paddingSize 位是否都是 padding 的内容并将 padding 删除就可以恢复原始数据。

如果发现密文的 padding 无效,加密工具一般会抛出一个 Padding Invalid 异常。

# ECB Byte-at-a-time Attack 原理

我们假设这样的一个场景,用户输入一串数据msgmsg 传给服务器。服务器在这段数据后面追加一段秘密数据secretsecret,将这部分内容使用密钥keykey 进行加密得到EECB(key,msgsecret)E_{ECB}(key, msg||secret),传回客户端。

Error: Could not find Chrome (ver. 115.0.5790.98). This can occur if either
 1. you did not perform an installation before running the script (e.g. `npm install`) or
 2. your cache path is incorrectly configured (which is: C:\Users\pc\.cache\puppeteer).
For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.

那么很显然,客户端没有解密密钥,自然是没有办法将这段数据解密得到secretsecret 的,那么有没有办法去获取这段数据呢?

既然用户能够构造输入的msgmsg,那么这里可以是一种选择明文攻击的场景,而 ECB 模式显然并不是 CPA 安全的,这里肯定是存在一种攻击方式能够破解这个加密体制。

有一个思路就是想办法去一点点地泄漏后面secretsecret 的信息。来看这样的过程:

我们首先确定块密码的块长度 block_size ,接下来填充 block_size-1 的无用字符。
对于 AES 来说,我们先填充 15 个 A ,得到 AAAAAAAAAAAAAAA 。接下来,我们列举这个块的最后一个字节的内容,从 0 到 255 进行枚举: AAAAAAAAAAAAAAA\x00AAAAAAAAAAAAAAA\x01 ,..., AAAAAAAAAAAAAAA\xff ,分别发送给服务器,并且得到对应的密文的第一块。这样子,我们就能够得到 256 个长度的明 - 密文对(mi,ci)(m_i,c_i)

BLOCK 1          BLOCK 2           ---- BLOCK INDEX
 0123456789abcdef 0123456789abcdef  ---- INDEX
+----------------+----------------+
|AAAAAAAAAAAAAAA.|secret secret se| ---- MESSAGE
+----------------+----------------+

我们截取的密文的第一块也就是我们列举的块加密之后的结果。

接下来,将前面填充的 15 个 A 发送给服务器,得到密文:

BLOCK 1          BLOCK 2           ---- BLOCK INDEX
 0123456789abcdef 0123456789abcdef  ---- INDEX
+----------------+----------------+
|AAAAAAAAAAAAAAAs|ecret secret sec| ---- MESSAGE
+----------------+----------------+
                ^ the first byte of secret

这样子密文第一块的最后一个字节也就是秘密 secret 的第一个字节,我们将密文第一块和前面生成的明 - 密文对(mi,ci)(m_i,c_i) 进行对比,就能找到秘密 secret 的第一个字节。

同样的,继续可以破解后面的字节:

BLOCK 1          BLOCK 2           ---- BLOCK INDEX
 0123456789abcdef 0123456789abcdef  ---- INDEX
+----------------+----------------+
|AAAAAAAAAAAAAAse|cret secret secr| ---- MESSAGE
+----------------+----------------+
 already known ^^ the second byte of secret

以此类推,能够破解出整个 secret

下面是实现过程:

def byte_at_a_time_ECB_decrypt() -> str:
    m_len, _, block_size = detect_len()
    random_index, random_padding_size = detect_random_text_len(block_size)
    recovered_list = [0 for i in range(block_size - 1)]
    for i in range(m_len):
        d = {}
        for j in range(256):
            m = recovered_list[i : i + block_size - 1]
            m.append(j)
            m = list_to_bytes(m)
            c = target_ECB(m)[0 : block_size] # target ECB Server
            d[c] = m
        m = [0 for j in range(block_size - i % block_size - 1)]
        c = target_ECB(list_to_bytes(m))[block_size * (i // block_size) : block_size * (i // block_size + 1)]
        recovered_list.append(d[c][-1])
    ans = list_to_bytes(recovered_list[block_size - 1:])
    return ans.decode()
此文章已被阅读次数:正在加载...更新于