前段时间参与了0CTF的竞赛,其中有一道关于密码学的题目,学习到不少知识。
这道题目应用到的主要知识点是AES加密中的CBC模式,这一点在《密码编码学与网络安全》课程中提到,我想起了一个有意思的事情,在这里先分享一下:据说在我们刚上这门课程的时候,赵淦森教授在第一节课说,谁能上黑板写出AES的5种加密模式的英文缩写以及工作原理,这门课的平时成绩就为满分,遗憾的是我的这门课的老师不是赵淦森教授。囧
这里简单的说一下关于CBC的工作原理:
Cipher Block Chaining,CBC模式,这种模式下加密算法的输入是当前明文组和上一个密文组的异或,使用的密钥k是相同的,这就相当于将所有的明文组链接在一起。加密算法的每次输入与明文没有固定的关系,因此重复的分组经过加密后这种特性将会消除。
那么在0CTF的题目中,我们能够拿到服务器后台的源代码,如下:
#!/usr/bin/python -u
from Crypto.Cipher import AES
from hashlib import md5
from Crypto import Random
from signal import alarm
BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-ord(s[-1])]
class Scheme:
def __init__(self,key):
self.key = key
def encrypt(self,raw):
raw = pad(raw)
raw = md5(raw).digest() + raw
iv = Random.new().read(BS)
cipher = AES.new(self.key,AES.MODE_CBC,iv)
return ( iv + cipher.encrypt(raw) ).encode("hex")
def decrypt(self,enc):
enc = enc.decode("hex")
iv = enc[:BS]
enc = enc[BS:]
cipher = AES.new(self.key,AES.MODE_CBC,iv)
blob = cipher.decrypt(enc)
checksum = blob[:BS]
data = blob[BS:]
if md5(data).digest() == checksum:
return unpad(data)
else:
return
key = Random.new().read(BS)
scheme = Scheme(key)
flag = open("flag",'r').readline()
alarm(30)
print "Welcome to 0CTF encryption service!"
while True:
print "Please [r]egister or [l]ogin"
cmd = raw_input()
if not cmd:
break
if cmd[0]=='r' :
name = raw_input().strip()
if(len(name) > 32):
print "username too long!"
break
if pad(name) == pad("admin"):
print "You cannot use this name!"
break
else:
print "Here is your secret:"
print scheme.encrypt(name)
elif cmd[0]=='l':
data = raw_input().strip()
name = scheme.decrypt(data)
if name == "admin":
print "Welcome admin!"
print flag
else:
print "Welcome %s!" % name
else:
print "Unknown cmd!"
break
题目的大意是,我们可以输入两个指令,分别是r和l。其中r用于注册,输入用户名之后,服务器会返回一段密文SCR
;而输入l后,服务器会要求输入一段字符串,并将其进行解密,如果解密的结果为admin
,那么服务器就会返回flag。
我们先观察这里的加密函数Scheme.encrypt(self,raw)
,其中raw
是明文。函数将明文进行填充至16位,然后进行md5
的计算并拼接在一起,然后利用AES-CBC
进行加密。下面的图式总结了这个函数的作用:
而解密函数Scheme.decrypt(self,enc)
的原理则是先提取前16位的iv向量,然后对后面的数据进行AES-CBC
解密。其中函数将检查解密出来的数据中,提取前16位作为data
,后面的数据作为checksum
,如果满足md5(data)==checksum
则返回data
。注意这里返回的结果将会是对应加密的内容,在返回前已经进行了去除填充的操作。下面的图式总结了这个函数的作用:
因为在上面的代码中,我们发现key和iv变量都是临时生成的,不过它们都是可以直接获得。在每一次与服务器交互的过程中,这两个量不影响我们的破解过程。这个问题转化为:如何构造密文,使得服务器能够将其解密得到明文“admin”。
我们首先需要注册一个用户,其中名字是md5[pad("admin")]+"admin"
,这个时候服务器将会返回一段数据给我们,这段数据的内容将会是这样:
当我们需要解密的时候,传入的数据这样构造:
这时就能得到flag,由于比赛用的数据是直接与服务器交互,下面我给出一份简短的代码可以在本地直接完成测试。
#!/usr/bin/python -u
from Crypto.Cipher import AES
from hashlib import md5
from Crypto import Random
from signal import alarm
BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-ord(s[-1])]
class Scheme:
def __init__(self,key):
self.key = key
def encrypt(self,raw):
raw = pad(raw)
raw = md5(raw).digest() + raw
iv = Random.new().read(BS)
cipher = AES.new(self.key,AES.MODE_CBC,iv)
return ( iv + cipher.encrypt(raw) ).encode("hex")
def decrypt(self,enc):
enc = enc.decode("hex")
iv = enc[:BS]
enc = enc[BS:]
cipher = AES.new(self.key,AES.MODE_CBC,iv)
blob = cipher.decrypt(enc)
checksum = blob[:BS]
data = blob[BS:]
if md5(data).digest() == checksum:
return unpad(data)
else:
return
key = Random.new().read(BS)
scheme = Scheme(key)
flag = "this is flag"
alarm(30)
name = md5(pad("admin")).digest()+"admin"
res=scheme.encrypt(name)
data = res[32:64]+res[64:]
name = scheme.decrypt(data)
if name == "admin":
print flag
这是一道关于CBC模式的简单的密码题,攻击的方法属于“选择密文攻击”。这篇文章就讲到这里,谢谢大家。