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

栏目: 编程工具 · 发布时间: 6年前

内容简介:最近,为了防止整天沉迷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那一块还算比较简单,前后思考了很久,学到不少知识。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Pro JavaScript Design Patterns

Pro JavaScript Design Patterns

Dustin Diaz、Ross Harmes / Apress / 2007-12-16 / USD 44.99

As a web developer, you’ll already know that JavaScript™ is a powerful language, allowing you to add an impressive array of dynamic functionality to otherwise static web sites. But there is more power......一起来看看 《Pro JavaScript Design Patterns》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具