CTF题目实战:2019-Hgame-Week4-Crypto&Sign_in_SemiHard

一叶飘零 技术 2019年2月28日发布
Favorite收藏

导语:最近,刷了一道有意思的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

图片1.png

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

hash长度拓展攻击

显示这不是一个简单的hash长度攻击。

比如我们第一次注册用户名skysky,得到:

图片2.png

此时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

图片3.png

我们可以发现上下一致,我们成功可以利用hash长度拓展攻击预测签名值,所以只要注册用户名:

01010101010101010101010101010101

即可。

CBC字节翻转攻击

图片4.png

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

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

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

010101010101010101010101010101018000000000000000000000000000000000000000000000000001000000000000ffffffffffffffffffffffffffffffff61646d696f

图片6.png

我们只要让最后的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!}

图片5.png

后记

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

本文为 一叶飘零 原创稿件,授权嘶吼独家发布,如若转载,请注明原文地址: https://www.4hou.com/technology/16343.html
点赞 1
CTF
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论