CBC位反转攻击

前段时间学习了CBC模式的加密,针对其中的位反转攻击,许多比赛的题目中也都有所体现,从一篇博客中找到了一个题目,想自己实现一下。
CBC字节翻转攻击
拿到题目后,我将源码修改了一下,主要是能够在本地建立服务端,与同在一个子网的主机进行交互。
点击下载源码
源码
提取码:did3
将代码在本地端用python编辑器运行,客户端使用 (nc 服务端IP 20001)链接即可。
如果在自己本地运行,就是自己的IP。

简单叙述一下CBC位反转攻击漏洞的来源:


解密

这是CBC模式解密的图解,从图中我们可以看出,我们解密第一段密文(A)的时候,使用Key进行解密 ,然后与初始化向量IV进行异或(XOR)运算,得到明文1(Plaintext_1)。
第二段明文(Plaintext_2)则是将Ciphertext_2用Key解密后(得到 B )与上一段密文分组 A 进行异或运算得到 C 。以此类推。

我们可以看到,解密时后一段的明文是受前一段密文影响的,所谓的位反转攻击就是通过修改前一段的密文,来达到解密时,篡改了后一段明文的一种攻击方式。

举个栗子:Eve想要篡改的是将 C 变成 M 。
我们知道 C = A ^ B
则 B = A ^ C
如果将 A 替换成(A ^ C ),则C = A ^ B = A ^ C ^ A ^ C = 0
那么再为C异或一次M,就是我们想要的明文分组了。

我们来看看源码当中的漏洞出现在什么地方。


def mkprofile(email,client_socket):
    if ((";" in email)):
        return -1
    prefix = "comment1=wowsuch%20CBC;userdata="
    suffix = ";coment2=%20suchsafe%20very%20encryptwowww"
    ptxt = prefix + email + suffix
    #client_socket.send ("����"+encrypt_cbc(KEY, IV, ptxt))
    return encrypt_cbc(KEY, IV, ptxt,client_socket)


def parse_profile(data,client_socket):
    print data,'break 3'
    ptxt = decrypt_cbc(KEY, IV, data.encode('hex'),client_socket)  # ����
    print data, 'break 4'
    ptxt = ptxt.replace(" ", "")  # ���ܺ�ȥ���ո�
    print data,'break 5'
    #client_socket.send(bytes(ptxt))
    if ";admin=true" in ptxt:
        client_socket.send(bytes(FLAG))
        #print FLAG
        return 1
    else:
        client_socket.send(bytes("you are stupid"))
        return 0


def dataReceived(data,client_socket):
    if (data.startswith("getapikey:")):
        data = data[10:]
        resp = mkprofile(data,client_socket)
        if (resp == -1):
            client_socket.send(bytes("No Cheating!\n"))
        else:
            client_socket.send(bytes(resp))
    # Decrypt Ciphertext and "parse" into Profile
    elif (data.startswith("getflag:")):
        client_socket.send(bytes("Parsing Profile...\n"))
        data=data.strip()
        data = data[8:].decode('hex')
        if (parse_profile(data,client_socket) == 1):
            client_socket.send(bytes(FLAG))
        else:
            client_socket.send(bytes("[BLACKBOX] You are a normal user.\n"))
    else:
        client_socket.send(bytes("\nyou should be admin"))


def connectionMade(self,client_socket):
    self.key = os.urandom(16)
    self.iv = os.urandom(16)
    self.client_socket.send_docs()

def body(client_socket,i):
    while True:
        print('start ',i,'thread')
        try:
            client_socket.send(bytes("\nplease input string:\n"))
            data = str(client_socket.recv(1024).decode('utf-8'))
            client_socket.send(bytes("\ncipher:" + mkprofile(data,client_socket) + "\n"))
            dataReceived(data,client_socket)
        except:
            break

server_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_socket.bind(('',20001))
server_socket.listen(128)

i=0
while True:
    try:
        client_socket, client_address = server_socket.accept()
        thread.start_new_thread(body,(client_socket,i))
        i=i+1
    except:
        print 'connect fail'

首先主体是body中的函数,表述了建立连接之后会做的一系列事情:

def body(client_socket,i):
    while True:
        print('start ',i,'thread')
        try:
            client_socket.send(bytes("\nplease input string:\n"))
            data = str(client_socket.recv(1024).decode('utf-8'))
            client_socket.send(bytes("\ncipher:" + mkprofile(data,client_socket) + "\n"))
            dataReceived(data,client_socket)
        except:
            break

在接受了客户端的输入之后,会通过dataReceived()函数,根据输入的不同,走不同的路径。
仔细研究dataReceived()和parse_profile()函数可以发现,我们获取flag的条件是

if (parse_profile(data,client_socket) == 1):
            client_socket.send(bytes(FLAG))
 if ";admin=true" in ptxt:
        client_socket.send(bytes(FLAG))

也就是说,用户的输入要带有";admin=true",这样,用户的输入被服务端接受后进行加密,解密出来的明文才会带有";admin=true",但是尝试着做题的朋友们肯定发现,客户端是不能直接输入";admin=true"的。

那么怎么破解呢,漏洞就在dataReceived()函数中:
与将用户的输入先加密再解密不同,当用户的输入以"getflag:"为开头的时候,在dataReceived()函数中调用的是parse_profile()函数,直接进行解密操作,这样我们就可以精心构造一个密文,让它以"getflag:"作为开头,解密之后就带有";admin=true"就可以得到flag了。

CBC加密模式每组16个字节,我们要修改的密文在第三个分组,通过前面的表述,得知我们需要改第二个分组。


20170811165053720.png

我们先输入"*admin=true"( * 也可以是其他的字符,只要将后面的是admin=true即可),得到一串密文,将其作为data,也就是我们准备开始构造密文的雏形,只要将它对应 * 的一位在解密时解密出" ; "即可。
利用上面提到的方法构造,如下:

# coding=UTF-8
data = 'cb16a54c2fad7eb698eb620e66bd642daed5230138e49c75fd4e12ba0ffbaef38e8082ded7cfb240d086dae2ba1bd32f90d1f5085311101fa437a29c98d2672ba5e0125b8ad88af53ade51adc8ed299468c490b03df1ce5b8bf633201830693d'
data = data.strip()
data = data.decode("hex")
data = list(data)
print data
data[16] = chr(ord("*") ^ ord(";") ^ ord(data[16]))
print "异或",data
data = "".join(data)
print data
data = data.encode("hex")
print data

将得到的data前面加上"getflag:"作为输入,即可得到flag。


image.png

后记:
关于出题过程当中出现的问题:

  1. 建立客户端与服务器之间的交互。
    一开始参考了网上关于socket的使用方法,写进代码之后发现服务端只能与单个客
    户端通信,不满足实验要求,然后在教员和同学的帮助下,使用thread,启用多个线程,每有一个客户端连接进来,都会启动一个新的线程,这样就解决了服务端与客户端的交互问题。
  2. 全局变量在各个用户端的数据混乱
    由于一开始的代码中client_socket是全局变量,导致各个客户端在client_socket.sent
    发送的时候数据会混乱。解决问题时,所有含有client_socket变量的函数,都将它作为参数传进函数。
  3. 程序在某个地方卡住,后面的信息打印不出来。
    解决:在服务端程序卡住的代码附近写一些打印函数,先定位到data = data[8:].decode(‘hex’)。


    image.png

在这行代码前后,检查data的类型以及长度,发现长度有问题。没有去掉字符串末尾的空格或者换行符。
加上data=data.strip()。解决了此问题。

你可能感兴趣的:(CBC位反转攻击)