哈希长度扩展攻击

0x01 简介

哈希长度扩展攻击(hash length extension attacks)是指针对某些允许包含额外信息的加密散列函数的攻击手段。该攻击适用于在消息与密钥的长度已知的情形下,所有采取了 H(密钥 ∥ 消息) 此类构造的散列函数。MD5和SHA-1等基于Merkle–Damgård构造的算法均对此类攻击显示出脆弱性。适用于已经确定哈希值和密钥长度的情况,即知道了哈希值和密钥的长度,可以推出H(密钥||消息||padding||append)的哈希值,padding是要填充的字段,append则是要附加的消息。其实如果不知道密钥长度,可通过暴力猜解得到,已知的有长度扩展攻击缺陷的函数有MD5,SHA-1,SHA-256等等,。

如果一个应用程序是这样操作的:

1、准备了一个密文和一些数据构造成一个字符串里,并且使用了MD5之类的哈希函数生成了一个哈希值(也就是所谓的signature/签名)

2、让攻击者可以提交数据以及哈希值,虽然攻击者不知道密文

3、服务器把提交的数据跟密文构造成字符串,并经过哈希后判断是否等同于提交上来的哈希值

这个时候,该应用程序就易受长度扩展攻击,攻击者可以构造出{secret || data || attacker_controlled_data}的哈希值。

0x02 sha1的原理

首先,当hash函数拿到需要被hash的字符串后,先将其字节长度整除64,取得余数。如果该余数正好等于56,那么就在该字符串最后添加上8个字节的长度描述符(具体用bit表示)。如果不等于56,就先对字符串进行长度填充,填充时第一个字节为hex(80),其他字节均用hex(00)填充,填充至余数为56后,同样增加8个字节的长度描述符(该长度描述符为需要被hash的字符串的长度,不是填充之后整个字符串的长度)。以上过程,称之为补位。
补位完成后,字符串以64位一组进行分组(因为上面的余数为56,加上8个字节的长度描述符后,正好是64位,凑成一组)。字符串能被分成几组就会进行多少次“复杂的数学变化”。

0x03 举个例子

想要对123456进行hash,首先判断len(str_a) % 64 == 56是否成立。这里很明显不成立,那么程序就进行补位操作。首先补位成余数为56的长度。

如上图,蓝色字体就为程序对该字符串进行补位的数据。当满足len(str_a) % 64 == 56后,程序就在该字符串的后面添加8个字节的长度描述符。注意,此处的长度为原始需要被hash的长度。也就是len(str_a) = 6字节*8bit/字节= 48bit=0x30bit。

补位+长度描述符=64个字节,正好是一个分组。所以此处只要进行一次复杂的数学变化就可以了。程序根据该64个字节的数据和registers值0生成新的registers值1。那么该新的registers值1就是str_a的sha1值.

运用

简单来说,就是服务器上会生成一个salt值,该salt值你是不可预测的。但是你又知道了sha1(salt+filename)的值,该filename的值你也是知道的。假设此处的filename的值report.pdf,最后sha1的值为:0a8d538b724c6f2b4288526eb540ee7c。为了方便理解,我们继续假设salt的长度为16位。

同样先进行整除,然后取余。最后再补上8位的长度描述符。补位+添加长度描述符后的字符串如下图:

该长度也就满足了64位的分组,只需要进行一次“复杂的数学运算”就可以得到最后的sha1值了。 同样字符串进行sha1操作    

同样,还是先进行分组。由于该字符串的长度大于64个字节,且小于128个字节,所以要分成两组,需要进行两次“复杂的数学运算”。这个时候我们发现,第一个分组的数据和上图中补码后的数据完全一样,又因为他们都是第一个分组,初始的registers值也一样。那么经过第一轮“复杂的数学运算”,他们各自生成的registers值也同样是相同的。唯一不同的是,由于上面的长度小于64字节,所以只需要进行一轮运算便得到了最后的sha1值。然后这里的字符串有两个分组,需要将第一轮更新的registers值(也就是第一轮运算出来的sha1值)作为第二轮“复杂的数学运算”的registers值,然后才能得出最终的sha1值。

总上讲的,如果salt的值你不知道,但是你知道长度,又知道sha1(salt),那么就也就可以知道sha1(salt+“填充数据”+“任意可控数据”).这里的salt+“填充数据”就是对salt进行sha1时所补全的数据+最后8位的长度描述符。一般来说,salt+”填充数据”的长度就是64字节,正好是一个分组。如果salt的长度就大于了56个字节,那么加入填充数据后的长度应该是N个64字节,等于N个分组。sha1程序再对(salt+“填充数据”+“任意可控数据”)进行hash时,只需要进行第二轮及第二轮以后的运算。因为第一轮运算后的registers值就是sha1(salt)的值,该值你已经知道了。

实验吧实例分析

http://ctf4.shiyanbar.com/web/kzhan.php

1、抓包进行分析

将source=0改为1出现代码

<pre>
$flag = "XXXXXXXXXXXXXXXXXXXXXXX";
$secret = "XXXXXXXXXXXXXXX"; // This secret is 15 characters long for         security!

$username = $_POST["username"];
$password = $_POST["password"];

if (!empty($_COOKIE["getmein"])) {
if (urldecode($username) === "admin" && urldecode($password) !=         "admin") {
    if ($COOKIE["getmein"] === md5($secret . urldecode($username . $password))) {
        echo "Congratulations! You are a registered user.\n";
        die ("The flag is ". $flag);
    }
    else {
        die ("Your cookies don't match up! STOP HACKING THIS SITE.");
    }
}
else {
    die ("You are not an admin! LEAVE.");
}
}

    setcookie("sample-hash", md5($secret . urldecode("admin" . "admin")), time() + (60 * 60 * 24 * 7));
cookie生命周期时间7天
if (empty($_COOKIE["source"])) {
setcookie("source", 0, time() + (60 * 60 * 24 * 7));
}
else {
if ($_COOKIE["source"] != 0) {
    echo ""; // This source code is outputted here
}
}
</pre>

2、进行代码审计

$secret = "XXXXXXXXXXXXXXX"; // This secret is 15 characters long for security!
setcookie("sample-hash", md5($secret . urldecode("admin" . "admin")), time() + (60 * 60 * 24 * 7));

可以看出来sample-hash这个值是由15字节长的salt串上adminadmin后经md5得出

password不能是admin,但可以输入adminxxxx这种,使计算getmein时前25字节和计算sample-hash时前25字节输入相同,又知道了salt的位数,使用hash长度扩展攻击。

Hashpump:

HashPump是一个借助于OpenSSL实现了针对多种散列函数的攻击的工具,支持针对MD5、CRC32、SHA1、SHA256和SHA512等长度扩展攻击。而MD2、SHA224和SHA384算法不受此攻击的影响,因其部分避免了对状态变量的输出,并不输出全部的状态变量。

安装linux下:
git clone https://github.com/bwall/HashPump
apt-get install g++ libssl-dev
cd HashPump
make
make install

Python爱好者若想直接在python里实现hashpump,可以使用zachriggle编写的这个叫做hashpumpy的Python插件

python安装

pip install hashpumpy

使用

>>> import hashpumpy
>>> help(hashpumpy.hashpump)
Help on built-in function hashpump in module hashpumpy:
hashpump(...)
   hashpump(hexdigest, original_data, data_to_add, key_length) -> (digest, message)
   Arguments:
    hexdigest(str):      Hex-encoded result of hashing key + original_data.
  original_data(str):  Known data used to get the hash result hexdigest.
  data_to_add(str):    Data to append
  key_length(int):     Length of unknown data prepended to the hash
Returns:
  A tuple containing the new hex digest and the new message.
  >>> hashpumpy.hashpump('ffffffff', 'original_data', 'data_to_add', len('KEYKEYKEY'))
  ('e3c4a05f', 'original_datadata_to_add')

分析上题

md5($secret.”adminadmin”)的值为571580b26c65f306376d4f64e53cb5c7

$secret是密文,长度为15,如果再算上后面第一个admin,长度就是20
而数据是admin
签名(哈希值)是571580b26c65f306376d4f64e53cb5c7

使用hashpump直接运行
hashpump -s 571580b26c65f306376d4f64e53cb5c7 -d admin -k 20 -a admin

e18dfd8404515016d3aeeea2aa196909
admin\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\xc8\x00\x00\x00\x00\x00\x00\x00admin
 第一个是新的签名,把它设置到cookies的getmein里。
第二个先把\x替换为%后,post提交
password=admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%c8%00%00%00%00%00%00%00admin

即可得到flag。