「密码学」哈希为什么要将盐加在明文后面?

众所周知,在做消息认证或者签名时,仅使用hash函数安全性是不高的,容易遭受字典和暴力破解(https://www.cmd5.com/)。所以通常会使用带密钥或加盐的哈希算法作为消息认证或者口令存储,正如标题所说,我们在检索互联网上关于加盐的实现时,内容往往都是在明文后面加上随机值:

image-20230307141942058

那做消息认证的密钥或者盐可不可以加在明文前面呢?

这就引出本文的攻击方式。

MD5 算法计算逻辑

为了清楚这个攻击方式原理,需要先了解下md5的计算逻辑。

md5算法本质上是一种压缩算法,将长度小于2^64bit的任意字符压缩成128bit固定长度字符。同AES之类的分组加密算法一样,md5也需要进行分组计算。

图来自先知社区

如图所示,md5的计算需要经过两个步骤:

  1. 分组&填充
  2. 具体计算

1.分组&填充

首先会对明文按照512bit的长度进行分组,最后一个分组可能会发生长度不足512bit,或者刚刚512bit。

无论最后一个分组的长度是否刚好等于512bit,按照填充规则都需要进行填充,具体细节:

  • 假设明文刚好能被512整除,需要新增一个分组,在末尾8*8=64bit 按照小端存储放入原始明文的长度,分组中间剩余的bit 按照10000000的方式进行填充,形成一个总长512bit的新分组。
  • 假设整除512bit余数大于0
    • 且余数大于512-8*8 =448 bit 则需要继续填充10000(0x80000.....)至下一个分组的448bit,剩余的bit按照小端存储填充原始明文长度
    • 余数小于448bit,末尾填充原始明文长度,中间剩余部分填充填充10000(0x80000.....)

以上两个步骤用代码表示即为:

        m_l = len(message)  # 原始消息长度
        length = struct.pack('<Q', (m_l) * 8)  # 长度转化为小端 unsigned long long 8B
        blank_padding = b""
        message += b'\x80'  # 10000000
        # 此分组不足以填充长度时
        if 56 < len(message) < 64:
            blank_padding += b'\x00' * (56 + 64 - len(message))  # 填充至下一个分组
        # 分组能填充长度
        else:
            blank_padding = b'\x00' * (56 - len(message) % 64)  # 本分组填充
        if len(blank_padding) > 0:
            # 填充10000
            message += blank_padding
        # 填充长度
        message += length

2. 具体计算

其实具体的计算过程,我们不用关注,把这个过程当做一个黑盒(关注细节的可以关注文末github地址)就可以:

image-20230309111335844

在上一步分组的基础上,第一组的分组的明文会和128bit的初始序列(幻数)做为输入进行压缩计算,初始序列是一组固定的值:

image-20230309174432910

计算后会产生新的序列,做为下一组的“初始序列”和下一组的明文再次进行压缩计算,接下来的分组重复这种“上一组的输出作为下一组的输入”,最后一组的128bit输出即为最终的md5值。

哈希长度拓展攻击

了解了md5的计算逻辑,再回到这张图,上一次的的输出作为下一次的输入这种方式可能会导致一个问题。假设存在明文分组abc,明文分组产生md5的过程可以简化为:

  • 分组a:h.a = md5(iv,a), md5计算需要两个参数,iv 为初始序列,h.a 为压缩计算结果
  • 分组b:h.ab = md5(h.a,b)
  • 分组c或者最终md5: h.abc = md5(h.ab,c)

图来自先知社区

在这个过程当中,如果密钥被放在a分组当中,bc为原消息认证的明文,那攻击者可以在不知道密钥的情况下,扩展明文长度,如增加明文d,计算abcd的hash,只需要知道基于abc的hash值,即可生成新的hash。

即:h.abcd = md5(h.abc,d)

这个过程即为hash长度扩展攻击。

接下来我们根据实际的例子来实操下

实操

假设存在一个商城订单支付场景,订单的确认是通过前端参数给出,存在一个逻辑漏洞可以通过前端参数来控制商品价格,从而实现“零元购”或者越权购买。

image-20230307165648656

正常情况下进行购买,因为默认此用户只有300积分,所以会购买失败:

image-20230307171050046

但如果进行参数价格good_price修改,会因为签名校验不通过:

image-20230307171601458

所以需要进行签名破解,先看下后端验证的逻辑:

<?php
	$total_score = 300;
	$flag = 'xxxxxxxxxxxx';
	$secret_key = "??????????????????????????????????????";  // 前端未知
	$post_data =urldecode(file_get_contents("php://input"));
	$user_sign = $_GET['signature'];
	$sign = md5($secret_key.$post_data);
	if ($user_sign === $sign) {
		$price = $_POST['good_price'];
		if ($price > $total_score){
			echo '对不起,您的积分余额不足,交易失败!';
		}else{
			echo "恭喜,购买成功!$flag";
		}
	}else{
		echo '签名数据被篡改!';
	}

?>

后端存在一个签名逻辑,会验证用户的post参数加上密钥的md5值,如果用户修改了post参数,但因为不知道密钥也就没发生成合法的md5,所以验证会不通过。

如果没有了解过哈希长度扩展攻击,这个代码是没啥问题的,所以知识面决定攻击面。

而且这个地方密钥被放在了明文前面拼接,针对哈希长度扩展攻击,利用起来还挺简单的,可以使用现成的工具,比如hashpump,按照提示输入内容即可:

image-20230310154439762

最后的明文中十六进制部分需要url编码,但因为hashpump需要编译,在win平台编译比较麻烦,所以我自己实现了一个md5版本的利用工具(https://github.com/shellfeel/hash-ext-attack)

image-20230310161015225

最后把得到的结果粘贴到burp,成功购买。

image-20230310164429827

总结

文章分析了下md5的计算逻辑,以及哈希长度扩展的攻击原理,对于此类攻击的修复,其实很简单只需要把密钥由加在明文前面改为明文后面,或者使用标准的hmac算法,hmac算法里面会用密钥和明文做移位异或操作,从而增强hash的安全性,本文是以md5为例,其实对于有着类似M-D结果的hash算法都是可以这样利用的,比如sha-0,sha-1,sha-2 等。

image-20230301201336027

参考

公众号

欢迎大家关注我的公众号,这里有干货满满的硬核安全知识,和我一起学起来吧!

欢迎关注

posted @ 2023-03-10 17:02  9eek  阅读(603)  评论(0编辑  收藏  举报