Sky's blog

2019-Hgame-Week4-Crypto&Sign_in_SemiHard

字数统计: 2,362阅读时长: 11 min
2019/02/19 Share

前言

为了防止整天沉迷switch智力降低,于是刷了一道有意思的Crypto题,其中涉及大概2个考点:Hash Length Extension Attacks & CBC Byte Flipping Attack,虽然利用有些麻烦,但是锻炼脑力,从CTF开始~

程序分析

先看主程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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给你

1
2
3
4
5
6
7
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输入让程序校验

1
2
3
4
5
6
7
8
9
10
11
12
13
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的生成方式

1
2
3
4
5
6
7
8
9
10
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加密得到
再看解密方式

1
2
3
4
5
6
7
8
9
10
11
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进行了操作,并且还多了一步

1
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也得相应改变

1
2
3
p = aes.decrypt(cipher)
p = p[:-p[-1]]
return [p, md5(self.salt + p).digest() == sig]

那么sig如何预测知道username=admin时候的sig呢?
这里我们注意到签名方式

1
sig = md5(self.salt + username).digest()

很明显符合我们的hash长度拓展攻击需求
首先是salt的长度

1
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
此时发现关键代码

1
user = res[0].strip(unprintable).decode("Latin1")

所以我们的解密结果不是必须等于admin,前后有不可见字符也行,例如

1
\x00\x00\x00\x80\x00\x00admin\x00\x00\x00


在其中的都会被过滤掉,所以最后得到的username还是admin

hash长度拓展攻击

显示这不是一个简单的hash长度攻击
比如我们第一次注册用户名skysky,得到

此时sig为

1
eb1d2538fcb11aff70aa21213e7ddba9

如果我们此时去进行hash长度拓展攻击

1
2
3
import hashpumpy
tmp = hashpumpy.hashpump('eb1d2538fcb11aff70aa21213e7ddba9', 'skysky', 'admin', 16)
print tmp

我们可以得到结果

1
('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控制为不可显字符,例如

1
2
3
import hashpumpy
tmp = hashpumpy.hashpump('eb1d2538fcb11aff70aa21213e7ddba9', '\x00', 'admin', 16)
print tmp

得到结果

1
('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还不止是为不可显字符这么简单,我们这里精心构造一下:
首先我们构造一个如下字符串

1
2
s="010101010101010101010101010101018000000000000000000000000000000000000000000000000001000000000000".decode("hex")
s+="\xff"*16+"admi"+chr(ord(n)^1)

我们用这个字符串去进行cbc翻转攻击,得到一个合适的iv+c(虽然程序iv会变,但key不会变,cbc本身需要控制iv,所以iv会变无妨),然后我们get解密结果

1
\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

1
\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长度拓展攻击预测签名值,所以只要注册用户名

1
01010101010101010101010101010101

即可

CBC字节翻转攻击


CBC翻转攻击的精髓都在这张图里,我们只需要了解如下公式

1
构造的iv[1]= 原来的iv[1]^plain[1]^’a’

本题中,我们可控iv,username也是自己注册的

1
010101010101010101010101010101018000000000000000000000000000000000000000000000000001000000000000ffffffffffffffffffffffffffffffff61646d696f


我们只要让最后的o进行cbc翻转攻击,变成n即可
例如我们得到token

1
2d74da0969b99075b213e470b907f8e11d82f656d676080efda01be5b75941f147673b8e05686f98d2f1f6acccea2f0bf02a2bd1de59b70b31d84ebbc0dcec84ad6a8adab3589c919db153b98b9f4879ad492c39690116bbbc2f9e55d7e0cc0635e32ea23bbeb431b5c710094591d6fd

去掉后面的sig得到

1
2d74da0969b99075b213e470b907f8e11d82f656d676080efda01be5b75941f147673b8e05686f98d2f1f6acccea2f0bf02a2bd1de59b70b31d84ebbc0dcec84ad6a8adab3589c919db153b98b9f4879ad492c39690116bbbc2f9e55d7e0cc06

去掉iv得到c

1
2
3
4
5
1d82f656d676080efda01be5b75941f1
47673b8e05686f98d2f1f6acccea2f0b
f02a2bd1de59b70b31d84ebbc0dcec84
ad6a8adab3589c919db153b98b9f4879
ad492c39690116bbbc2f9e55d7e0cc06

我们的明文m为

1
2
3
4
5
01010101010101010101010101010101
80000000000000000000000000000000
00000000000000000001000000000000
ffffffffffffffffffffffffffffffff
61646d696f

最后一组只有5位,需要填充11,即\x0b*11
那我们即调整c的第4个block对应n的位置即可

1
ad6a8adab3589c919db153b98b9f4879

编写脚本

1
2
3
token = token.decode('hex')
cipher1 = token[:-16]
cipher2 = cipher1[:-32]+cipher1[-32:-28]+chr(ord(cipher1[-28])^1)+cipher1[-27:-16]+cipher1[-16:]

getflag

知道原理后即可编写脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/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

1
hgame{hard_cryptooooo!}

后记

算是Hash Length Extension Attacks进阶版的题目吧,CBC那一块还算比较简单,前后思考了很久,学到不少知识XD

1
文章首发于嘶吼 https://www.4hou.com/technology/16343.html
点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. 程序分析
  3. 3. 攻击点思考
  4. 4. hash长度拓展攻击
  5. 5. CBC字节翻转攻击
  6. 6. getflag
  7. 7. 后记