神奇的哈希长度拓展攻击(hash length extension attacks)

  • A+
所属分类:2019CTF-wp moeCTF笔记

介绍

基本定义如下,源自维基百科

哈希长度扩展攻击 (Hash Length Extension Attacks) 是指针对某些允许包含额外信息的加密散列函数的攻击手段。该攻击适用于在消息与密钥的长度已知的情形下,所有采取了 H(key ∥ message) 此类构造的散列函数。MD5 和 SHA-1 等基于 Merkle–Damgård 构造的算法均对此类攻击显示出脆弱性。

这类哈希函数有以下特点

  • 消息填充方式都比较类似,首先在消息后面添加一个 1,然后填充若干个 0,直至总长度与 448 同余,最后在其后附上 64 位的消息长度(填充前)。
  • 每一块得到的链接变量都会被作为下一次执行 hash 函数的初始向量 IV。在最后一块的时候,才会将其对应的链接变量转换为 hash 值。

一般攻击时应满足如下条件

  • 我们已知 key 的长度,如果不知道的话,需要爆破出来
  • 我们可以控制 message 的消息。
  • 我们已经知道了包含 key 的一个消息的 hash 值。

这样我们就可以得到一对 (messge,x) 满足 x=H(key ∥ message)虽然我们并不清楚 key 的内容。

攻击原理

这里不妨假设我们我们知道了 hash(key+s) 的 hash 值,其中 s 是已知的,那么其本身在计算的时候,必然会进行填充。那么我们首先可以得到 key+s 扩展后的字符串 now,即

now=key|s|padding

那么如果我们在 now 的后面再次附加上一部分信息 extra,即

key|s|padding|extra

这样再去计算 hash 值的时候,

  1. 会对 extra 进行填充直到满足条件。
  2. 先计算 now 对应的链接变量 IV1,而我们已经知道这部分的 hash 值,并且链接变量产生 hash 值的算法是可逆的,所以我们可以得到链接变量。
  3. 下面会根据得到的链接变量 IV1,对 extra 部分进行哈希算法,并返回 hash 值。

那么既然我们已经知道了第一部分的 hash 值,并且,我们还知道 extra 的值,那么我们便可以得到最后的 hash 值。

而之前我们也说了我们可以控制 message 的值。那么其实 s,padding,extra 我们都是可以控制的。所以我们自然可以找到对应的 (message,x) 满足 x=hash(key|message)。

例子

来自moectf的一道题 以下是题目给出的后端源码

import hashlib
from quart import Quart, request, make_response, jsonify, send_file

KEY = b'123'
FLAG = 'moectf{?}'

user_passwords = dict()

app = Quart(__name__)


def sign(data, key):   #签名封装模块
    return hashlib.sha512(key + data).hexdigest()   #返回十六进制数据字符串


def verify(data, signature, key):   #比较模块
    return sign(data, key) == signature


@app.route('/')
async def home():
    return await send_file('templates/home.html')


@app.route('/login', methods=['POST'])     #post方式向/login页面提交数据
async def do_login():
    user_data = await request.json   #用户提交数据存为json文本

    if user_data is None or 'username' not in user_data or 'password' not in user_data:
        return jsonify({'status': 0, 'message': '奇怪的请求'})

    password = user_passwords.get(user_data['username'])  #取之前存入的用户名对应的密码

    if not password:
        return jsonify({'status': 0, 'message': '找不到用户'})

    if user_data['password'] != password:#将上传的password和本地的password作比较
        return jsonify({'status': 0, 'message': '密码错误'})

    user_data = [
        ['username', user_data['username']],
        ['role', 'user']
    ]
    user_data = '.'.join(['#'.join(item) for item in user_data])

    resp = await make_response(jsonify({'status': 1, 'message': '登录成功'}))  #把登录请求结果的json文本返回
    print(user_data)
    resp.set_cookie('user_data', sign(user_data.encode(), KEY) + '.' + user_data.encode().hex())
    print(user_data)
    #对照信息签名封装后写入cookies
    return resp


@app.route('/register', methods=['POST'])
async def do_register():
    if len(user_passwords) > 100:
        user_passwords.clear()

    user_data = await request.json

    if user_data is None or 'username' not in user_data or 'password' not in user_data:
        return jsonify({'status': 0, 'message': '奇怪的请求'})

    if not user_data['username'] or not user_data['password']:
        return jsonify({'status': 0, 'message': '注册信息错误'})

    user_passwords[user_data['username']] = user_data['password']
    return jsonify({'status': 1, 'message': '注册成功'})


@app.route('/flag', methods=['POST'])
async def get_flag():
    cookies = request.cookies

    user_data = cookies.get('user_data')

    if not user_data:
        return jsonify({'status': 0, 'message': '请先登录'})

    sp_user_data = user_data.split('.', 1)

    if len(sp_user_data) != 2:
        return jsonify({'status': 0, 'message': '奇怪的请求'})

    signature, user_data = sp_user_data
    user_data = bytes.fromhex(user_data)

    if not verify(user_data, signature, KEY):
        return jsonify({'status': 0, 'message': '验证失败'})

    user_data = user_data.decode(errors='ignore')
    user_info = {}

    for item in user_data.split('.'):
        kv = item.split('#', 1)
        k, v = kv if len(kv) == 2 else ('role', 'user')
        user_info[k] = v

    role = user_info.get('role')

    if not role or role != 'admin':
        return jsonify({'status': 0, 'message': '无权限访问'})

    return jsonify({'status': 1, 'message': FLAG})


if __name__ == '__main__':
    app.run('127.0.0.1', 8080, debug=True)

可以看出此题的关键在于Sign+Data的签名部分,很明显可以使用上文所述的长度拓展攻击。由于不知道密钥KEY的长度,我们可以利用python编写以下脚本进行爆破(部分关键数据以删除)

# -*- coding: utf-8 -*-
#作者:赤道企鹅™
import requests
import json
import hashpumpy
import hashlib
import time

a="username#1.role#user"
add="username#1.role#admin"
target="这里是首次签名生成的cookies前半部分"

url = 'https://quality.challenge.moectf.cn/flag'
body = {"username": "1", "password": "1"}#登录用的json
headers = {'content-type': "application/json", 'Authorization': 'APP appid = 4abf1a,token = 9480295ab2e2eddb8'}#header不能少

for keynum in range(1,999):#循环扩大key的位数
	result=hashpumpy.hashpump(target, a, add,keynum)#核心代码,获取同密钥不同数据的新签名
	result=list(result)
	co=str(result[0])+'.'+str(result[1].hex())
	print(co)
	cookies={'user_data':co}
	response = requests.post(url, data = json.dumps(body), headers = headers,cookies=cookies) #将新签名重新提交
	if 'moectf' in response.text:
		print(response.text)
		break
	else:
		print(keynum,'->NOT')
	time.sleep(0.5)

Hashpump的简单参数

$ hashpump -h HashPump [-h help] [-t test] [-s signature] [-d data] [-a additional] [-k keylength]     
HashPump generates strings to exploit signatures vulnerable to the Hash Length Extension Attack.     
-h --help          Display this message.     
-t --test          Run tests to verify each algorithm is operating properly.     
-s --signature     The signature from known message.     
-d --data          The data from the known message.     
-a --additional    The information you would like to add to the known message.     
-k --keylength     The length in bytes of the key being used to sign the original message with.     
Version 1.2.0 with CRC32, MD5, SHA1, SHA256 and SHA512 support.     <Developed by bwall(@botnet_hunter)>
>>> hashpumpy.hashpump('ffffffff', 'original_data', 'data_to_add', len('KEYKEYKEY')) 

>>>('e3c4a05f', 'original_datadata_to_add')

工具

如何使用请参考 github 上的 readme。

eqqie

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: