前段时间学习了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个字节,我们要修改的密文在第三个分组,通过前面的表述,得知我们需要改第二个分组。
我们先输入"*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。
后记:
关于出题过程当中出现的问题:
- 建立客户端与服务器之间的交互。
一开始参考了网上关于socket的使用方法,写进代码之后发现服务端只能与单个客
户端通信,不满足实验要求,然后在教员和同学的帮助下,使用thread,启用多个线程,每有一个客户端连接进来,都会启动一个新的线程,这样就解决了服务端与客户端的交互问题。 - 全局变量在各个用户端的数据混乱
由于一开始的代码中client_socket是全局变量,导致各个客户端在client_socket.sent
发送的时候数据会混乱。解决问题时,所有含有client_socket变量的函数,都将它作为参数传进函数。 -
程序在某个地方卡住,后面的信息打印不出来。
解决:在服务端程序卡住的代码附近写一些打印函数,先定位到data = data[8:].decode(‘hex’)。
在这行代码前后,检查data的类型以及长度,发现长度有问题。没有去掉字符串末尾的空格或者换行符。
加上data=data.strip()。解决了此问题。