内容简介:最近,为了防止整天沉迷switch智力降低,于是刷了一道有意思的Crypto题,其中涉及大概2个考点:Hash Length Extension Attacks & CBC Byte Flipping Attack,虽然利用有些麻烦,但是锻炼脑力,从CTF开始~先看主程序代码:大致分为两部分:
前言
最近,为了防止整天沉迷switch智力降低,于是刷了一道有意思的Crypto题,其中涉及大概2个考点:Hash Length Extension Attacks & CBC Byte Flipping Attack,虽然利用有些麻烦,但是锻炼脑力,从CTF开始~
程序分析
先看主程序代码:
if __name__ == '__main__': unprintable = b"" for i in range(256): if chr(i) not in string.printable: unprintable += bytes([i]) alarm(60) s = Sign(urandom(16), urandom(16)) while True: print("Choose:\n[1] Register\n[2] Login") op = input() if op == '1': user = input("Input your username(hex): ") token = s.register(bytes.fromhex(user)) if not token: print("Sorry, invalid username.") else: print("Your token is: %s" % token.hex()) elif op == '2': token = input("Input your token: ") res = s.login(bytes.fromhex(token)) if not res: print("Sorry, invalid token.") elif not res[1]: user = res[0].hex() print("Sorry, your username(hex) %s is inconsistent with given signature." % user) else: user = res[0].strip(unprintable).decode("Latin1") print("Login success. Welcome, %s!" % user) if user == "admin": print("I have a gift for you: %s" % FLAG) else: print("See you") break
大致分为两部分:
1.注册功能输入用户名,程序会计算出一个token给你。
if op == '1': user = input("Input your username(hex): ") token = s.register(bytes.fromhex(user)) if not token: print("Sorry, invalid username.") else: print("Your token is: %s" % token.hex())
2.将token输入让程序校验。
py elif op == '2': token = input("Input your token: ") res = s.login(bytes.fromhex(token)) if not res: print("Sorry, invalid token.") elif not res[1]: user = res[0].hex() print("Sorry, your username(hex) %s is inconsistent with given signature." % user) else: user = res[0].strip(unprintable).decode("Latin1") print("Login success. Welcome, %s!" % user) if user == "admin": print("I have a gift for you: %s" % FLAG)
如果签名正确且用户名为admin则可以得到flag。
我们继续跟进函数看一下:
首先是token的生成方式。
def register(self, username): if b'admin' in username: return None sig = md5(self.salt + username).digest() padlen = self.block - len(username) % self.block username += bytes([padlen] * padlen) iv = urandom(self.block) aes = AES.new(self.key, AES.MODE_CBC, iv) c = aes.encrypt(username) return iv + c + sig
发现程序不允许注册admin用户,然后token分为3部分:`iv + c + sig`
iv为随机数`urandom(self.block)`,签名sig为用户名加盐的hash值`sig = md5(self.salt + username).digest()`,密文c为AES加密得到。
再看解密方式:
def login(self, cipher): if len(cipher) % self.block != 0: return None self.T -= 1 iv = cipher[:self.block] sig = cipher[-self.block:] cipher = cipher[self.block:-self.block] aes = AES.new(self.key, AES.MODE_CBC, iv) p = aes.decrypt(cipher) p = p[:-p[-1]] return [p, md5(self.salt + p).digest() == sig]
解密只对密文c进行了操作,并且还多了一步:
p = p[:-p[-1]]
这只是一步常见去除padding的操作,无需理会。
攻击点思考
那么下面思考如何进行攻击。
首先明确我们可控参数:注册时的username以及登录时的token。
视线定位到解密流程,大致分为3步:
1.解密后res是否正常 2.解密后签名是否正确 3.解密后username是否等于admin
不难发现,我们解密的3部分`iv+c+sig`,其中c和sig都要校验,而iv却没有任何的校验。
这不禁让我们想到了一些攻击思路,例如控制iv,进行cbc字节翻转攻击,令c解密后得到admin的明文。
那么思路就接踵而至,当我们控制iv,使c改变后,sig也得相应改变。
p = aes.decrypt(cipher) p = p[:-p[-1]] return [p, md5(self.salt + p).digest() == sig]
那么sig如何预测知道username=admin时候的sig呢?
这里我们注意到签名方式:
sig = md5(self.salt + username).digest()
很明显符合我们的hash长度拓展攻击需求。
首先是salt的长度:
s = Sign(urandom(16), urandom(16))
我们发现key和salt的长度都是16
我们又可控username,那么我们可以利用hash拓展攻击计算username=admin时的sig MD5值。
那么现在思路变得比较清晰:
1.利用已知长度的salt和已知username=admin进行hash长度拓展攻击计算sig_new 2.利用原有Iv和c,进行cbc字节翻转攻击得到iv_new,使得c解密得到admin 3.得到新的token:iv_new+c+sig_new
但是随后我又陷入僵局,我没法计算出md5(secret+m),此时只知道secret的长度,并且要求m=admin
此时发现关键代码:
user = res[0].strip(unprintable).decode("Latin1")
所以我们的解密结果不是必须等于admin,前后有不可见字符也行,例如:
\x00\x00\x00\x80\x00\x00admin\x00\x00\x00
在其中的都会被过滤掉,所以最后得到的username还是admin
hash长度拓展攻击
显示这不是一个简单的hash长度攻击。
比如我们第一次注册用户名skysky,得到:
此时sig为:
eb1d2538fcb11aff70aa21213e7ddba9
如果我们此时去进行hash长度拓展攻击:
import hashpumpy tmp = hashpumpy.hashpump('eb1d2538fcb11aff70aa21213e7ddba9', 'skysky', 'admin', 16) print tmp
我们可以得到结果:
('0931bd0b164be725a1eba43031642e43', 'skysky\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x00\x00\x00\x00admin')
但这不是我们要的结果,此时明文块除了admin还带有可显字符skysky。
我们需要将username控制为不可显字符,例如:
import hashpumpy tmp = hashpumpy.hashpump('eb1d2538fcb11aff70aa21213e7ddba9', '\x00', 'admin', 16) print tmp
得到结果:
('0931bd0b164be725a1eba43031642e43', '\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x88\x00\x00\x00\x00\x00\x00\x00admin')
但是这样又有新的问题,我注册的username长度只有1,密文块长度只有16,而这里我要解密成的结果长度却有53。
那无论如何cbc翻转也不可能达到我们的目的,所以这里的username还不止是为不可显字符这么简单,我们这里精心构造一下:
首先我们构造一个如下字符串:
s="010101010101010101010101010101018000000000000000000000000000000000000000000000000001000000000000".decode("hex") s+="\xff"*16+"admi"+chr(ord(n)^1)
我们用这个字符串去进行cbc翻转攻击,得到一个合适的iv+c(虽然程序iv会变,但key不会变,cbc本身需要控制iv,所以iv会变无妨),然后我们get解密结果。
\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x14\xff\xca\x8a\xb6\xc3\xd2\x1d\x07\xd7\x16\x14\x86\xe1\x17\xb9admin
(注:这里需要爆破n轮,因为我们虽然能进行cbc翻转攻击,但是无法控制整个解密内容,所以不能保证攻击之后还全是不可见字符)
得到解密结果后,我们再根据此时的情况构造hash长度拓展攻击,计算出sig。
\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x14\xff\xca\x8a\xb6\xc3\xd2\x1d\x07\xd7\x16\x14\x86\xe1\x17\xb9admin
我们可以发现上下一致,我们成功可以利用hash长度拓展攻击预测签名值,所以只要注册用户名:
即可。
CBC字节翻转攻击
CBC翻转攻击的精髓都在这张图里,我们只需要了解如下公式:
构造的iv[1]= 原来的iv[1]^plain[1]^’a’
本题中,我们可控iv,username也是自己注册的
010101010101010101010101010101018000000000000000000000000000000000000000000000000001000000000000ffffffffffffffffffffffffffffffff61646d696f
我们只要让最后的o进行cbc翻转攻击,变成n即可。
例如我们得到token:
2d74da0969b99075b213e470b907f8e11d82f656d676080efda01be5b75941f147673b8e05686f98d2f1f6acccea2f0bf02a2bd1de59b70b31d84ebbc0dcec84ad6a8adab3589c919db153b98b9f4879ad492c39690116bbbc2f9e55d7e0cc0635e32ea23bbeb431b5c710094591d6fd
去掉后面的sig得到:
2d74da0969b99075b213e470b907f8e11d82f656d676080efda01be5b75941f147673b8e05686f98d2f1f6acccea2f0bf02a2bd1de59b70b31d84ebbc0dcec84ad6a8adab3589c919db153b98b9f4879ad492c39690116bbbc2f9e55d7e0cc06
去掉iv得到c:
1d82f656d676080efda01be5b75941f1 47673b8e05686f98d2f1f6acccea2f0b f02a2bd1de59b70b31d84ebbc0dcec84 ad6a8adab3589c919db153b98b9f4879 ad492c39690116bbbc2f9e55d7e0cc06
我们的明文m为:
01010101010101010101010101010101 80000000000000000000000000000000 00000000000000000001000000000000 ffffffffffffffffffffffffffffffff 61646d696f
最后一组只有5位,需要填充11,即\x0b*11
那我们即调整c的第4个block对应n的位置即可。
ad6a8adab3589c919db153b98b9f4879
编写脚本:
py token = token.decode('hex') cipher1 = token[:-16] cipher2 = cipher1[:-32]+cipher1[-32:-28]+chr(ord(cipher1[-28])^1)+cipher1[-27:-16]+cipher1[-16:]
getflag
知道原理后即可编写脚本如下:
#!/usr/bin/python2 from Crypto.Cipher import AES from hashlib import md5 from os import urandom import string from libnum import * import os from pwn import * import hashpumpy con = remote("47.95.212.185" ,38611) def register(username): con.sendlineafter("Login\n","1") con.sendlineafter("(hex): ",username.encode("hex")) con.recvuntil("is: ") return con.recvline().strip() def encrypt(username): token = register(username).decode("hex") return token[:-16] def decrypt(cipher): cipher +="a"*16 con.sendlineafter("Login\n","2") con.sendlineafter("token: ",cipher.encode("hex")) con.recvuntil("(hex) ") data = con.recvuntil("admin".encode("hex")) return data.decode("hex") s="010101010101010101010101010101018000000000000000000000000000000000000000000000000001000000000000".decode("hex") target = ord("n") mask = 1 assert(target^mask != target) flag = True cnt = 0 s+="\xff"*16+"admi"+chr(target^mask) while(flag): cnt+=1 if(cnt %200 ==0): print cnt flag = False cipher1 = encrypt(s) cipher2 =cipher1[:-32]+cipher1[-32:-28]+chr(ord(cipher1[-28])^mask)+cipher1[-27:-16]+cipher1[-16:] pp = decrypt(cipher2) pp = pp.strip("admin") pp = pp[-16:] for x in pp: if x in string.printable: flag = True break s = decrypt(cipher2) name ="\x01"*16 sig1 = register(name)[-32:] tmp = hashpumpy.hashpump(sig1, '01010101010101010101010101010101'.decode('hex'), s[48:], 16) sig2=tmp[0] context.log_level="debug" con.sendlineafter("Login\n","2") payload = cipher2.encode("hex")+sig2 con.sendlineafter("token: ",payload) con.interactive()
运行得到flag:
hgame{hard_cryptooooo!}
后记
算是Hash Length Extension Attacks进阶版的题目吧,CBC那一块还算比较简单,前后思考了很久,学到不少知识。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 驾校答题小程序实战全过程【连载】——4.题目采集与测试
- CTF题目实战:2019-Hgame-Web-Week4
- 从一道线下赛题目看VM类Pwn题目-debugvm
- Leetcode 题目:括号匹配
- HCTF逆向题目详析
- Hitcon2018 BabyCake题目分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。