内容简介:最近,为了防止整天沉迷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题目分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
孵化Twitter
[美]尼克·比尔顿(Nick Bilton) / 欧常智、张宇、单旖 / 浙江人民出版社 / 2014-1 / 49.90元
一个在挣扎中生存的博客平台Odeo,一小撮龙蛇混杂的无政府主义者员工,经历了怎样的涅槃,摇身一变,成为纽交所最闪耀的上市企业Twitter? 一个野心勃勃的农场小男孩,一个满身纹身的“无名氏“,一个爱开玩笑的外交家,一位害羞而又充满活力的极客,这四位各有特色的创始人如何从兢兢业业、每日劳作的工程师,成为了登上杂志封面、奥普拉秀和每日秀的富裕名人?而在Twitter日益茁壮成长的过程中,他们又......一起来看看 《孵化Twitter》 这本书的介绍吧!