Learning Man's Blog

Web(CTF)中的密码学 (一)

字数统计: 2.5k阅读时长: 11 min
2019/04/27

常用模式

  • ECB 电子密码本
  • CBC 密码块链接
  • PCBC 填充密码块链接
  • CFB 密文反馈
  • OFB 输出反馈
  • CTR 计数器模式

0x01 ECB 模式

最简单的加密模式即为电子密码本(Electronic codebook,ECB)模式。需要加密的消息按照块密码的块大小被分为数个块,并对每个块进行独立加密。

攻击点

问题在于同样的明文块会被加密成相同的密文块,通过可控的一个或多个连续的明文-密文组,就可以爆破获取明文信息

练习题

2018 DDCTF 通信安全

#!/usr/bin/env python
import sys
import json
from Crypto.Cipher import AES
from Crypto import Random

def get_padding(rawstr):
    remainder = len(rawstr) % 16
    if remainder != 0:
        return '\x00' * (16 - remainder)
    return ''

def aes_encrypt(key, plaintext):
    plaintext += get_padding(plaintext)
    aes = AES.new(key, AES.MODE_ECB)
    cipher_text = aes.encrypt(plaintext).encode('hex')
    return cipher_text

def generate_hello(key, name, flag):
    message = "Connection for mission: {}, your mission's flag is: {}".format(name, flag)
    return aes_encrypt(key, message)

def get_input():
    return raw_input()

def print_output(message):
    print(message)
    sys.stdout.flush()

def handle():
    print_output("Please enter your Agent ID to secure communications:")
    agentid = get_input().rstrip()
    rnd = Random.new()
    session_key = rnd.read(16)

    flag = 'DDCTF{87fa2cd38a4259c29ab1af39995be81a}'
    print_output(generate_hello(session_key, agentid, flag))
    while True:
        print_output("Please send some messages to be encrypted, 'quit' to exit:")
        msg = get_input().rstrip()
        if msg == 'quit':
            print_output("Bye!")
            break
        enc = aes_encrypt(session_key, msg)
        print_output(enc)

if __name__ == "__main__":
    handle()
  1. 通过 session_key 和 func:aes_encrypt 可以看出使用 AES-ECB-128位分组加密

  2. 紧接着通过控制 Agent ID 长度,发现在长度 7 和 8 的时候密文多了一组,说明在 agentID 长度为 7 时 msg 整体长度恰好控制在 16 的倍数,由此可以推算出 flag 长度为 39

Please enter your Agent ID to secure communications:
1234567
52a3087da8ebda8b98f678fa325d9fd863aaa9be12cdf29458df5c6db7aea412bf47046149c94a03cb349cc0b763add7577c545aa291371626c900fbf85f56afc37de41da81528ca079872d23e1f5a6d96f78a38752d916fdf1884ebe043aadb

Please enter your Agent ID to secure communications:
12345678
02a44fd6291280a9703292edf03a20dfea822c8827a5b895873e61bb82f712c9abb72b8b4a2f36c153f69fda78fbe7e44adaf422a247e8b5ff81e28a4aff34cd72a00a16cbf5570ce8f25c0b6aa660068d60debaf3d066dc16ea2d1374d2745dfd58238da81647c87a75d13d8e33d3ce

对应的明文分组

+    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +
+----------------+----------------+----------------+----------------+----------------+----------------+
|Connection for m|ission: 1234567,| your mission's |flag is: DDCTF{.|................|...............}|
+----------------+----------------+----------------+----------------+----------------+----------------+----------------+
|Connection for m|ission: 12345678|, your mission's| flag is: DDCTF{|................|................|}               |
+----------------+----------------+----------------+----------------+----------------+----------------+----------------+

长度计算

  • len(ALL) = 192/2 = 96
  • len(Msg) = len(Connection for mission: , your mission's flag is:) + len(AgentID) = 50 + 7 = 57
  • len(Flag) = len(ALL) - len(Msg) = 39
  1. 此时,通过控制 Agent ID 内容长度,我们可以控制分组,且只有 flag 是未知的,那么通过控制在分组内flag的内容长度,比对相同组长度的密文是否一致,就可以通过轮询一位一位将其爆破出来
Please enter your Agent ID to secure communications:
123456789012345678901234567890123456789012345
441a90ff809cd834ac01ebc517e53cd9124a19778e5251394356bcc4bfc530c129db288c9fb7367737fc5de6061b9efb04cdfd6bce90d8e551e416fca04439f6d7d0209205014da5d4071b862e80fe9f5633dee51fef20b9c3c3a68236ac2fa3617e7dfee627248729c38b12557d8f80c3290b0f8ea9049823ad05d5030dd6cd78a96f92646e400ca6221f5a41af8aaf
Please send some messages to be encrypted, 'quit' to exit:
Connection for mission: 123456789012345678901234567890123456789012345, your mission's flag is: D
441a90ff809cd834ac01ebc517e53cd9124a19778e5251394356bcc4bfc530c129db288c9fb7367737fc5de6061b9efb04cdfd6bce90d8e551e416fca04439f6d7d0209205014da5d4071b862e80fe9f5633dee51fef20b9c3c3a68236ac2fa3

对应明文分组如下

+    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +    16 Bytes    +
+----------------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+
|Connection for m|ission: 12345678|9012345678901234|5678901234567890|123456, your mis|sion's flag is: |DDCTF{..........|................|......}         |
+----------------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+
|Connection for m|ission: 12345678|9012345678901234|5678901234567890|12345, your miss|ion's flag is: D|DCTF{...........|................|.....}          |
+----------------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+----------------+

0x02 CBC 模式

在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量(IV)。

若第一个块的下标为1,则CBC模式的加密过程为

Ci=EK(PiCi1)


C0=IV

而其解密过程则为
Pi=DK(Ci)Ci1


C0=IV


CBC是最为常用的工作模式。它的主要缺点在于加密过程是串行的,无法被并行化,而且消息必须被填充到块大小的整数倍。解决后一个问题的一种方法是利用密文窃取

有三种密文窃取模式,具体描述

  1. CBC-CS1:最末两个块做挪动和特殊处理。当最末分组完整时,等价于CBC。
  2. CBC-CS2:当最末分组不完整时,把CBC-CS1的最末两个块交换位置;否则,保持CBC-CS1处理结果。
  3. CBC-CS3:无条件将CBC-CS1的最末两个块交换位置。

攻击点

解密时,密文中一位的改变只会导致其对应的明文块完全改变和下一个明文块中对应位发生改变,不会影响到其它明文的内容

  1. 修改 IV,可直接影响第一个明文分组
  2. 修改 Ci1 会影响到 Pi1 以及 Pi

假设

  • A=DK(Ci)
  • B=Ci1
  • T=Pi
  • X=

则有以下等式推导

T=AB –>> ABT=0 –>> ABTX=X

即当 B=BTX时,即可通过解密获取明文 X

练习题

待定

0x03 Padding Oracle Attack

CBC 模式下存在的攻击方法,使用PKCS5和PKCS7 Padding,然后以完整分组进行 CBC,实际上Padding Oracle不能算CBC模式的问题,它的根源在于应用程序对异常的处理反馈到了用户界面

PKCS#5在填充方面,是PKCS#7的一个子集:

  • PKCS#5只是对于8字节(BlockSize=8)进行填充,填充内容为0x01-0x08;
  • PKCS#7不仅仅是对8字节填充,其BlockSize范围是1-255字节。

攻击点

参考 Automated Padding Oracle Attacks With PadBuster

  1. 有以下链接 http://sampleapp/home.jsp?UID=7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6

    CBC PKCS#5模式下IV为前八字节即0x7B216A634951170F

  2. 加密过程,可见填充了0x0505050505

  3. 解密过程,通过解密后验证末尾 Padding 长度与值是否匹配

    到这里,因为IV可控,因为解密异常会返回给我们,所以如果能推测Intermediary Value,就可以获取明文

  4. 问题的关键在于如何获取Intermediary Value

    a. 向服务器发送请求,将 IV 全部设为0x00,并只保留第一个密文块,最终链接为

    http://sampleapp/home.jsp?UID=0000000000000000F851D6CC68FC9537

    对应解密过程

    因 Padding 校验错误,服务器就会报错

    b. 后面依次增大 IV,进行探测

    http://sampleapp/home.jsp?UID=0000000000000001F851D6CC68FC9537

    因为Intermediary Value是不变的,通过最多尝试0xFF次就能使最后的 Padding 为 0x01,例如使 IV 为0x000000000000003C

    是不是当我们递增初始向量最后一位时,如果碰到服务器返回200时,必然Padding最后一位是0x01呢??答案并不是

    比如,当中间值最后两位是0x02 0x00,而我们测试的初始向量最后两位是0x00 0x02时,也就是探测最后一位是0x02时,最终的Padding的最后两位是0x02 0x02,必然也满足Padding规则,服务器当然也会返回200。可见,仅仅依靠我们递增最后一位和测试服务器是否返回200,是没办法确认最终的Padding是0x01的。

    那么怎么才能确认呢?观察异或的过程,可以看出,如果padding是0x01,那么,倒数第二位是什么,并不会影响服务器测试结果(因为改变倒数第二位,仅仅是改变了解码后的明文,会导致明文验证过程异常,但是解密过程是没有任何异常的),此时服务器还是返回200。但如果Padding是0x02 0x02,则改变倒数第二位,会导致解密异常,服务器返回500。因此,我们通过测试倒数第二位,确认了探测过程中得到的Padding是0x01。

    简单来说如果 Padding 是0x01,任意修改倒数第二位不会报错,如果不是,则会报错误

    c. 通过上一步使 Padding 为0x01后,即可推导出Intermediary Value的最后一位

    设有 A 为中间值,B 为 IV,C 为明文(Padding)
    则有 C=AB ==>> A=CB
    即中间值最后一位为 0x3C 0x01 = 0x3D

    d. 为了碰撞倒数第二位使 Padding 为0x0202,先将 IV 最后一位置为0x3F(0x3D0x02),再轮询倒数第二位

    e. 依次类推,获取所有中间值

    f. 通过上面即可获取第一个块的中间值,结合上一个块的密文(亦或)可知第一个块的明文信息,依次类推就可以获取后面的中间值,再得到明文

  5. 为修改目标明文

DK(Ci)Cii=Xi


DK(Ci)(CiiPiXi)=Xi

Cii={Ci1PiXii = NPiDK(Ci)otherwise

其中DK(Ci)即为中间值,而只有最后一组的中间值(DK(CN))是固定不变的,前面的中间值都需要通过 Padding Oracle 攻击重新获取,直到更新 C0 即 IV

练习题

Admin身份伪造:http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/home

# coding: utf-8
import requests
import sys

# 预设置
apiUrl = 'http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/gen_token'
method = 'GET'
headers = {}
cookies = {}

length = 16  # bytes
checkStr = 'decrypt err~'
originCipher = 'UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF'.decode('base64')
originPlain = '{"id":100,"roleAdmin":false}'
targetPlain = '{"id":100,"roleAdmin":true}'


# 自动 padding
def auto_padding(plain):
    return plain + (chr(length - len(plain) % length) * (length - len(plain) % length))


# 自动预处理
iv = originCipher[:length]
originPlain = auto_padding(originPlain)
targetPlain = auto_padding(targetPlain)
lenGroup = len(originCipher) / length
assert (lenGroup == len(originPlain) / length + 1 == len(targetPlain) / length + 1)


def output(msg):
    sys.stdout.write('\r' + '[' + str((1-float(msg.count(chr(0)))/length)*100) + '%]\t' + msg.encode('hex'))
    sys.stdout.flush()


# 字符串亦或
def xor_string(a, b):
    return ''.join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])


#  需自行修改此函数 data 所在位置以及判断条件
def request_api(data):
    if method == 'GET':
        # rep = requests.get(apiUrl, params=data, headers=headers, cookies=cookies)
        rep = requests.get(apiUrl, headers=headers, cookies={'token': data.encode('base64')[:-1]})
    else:
        rep = requests.post(apiUrl, data=data, headers=headers, cookies=cookies)
    if checkStr not in rep.content:
        return True
    return False


# 推测中间值
# pre -> None 反向推密文;not None 正向推明文
def padding_oracle(cipher, CN, pre=None):
    tmpValue = ''
    pre = originCipher[:(CN-lenGroup)*length] if pre is None else pre
    for i in xrange(1, length+1):
        for v in xrange(0x1, 0x100):
            tmp = chr(0)*(length-i) + chr(v) + xor_string(tmpValue, chr(i)*(i-1))
            payload = pre + tmp + cipher
            output(payload)
            if request_api(data=payload):
                tmpValue = chr(i ^ v) + tmpValue
                print i, v
                break
    return tmpValue


def get_index(groupIndex):
    return groupIndex*length


def general_cipher():
    last_index = get_index(lenGroup-1)
    last_cipher = originCipher[last_index:]
    tmp = xor_string(originCipher[get_index(lenGroup-2):last_index], originPlain[get_index(lenGroup-2):])
    pre_cipher = xor_string(tmp, targetPlain[get_index(lenGroup-2):])
    new_cipher = pre_cipher + last_cipher

    if lenGroup >= 3:
        for i in reversed(xrange(0, lenGroup-2)):
            tmp = padding_oracle(pre_cipher, i)
            pre_cipher = xor_string(tmp, targetPlain[get_index(i):get_index(i+1)])
            new_cipher = pre_cipher + new_cipher
    return new_cipher


# 解密可以是并行的,这里为简便使用单线程
def crack_cipher():
    plain_text = ''
    for i in xrange(1, lenGroup):
        cipher = originCipher[get_index(i):get_index(i+1)]
        tmp = padding_oracle(cipher, i, originCipher[:get_index(i-1)])
        plain_text += xor_string(tmp, originCipher[get_index(i-1):get_index(i)])
    return plain_text


if __name__ == '__main__':
    try:
        # 正向解密
        # new_string = crack_cipher()
        # 反向加密
        new_string = general_cipher().encode('base64')[:-1]
        print '\n [+] New Cipher -> ' + new_string
    except KeyboardInterrupt:
        exit(0)

参考资料

  1. http://momomoxiaoxi.com/2016/12/08/WebCrypt/
CATALOG
  1. 1. 0x01 ECB 模式
  2. 2. 0x02 CBC 模式
  3. 3. 0x03 Padding Oracle Attack
  4. 4. 参考资料