多读书多实践,勤思考善领悟

关于哈希长度扩展攻击你需要知道的一切

本文于1967天之前发表,文中内容可能已经过时。

你可以在Github上获取hash_extender工具!

(行政说明:我不再在Tenable了!我的条款很好,现在我是Leviathan Security Group的顾问。如果您需要更多信息,请随时与我联系!)

不久前,我的朋友@mogigoma和我正在https://stripe-ctf.com上进行一场捕获旗帜比赛。比赛的其中一个级别要求我们执行哈希长度扩展攻击。我当时从来没有听说过这次攻击,经过一些阅读后我意识到这不仅是一次超酷(并且在概念上很容易!)的攻击,还有完全缺乏执行攻击的好工具!在添加错误数量的空字节或错误地添加长度值数小时之后,我发誓要编写一个工具,以便让我和其他任何尝试这样做的人都很容易。所以,经过几周的工作,就在这里!

现在我要发布这个工具,并希望我没有完全错过一个做同样事情的好工具!它被称为hash_extender,并针对我能想到的每个算法实现长度扩展攻击:

  • MD4
  • MD5
  • RIPEMD-160
  • SHA-0
  • SHA-1
  • SHA-256
  • SHA-512
  • 惠而浦

我非常乐意将其扩展到其他哈希算法,只要它们“易受”这种攻击 - MD2,SHA-224和SHA-384都没有。如果您有其他候选人,请与我联系,我会尽快添加!

攻击

如果应用程序将字符串预先设置为字符串,使用易受攻击的算法对其进行哈希处理,并将攻击者委托给字符串和哈希值,而不是秘密,则应用程序很容易受到哈希长度扩展攻击。然后,服务器依赖该秘密来决定稍后返回的数据是否与原始数据相同。

事实证明,即使攻击者不知道前置秘密的值,他仍然可以生成{secret ||的有效哈希值。数据|| attacker_controlled_data}!这是通过简单地拾取散列算法停止的位置来完成的; 事实证明,继续哈希所需的状态的100%是大多数哈希算法的输出!我们只需将该状态加载到适当的哈希结构中并继续散列。

TL; DR:给定由具有未知前缀的字符串组成的散列,攻击者可以附加到字符串并生成仍具有未知前缀的新散列。

让我们看一个循序渐进的例子。对于这个例子:

  • 秘密=“秘密”
  • data =“data”
  • H = md5()
  • let signature = hash(secret || data)= 6036708eba0d11f6ef52ad44e8b74d5b
  • append =“追加”

服务器向攻击者发送数据签名。攻击者猜测H是MD5的长度(它是最常见的128位哈希算法),基于源,应用程序的规范,或者他们能够做到的任何方式。

只知道数据^ h,并签名,攻击者的目标是追加追加数据并生成新的数据的有效签名。这很容易做到!我们来看看如何。

填充

在我们看看实际的攻击之前,我们不得不谈一谈填充。

当计算H秘密 + 数据)时,字符串(秘密 + 数据)用“1”位和一些“0”位填充,后跟字符串的长度。也就是说,在十六进制中,填充是0x80字节,后跟一些0x00字节,然后是长度。0x00字节的数量,为长度保留的字节数以及长度的编码方式取决于特定的算法和块大小。

对于大多数算法(包括MD4,MD5,RIPEMD-160,SHA-0,SHA-1和SHA-256),字符串将被填充,直到其长度与56个字节(mod 64)一致。或者,换句话说,它被填充直到长度比完整(64字节)块少8个字节(8个字节是编码长度字段的大小)。在hash_extender中实现了两个不使用这些值的哈希:SHA-512使用128字节的块大小并为长度字段保留16个字节,而WHIRLPOOL使用64字节的块大小并为长度字段保留32个字节。

长度字段的字节顺序也很重要。MD4,MD5和RIPEMD-160是little-endian,而SHA系列和WHIRLPOOL是big-endian。相信我,这种区别花了我几天的工作!

在我们的示例中,length(secret || data)= length(“secretdata”)是10(0x0a)字节或80(0x50)位。因此,我们有10个字节的数据(“secretdata”),46个字节的填充(80 00 00 …)和一个8字节的小端长度字段(50 00 00 00 00 00 00 00)总共64个字节(或一个块)。放在一起,它看起来像这样:

1
2
3
4
0000 73 65 63 72 65 74 64 61 74 61 80 00 00 00 00 00 secretdata ...... 
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ......... .......
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 50 00 00 00 00 00 00 00 ........ P .......

打破这个字符串,我们有:

  • “秘密”=秘密
  • “数据”=数据
  • 80 00 00 … - 填充的46个字节,从0x80开始
  • 50 00 00 00 00 00 00 00 - 小端的位长

这是H在原始示例中散列的确切数据。

攻击

既然我们有H哈希的数据,那么让我们来看看如何执行实际的攻击。

首先,让我们将附加追加到字符串中。够容易!这是它的样子:

1
2
3
4
5
0000 73 65 63 72 65 74 64 61 74 61 80 00 00 00 00 00 secretdata ...... 
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ......... .......
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 50 00 00 00 00 00 00 00 ........ P .......
0040 61 70 70 65 6e 64追加

该块的哈希是我们最终想要a)计算,以及b)让服务器计算。可以通过两种方式计算该数据块的值:

  • 通过将其粘贴在缓冲区中并执行H(缓冲区)
  • 通过从第一个块结束开始,使用我们已经从签名中获知的状态,并从该状态开始追加哈希

第一种方法是服务器将执行的操作,第二种方法是攻击者将执行的操作。让我们先看一下服务器,因为它是一个更简单的例子。

服务器的计算

我们知道服务器会在字符串前加上秘密,所以我们发送字符串减去秘密值:

1
2
3
4
0000 64 61 74 61 80 00 00 00 00 00 00 00 00 00 00 00数据............ 
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... .............
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...............
0030 00 00 50 00 00 00 00 00 00 00 61 70 70 65 6e 64 ..P .......追加

不要被这恰好是64字节(块大小)所欺骗 - 这只是因为秘密追加是相同的长度而发生的。也许我不应该选择那个作为例子,但我不会重新开始!

服务器将为该字符串添加秘密,创建:

1
2
3
4
5
0000 73 65 63 72 65 74 64 61 74 61 80 00 00 00 00 00 secretdata ...... 
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ......... .......
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 50 00 00 00 00 00 00 00 ........ P .......
0040 61 70 70 65 6e 64追加

并将其哈希值为以下值:

1
6ee582a1669ce442f3719c47430dadee

对于那些在家里玩的人,你可以通过将其复制并粘贴到终端来证明这是有效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
echo' 
#include <stdio.h>
#include <openssl / md5.h>

int main(int argc,const char * argv [])
{
MD5_CTX c;
unsigned char buffer [MD5_DIGEST_LENGTH];
int i;

MD5_Init(C);
MD5_Update(&c,“secret”,6);
MD5_Update(&c,“data”
“\ 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 \ x00 \ x00 \ x00“
”\ x00 \ x00 \ x00 \ x00“
”\ x50 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00“
MD5_Final(缓冲区,&c);

for(i = 0; i <16; i ++){
printf(“%02x”,buffer [i]);
}
printf(“\ n”);
返回0;
}'> hash_extension_1.c

gcc -o hash_extension_1 hash_extension_1.c -lssl -lcrypto

./hash_extension_1

好的,所以服务器将检查我们发送的针对签名6ee582a1669ce442f3719c47430dadee的数据。现在,作为攻击者,我们需要弄清楚如何生成该签名!

客户的计算

那么,我们如何在不实际访问秘密的情况下计算上面显示的数据的哈希值呢?

嗯,首先,我们需要看一下我们要处理的内容:dataappendHH(secret || data)

我们需要定义一个新函数H’,它使用与H相同的散列算法,但其起始状态是H(secret || data)的最终状态,即签名。一旦我们有了,我们只需计算H’(追加),该函数的输出就是我们的哈希值。这听起来很容易(而且是!); 看看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
echo' 
#include <stdio.h>
#include <openssl / md5.h>

int main(int argc,const char * argv [])
{
int i;
unsigned char buffer [MD5_DIGEST_LENGTH];
MD5_CTX c;

MD5_Init(C);
MD5_Update(&c,“AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA”,64);

cA = htonl(0x6036708e); / * < - 这是我们已经拥有的哈希值* /
cB = htonl(0xba0d11f6);
cC = htonl(0xef52ad44);
cD = htonl(0xe8b74d5b);

MD5_Update(&c,“append”,6); / *这是附加数据。* /
MD5_Final(缓冲区,&c);
for(i = 0; i <16; i ++){
printf(“%02x”,缓冲液[I]);
}
的printf( “\ n”);
返回0;
}'> hash_extension_2.c

gcc -o hash_extension_2 hash_extension_2.c -lssl -lcrypto

./hash_extension_2

输出就像以前一样:

1
6ee582a1669ce442f3719c47430dadee

所以我们知道签名是正确的。不同的是,我们根本没有使用秘密!发生了什么!?

好吧,我们从头开始创建MD5_CTX结构,就像正常一样。然后我们采用64’A的MD5。我们采用’A’的完整(64字节)块的MD5来确保除了散列本身的状态之外的任何内部值都设置为我们期望的值。

然后,在完成之后,我们将cAcBcCcD替换为签名中的值:6036708eba0d11f6ef52ad44e8b74d5b。这使得MD5_CTX结构处于与最初完成时相同的状态,并且意味着我们散列的任何其他内容(在本例中为append)将产生与我们以常规方式对其进行哈希处理时相同的输出。

我们在设置状态变量之前对值使用htonl(),因为MD5 - 是little-endian - 也以little-endian输出它的值。

结果

所以,现在我们有了这个字符串:

1
2
3
4
0000 64 61 74 61 80 00 00 00 00 00 00 00 00 00 00 00数据............ 
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... .............
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...............
0030 00 00 50 00 00 00 00 00 00 00 61 70 70 65 6e 64 ..P .......追加

这个签名为H(secret || data || append)

1
6ee582a1669ce442f3719c47430dadee

我们可以在不知道秘密是什么的情况下生成签名!因此,我们将字符串与我们的新签名一起发送到服务器。服务器将在签名前添加哈希值,并提供与我们完全相同的哈希值(胜利!)。

工具

这个例子花了我几个小时写。为什么?因为我写了大约一千个错误代码。NUL字节太多,没有足够的NUL字节,错误的字节序,错误的算法,使用的字节而不是长度的位,以及各种其他愚蠢的问题。我第一次参加这种类型的攻击时,我花了2300h到0700h尝试让它工作,并且直到睡觉之后才得到它(并且在Mak的帮助下)。甚至不让我开始将这次攻击移植到MD5需要多长时间。Endianness可能会在火灾中死亡。

为什么这么难?因为这是加密,并且加密非常复杂并且众所周知难以排除故障。有许多活动部件,需要记住许多侧面案例,而且从来都不清楚为什么出现问题,只是结果不对。太痛苦了!

所以,我写了hash_extender。hash_extender是(我希望)第一个实现此类攻击的免费工具。它易于使用,并为我能想到的每种算法实现此攻击。

以下是其使用示例:

1
2
3
4
5
$ ./hash_extender --data data --secret 6  -  append append --signature 6036708eba0d11f6ef52ad44e8b74d5b --format md5 
类型:md5
密钥长度:6
新签名:6ee582a1669ce442f3719c47430dadee
新字符串:646174618000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000

如果您不确定哈希类型,可以通过省略–format参数让它尝试不同的类型。如果你正在尝试多种算法,我建议使用–table参数:

1
2
3
$ ./hash_extender --data数据--secret 6 --append追加--signature 6036708eba0d11f6ef52ad44e8b74d5b --out数据格式HTML --table 
MD4 89df68618821cd4c50dfccd57c79815b data80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000P00000000000000append
MD5 6ee582a1669ce442f3719c47430dadee data80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000P00000000000000append

有很多选项可用于格式化输入和输出,包括HTML(使用%NN表示法),CString(使用\ xNN表示法,以及\ r\ n\ t等),hex (例如上面如何指定哈希)等。

默认情况下,我试图选择我认为最合理的选择:

  • 输入数据:原始
  • 输入哈希:十六进制
  • 输出数据:十六进制
  • 输出哈希:十六进制

这是帮助页面供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
-------------------------------------------------- ------------------------------ 
HASH EXTENDER
------------------ -------------------------------------------------- ------------

Ron Bowes

有关许可证信息,请参阅LICENSE.txt。

用法:./ hash_extender < - data =| --file => - 签名= --format =[options]

INPUT OPTIONS
-d --data =
我们要扩展的原始字符串。
--data格式=
字符串传入的格式为。默认值:raw。
有效格式:raw,hex,html,cstr
--file =
作为指定字符串的替代方法,它将原始字符串
作为文件读取。
-s --signature =
原始签名。
--signature格式=
签名传递的格式为。默认值:十六进制
有效格式:raw,hex,html,
cstr -a --append =
要附加到字符串的数据。默认值:raw。
--append格式=
有效格式:raw,hex,html,cstr
-f --format =[必需]
签名的hash_type。如果您
想尝试多个签名,可以多次给出。'all'将
根据签名的大小选择所选类型,并使用有意义的哈希值。
有效类型:md4,md5,ripemd160,sha,sha1,sha256,sha512,whirlpool
-l --secret =
秘密的长度,如果知道的话。默认值:8.
-secret-min =
--secret-MAX =
尝试不同的密钥长度(两个选项都是必需的)

OUTPUT OPTIONS
--table
以表格格式输出字符串。
--out数据格式变换=
输出数据格式。
有效格式:none,raw,hex,html,html-pure,
cstr ,cstr- pure,fancy --out-signature-format =
输出签名格式。
有效格式:none,raw,hex,html,html-pure,cstr,cstr-pure,fancy

OTHER OPTIONS
-h --help
显示用法(this)。
--test
运行测试套件。
-q --quiet
只输出绝对必要的内容(输出字符串和
签名)

防御

那么,作为一名程序员,你如何解决这个问题呢?它实际上非常简单。有两种方法:

  • 如果可以避免,请不要信任具有加密数据或签名的用户。
  • 如果你无法避免它,那么使用HMAC而不是试图自己做。HMAC就是为此而设计的。

HMAC是真正的解决方案。HMAC设计用于使用密钥安全地散列数据。

像往常一样,使用为你正在做的而不是自己做的构造。所有加密的关键![双关语]


原文https://blog.skullsecurity.org/2012/everything-you-need-to-know-about-hash-length-extension-attacks