hash长度拓展攻击

GTL-JU Lv3

一、hash长度攻击的简要介绍

1、首先什么是hash长度拓展攻击?

简单来说,由于hash的生成机制原因,使得我们可以认为的在原先明文数据的基础上添加新的拓展字符,使得原本的加密链变长,进而控制加密链的最后一节,使得我们得以控制最终结果。

也就是说当我们知道hash(secret+data)的值以及secret的长度的情况下,我们就可以推算出hash(secret+data||padding||a)在这里padding是secret后面的填充内容,包含整个消息的长度,a可以是任何数据,我们需要知道secret的长度,这样才能够计算出padding。

2、什么是hash算法?

哈希算法(Hash算法)是一种将任意长度的消息压缩到固定长度的消息摘要的数学函数。哈希算法将输入消息(也称为明文)作为输入,并生成唯一的固定长度的输出,该输出称为哈希值,摘要或指纹。哈希值通常用于数字签名,数据完整性校验,数据索引和加密等安全应用中。常见的hash算法包括md5,sha-1,sha-256等。

二、MD5算法的加密流程

想要搞清楚hash长度拓展攻击的逻辑,就要先理清楚hash算法的加密流程。

这里以md5加密为例进行分析。

1
2
3
4
5
6
md5的加密流程,大概分为以下几部分:
1、填充消息
2、添加长度信息
3、初始化状态
4、分组处理进行复杂函数处理
5、输出结果

下面我们大概分析一下每个步骤的过程:

1、填充消息

将原始消息(字节序列)填充到长度为448 mod 512的位置,使得填充后的消息长度为512的整数倍。填充方式为在原始消息末尾添加一个1,后面再补0直到长度满足要求。

也就是说当消息长度小于56个字节时要讲其填充到56个字节,大于等于56字节的要填充到对64取余的余数为8个字节.

如下图所示:

加入我们对,message进行填充:

image-20230225145218735

这里的80是16进制,其代表的是二进制下面的10000000,那么这里就是补一个1和若干0,把消息补位到56个字节,也就是448bit.

2、存储长度信息

上面补位后,上面消息长度以及达到了56字节,从第57字节开始存储补位之前消息的长度

长度是小端存储。也就是高字节存放在高地址

我们还以上面的例为例:

字符串message的长度为7个字母,也就是56byte 换算成16进制是0x38

即:

image-20230225150939934

3、初始化状态

md5使用四个32为寄存器(A,B,C,D)保存中间运算结果,初始值为常量,具体来说,A,B,C,D的初始值如下:

1
2
3
4
A = 0x67452301
B = 0xEFCDAB89
C = 0x98BADCFE
D = 0x10325476

4、分组处理进行复杂函数处理

将填充后的消息分为若干个512位的消息块。对于每个消息块,MD5算法执行四轮循环,每轮循环包含16次操作,共计64次操作。每次操作都使用一个消息块中的32位字作为输入,对寄存器A、B、C、D进行修改,最终输出新的A、B、C、D的值。

也就是说第一个数据块与初始向量进行四轮循环,生成第一个新的字符串,保存在寄存器A,B,C,D中,寄存器继续与第二个数据块进行运算,直到最后一个数据块。

其过程可以理解为:

image-20230226120409357

5、结果的输出

假设最终生成的寄存器的值是:

1
2
3
4
A=0xab45bc01
B=0x6a64bb53
C=0x23ba8afe
D=0x46847a62

先两两一组进行组合,得到下面的数据

1
2
3
4
ab 45 bc 01
6a 64 bb 53
23 ba 8a fe
46 84 7a 62

在进行高低位互换:

1
2
01 bc 45 ab
53 bb 64 6a

最终拼接在一起就能够得到md5的值

这就是md5加密的大概过程

下面这是网上找来的流程图:

image-20230225153856884

下面是我按自己的理解搞得一个流程图:

image-20230226120429193

三、hash长度拓展攻击逻辑分析

上面我们对md5的加密流程进行了大概的分析

下面我们通过md5的加密流程对hash长度拓展攻击逻辑进行分析

我们这里通过一道题目进行分析:

这是22年12月举办的铁三信息安全比赛的一道原题:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 <?php
error_reporting(0);
include "flag.php";
$user = $_POST['user'];
function encrypt($text)
{
global $key;
return md5($key . $text);
}
if (encrypt($user) === $_COOKIE['verify']) {
if (is_numeric(strpos($user, 'root'))) {
die($flag);
} else {
die('not root!!!');
}
} else {
setcookie("verify", encrypt("guest"), time() + 60 * 60 * 24);
setcookie("len", strlen($key), time() + 60 * 60 * 24);
}
highlight_file(__FILE__);
?>

这里可以看到

获得flag的条件:

1
2
if (encrypt($user) === $_COOKIE['verify']) {
if (is_numeric(strpos($user, 'root'))) {

我们输入的user经过md5加密后要与cookie里面的vefify相等,并且在输入的user里面要有root

看一下响应包:

image-20230225231719067

可以得到hash(secert+guest)的值为382441859bb6709d0d9fa11ef3c255b9,secert的长度为13

那我们这里重复一下md5加密的流程:

首先是数据填充:(这里假设secret是13个A)

image-20230225232048215

然后进行原始消息数据填充

image-20230225232735247

18个字符,144byte 转换为16进制表示为0x90

后面就是将填充后的消息分成若干个512位的位块,然后与初始向量进行四轮循环运算,这里不再详细讲,

直到最后一个数据块与寄存器中的向量值进行四轮损害运算,得到最终向量值(A’,B’,C’,D’),在经过高低位运算就可以得到最终的md5加密的hash值。

那么我们回到题目,通过响应包我们可以得到hash(secret+guest)的值382441859bb6709d0d9fa11ef3c255b9

那我们可以推出最后得到向量值为:

1
2
3
4
A'=0x85412438
B'=0x9d70b69b
C'=0x1ea19f0d
D'=0xb955c2f3

题目要求要匹配到root,但是我们并不知道secret,所以这里可以使用hash长度拓展攻击

首先我们把要添加的数据添加上去

image-20230225234731479

根据md5填充规则进行填充:

image-20230225234847560

去掉前面的我们假设的secret给去除掉,可以得到

1
guest\0x80\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00....\0x00\0x00\0x00\0x00\0x00\0x00\0x90\0x00\0x00\0x00\0x00\0x00\0x00\0x00root

进行url编码:

1
guest%00x80%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00..x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x00%00x90%00x00%00x00%00x00%00x00%00x00%00x00%00x00root

这里我们虽然通过添加的方法使得字符串中有了root,但是由于我们并不知道secret,导致我们并没有办法计算出md5加密后hash值。但是由于我们实在hash(secret+guest)的hash值,但是我们这里填加了数据,所以导致还要再进行一轮运算,那么我们就可以再不知道secret的情况下计算出正确的md5加密的hash值。

这里我们上面计算出了最后一轮加密的向量值为:

1
2
3
4
A'=0x85412438
B'=0x9d70b69b
C'=0x1ea19f0d
D'=0xb955c2f3

由于我们添加了新的字符串,导致分组时分的数据块会增加,那么hash(secret+guest)的值并不是我们最终要得到的md5值,而是做完向量串继续与后面新增加的数据块进行运算,那么这样的话我们就可以再不知道secret的情况下获得md5加密的hash值。

但是这个计算式非常复杂的,我们这里使用找到的脚本进行计算:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
my_md5.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author:DshtAnger
# theory reference:
# blog:
# http://blog.csdn.net/adidala/article/details/28677393
# http://blog.csdn.net/forgotaboutgirl/article/details/7258109
# http://blog.sina.com.cn/s/blog_6fe0eb1901014cpl.html
# RFC1321:
# https://www.rfc-editor.org/rfc/pdfrfc/rfc1321.txt.pdf
##############################################################################
import sys
def genMsgLengthDescriptor(msg_bitsLenth):
'''
---args:
msg_bitsLenth : the bits length of raw message
--return:
16 hex-encoded string , i.e.64bits,8bytes which used to describe the bits length of raw message added after padding
'''
return __import__("struct").pack(">Q",msg_bitsLenth).encode("hex")

def reverse_hex_8bytes(hex_str):
'''
--args:
hex_str: a hex-encoded string with length 16 , i.e.8bytes
--return:
transform raw message descriptor to little-endian
'''
hex_str = "%016x"%int(hex_str,16)
assert len(hex_str)==16
return __import__("struct").pack("<Q",int(hex_str,16)).encode("hex")

def reverse_hex_4bytes(hex_str):
'''
--args:
hex_str: a hex-encoded string with length 8 , i.e.4bytes
--return:
transform 4 bytes message block to little-endian
'''
hex_str = "%08x"%int(hex_str,16)
assert len(hex_str)==8
return __import__("struct").pack("<L",int(hex_str,16)).encode("hex")

def deal_rawInputMsg(input_msg):
'''
--args:
input_msg : inputed a ascii-encoded string
--return:
a hex-encoded string which can be inputed to mathematical transformation function.
'''
ascii_list = [x.encode("hex") for x in input_msg]
length_msg_bytes = len(ascii_list)
length_msg_bits = len(ascii_list)*8
#padding
ascii_list.append('80')
while (len(ascii_list)*8+64)%512 != 0:
ascii_list.append('00')
#add Descriptor
ascii_list.append(reverse_hex_8bytes(genMsgLengthDescriptor(length_msg_bits)))
return "".join(ascii_list)



def getM16(hex_str,operatingBlockNum):
'''
--args:
hex_str : a hex-encoded string with length in integral multiple of 512bits
operatingBlockNum : message block number which is being operated , greater than 1
--return:
M : result of splited 64bytes into 4*16 message blocks with little-endian

'''
M = [int(reverse_hex_4bytes(hex_str[i:(i+8)]),16) for i in xrange(128*(operatingBlockNum-1),128*operatingBlockNum,8)]
return M

#定义函数,用来产生常数T[i],常数有可能超过32位,同样需要&0xffffffff操作。注意返回的是十进制的数
def T(i):
result = (int(4294967296*abs(__import__("math").sin(i))))&0xffffffff
return result

#定义每轮中用到的函数
#RL为循环左移,注意左移之后可能会超过32位,所以要和0xffffffff做与运算,确保结果为32位
F = lambda x,y,z:((x&y)|((~x)&z))
G = lambda x,y,z:((x&z)|(y&(~z)))
H = lambda x,y,z:(x^y^z)
I = lambda x,y,z:(y^(x|(~z)))
RL = L = lambda x,n:(((x<<n)|(x>>(32-n)))&(0xffffffff))

def FF(a, b, c, d, x, s, ac):
a = (a+F ((b), (c), (d)) + (x) + (ac)&0xffffffff)&0xffffffff;
a = RL ((a), (s))&0xffffffff;
a = (a+b)&0xffffffff
return a
def GG(a, b, c, d, x, s, ac):
a = (a+G ((b), (c), (d)) + (x) + (ac)&0xffffffff)&0xffffffff;
a = RL ((a), (s))&0xffffffff;
a = (a+b)&0xffffffff
return a
def HH(a, b, c, d, x, s, ac):
a = (a+H ((b), (c), (d)) + (x) + (ac)&0xffffffff)&0xffffffff;
a = RL ((a), (s))&0xffffffff;
a = (a+b)&0xffffffff
return a
def II(a, b, c, d, x, s, ac):
a = (a+I ((b), (c), (d)) + (x) + (ac)&0xffffffff)&0xffffffff;
a = RL ((a), (s))&0xffffffff;
a = (a+b)&0xffffffff
return a

def show_md5(A,B,C,D):
return "".join( [ "".join(__import__("re").findall(r"..","%08x"%i)[::-1]) for i in (A,B,C,D) ] )

def run_md5(A=0x67452301,B=0xefcdab89,C=0x98badcfe,D=0x10325476,readyMsg=""):

a = A
b = B
c = C
d = D

for i in xrange(0,len(readyMsg)/128):
M = getM16(readyMsg,i+1)
for i in xrange(16):
exec "M"+str(i)+"=M["+str(i)+"]"
#First round
a=FF(a,b,c,d,M0,7,0xd76aa478L)
d=FF(d,a,b,c,M1,12,0xe8c7b756L)
c=FF(c,d,a,b,M2,17,0x242070dbL)
b=FF(b,c,d,a,M3,22,0xc1bdceeeL)
a=FF(a,b,c,d,M4,7,0xf57c0fafL)
d=FF(d,a,b,c,M5,12,0x4787c62aL)
c=FF(c,d,a,b,M6,17,0xa8304613L)
b=FF(b,c,d,a,M7,22,0xfd469501L)
a=FF(a,b,c,d,M8,7,0x698098d8L)
d=FF(d,a,b,c,M9,12,0x8b44f7afL)
c=FF(c,d,a,b,M10,17,0xffff5bb1L)
b=FF(b,c,d,a,M11,22,0x895cd7beL)
a=FF(a,b,c,d,M12,7,0x6b901122L)
d=FF(d,a,b,c,M13,12,0xfd987193L)
c=FF(c,d,a,b,M14,17,0xa679438eL)
b=FF(b,c,d,a,M15,22,0x49b40821L)
#Second round
a=GG(a,b,c,d,M1,5,0xf61e2562L)
d=GG(d,a,b,c,M6,9,0xc040b340L)
c=GG(c,d,a,b,M11,14,0x265e5a51L)
b=GG(b,c,d,a,M0,20,0xe9b6c7aaL)
a=GG(a,b,c,d,M5,5,0xd62f105dL)
d=GG(d,a,b,c,M10,9,0x02441453L)
c=GG(c,d,a,b,M15,14,0xd8a1e681L)
b=GG(b,c,d,a,M4,20,0xe7d3fbc8L)
a=GG(a,b,c,d,M9,5,0x21e1cde6L)
d=GG(d,a,b,c,M14,9,0xc33707d6L)
c=GG(c,d,a,b,M3,14,0xf4d50d87L)
b=GG(b,c,d,a,M8,20,0x455a14edL)
a=GG(a,b,c,d,M13,5,0xa9e3e905L)
d=GG(d,a,b,c,M2,9,0xfcefa3f8L)
c=GG(c,d,a,b,M7,14,0x676f02d9L)
b=GG(b,c,d,a,M12,20,0x8d2a4c8aL)
#Third round
a=HH(a,b,c,d,M5,4,0xfffa3942L)
d=HH(d,a,b,c,M8,11,0x8771f681L)
c=HH(c,d,a,b,M11,16,0x6d9d6122L)
b=HH(b,c,d,a,M14,23,0xfde5380c)
a=HH(a,b,c,d,M1,4,0xa4beea44L)
d=HH(d,a,b,c,M4,11,0x4bdecfa9L)
c=HH(c,d,a,b,M7,16,0xf6bb4b60L)
b=HH(b,c,d,a,M10,23,0xbebfbc70L)
a=HH(a,b,c,d,M13,4,0x289b7ec6L)
d=HH(d,a,b,c,M0,11,0xeaa127faL)
c=HH(c,d,a,b,M3,16,0xd4ef3085L)
b=HH(b,c,d,a,M6,23,0x04881d05L)
a=HH(a,b,c,d,M9,4,0xd9d4d039L)
d=HH(d,a,b,c,M12,11,0xe6db99e5L)
c=HH(c,d,a,b,M15,16,0x1fa27cf8L)
b=HH(b,c,d,a,M2,23,0xc4ac5665L)
#Fourth round
a=II(a,b,c,d,M0,6,0xf4292244L)
d=II(d,a,b,c,M7,10,0x432aff97L)
c=II(c,d,a,b,M14,15,0xab9423a7L)
b=II(b,c,d,a,M5,21,0xfc93a039L)
a=II(a,b,c,d,M12,6,0x655b59c3L)
d=II(d,a,b,c,M3,10,0x8f0ccc92L)
c=II(c,d,a,b,M10,15,0xffeff47dL)
b=II(b,c,d,a,M1,21,0x85845dd1L)
a=II(a,b,c,d,M8,6,0x6fa87e4fL)
d=II(d,a,b,c,M15,10,0xfe2ce6e0L)
c=II(c,d,a,b,M6,15,0xa3014314L)
b=II(b,c,d,a,M13,21,0x4e0811a1L)
a=II(a,b,c,d,M4,6,0xf7537e82L)
d=II(d,a,b,c,M11,10,0xbd3af235L)
c=II(c,d,a,b,M2,15,0x2ad7d2bbL)
b=II(b,c,d,a,M9,21,0xeb86d391L)


A += a
B += b
C += c
D += d

A = A&0xffffffff
B = B&0xffffffff
C = C&0xffffffff
D = D&0xffffffff

a = A
b = B
c = C
d = D

return show_md5(a,b,c,d)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
exp.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import my_md5
samplehash="382441859bb6709d0d9fa11ef3c255b9"
#将哈希值分为四段,并反转该四字节为小端序,作为64第二次循环的输入幻书
s1=0x85412438
s2=0x9d70b69b
s3=0x1ea19f0d
s4=0xb955c2f3
#exp
secret = "A"*13
secret_admin = secret + 'guest{padding}'
padding = '\x80{zero}\xc8\x00\x00\x00\x00\x00\x00\x00'.format(zero="\x00"*(64-15-10-1-8))
secret_admin = secret_admin.format(padding=padding) + 'root'
r = my_md5.deal_rawInputMsg(secret_admin)
inp = r[len(r)/2:] #我们需要截断的地方,也是我们需要控制的地方
print "getmein:"+my_md5.run_md5(s1,s2,s3,s4,inp)

这里使用脚本没有计算出来,可能是自己哪里的配置有错误,但是脚本不知道怎么改,就只能使用其他师傅写好的工具去运算。

流程图:

image-20230227225406242

image-20230227225415932

image-20230227225430350

四、hashpump工具的安装与使用

1
2
3
4
5
git clone https://github.com/bwall/HashPump
apt-get install g++ libssl-dev
cd HashPump
make
make install

使用方法:

image-20230226001051157

那么我们回到题目进行验证:

进行url编码:

1
guest%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%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00root

image-20230226001235963

可以看到以及攻击成功了。

五、防御方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
以下是一些防御哈希长度拓展攻击的方法:

1、使用加盐(Salting)技术

加盐技术是将一个随机字符串添加到原始数据之后,再进行哈希计算。由于加盐字符串是随机的,攻击者无法通过已知的哈希值推算出加盐字符串的内容,从而无法利用哈希长度拓展攻击。加盐技术是防御哈希长度拓展攻击的常见方法。

2、使用不可逆加密算法

不可逆加密算法将原始数据转换为不可逆的密文,防止攻击者通过逆向计算推算出原始数据。相比哈希算法,不可逆加密算法的计算复杂度更高,从而更加安全。

3、使用HMAC技术

HMAC是一种基于哈希算法的消息认证码技术,它将密钥与消息进行混合计算,生成一个认证码,以此保证消息的完整性和真实性。HMAC技术可以在保证消息认证的同时,防止哈希长度拓展攻击。

4、使用较长的哈希值

较长的哈希值可以增加哈希长度拓展攻击的难度,因为攻击者需要计算更多的哈希值才能找到一个合法的哈希值。SHA-512和SHA-3等哈希算法提供了较长的哈希值选项。

5、使用加密哈希算法

加密哈希算法是一种特殊的哈希算法,它在计算哈希值的同时,还将密钥混合到哈希值中,从而保证了哈希值的安全性和不可逆性。常见的加密哈希算法包括HMAC-SHA256和bcrypt等。
  • 标题: hash长度拓展攻击
  • 作者: GTL-JU
  • 创建于: 2023-04-08 16:12:24
  • 更新于: 2023-04-08 16:20:46
  • 链接: https://gtl-ju.github.io/2023/04/08/hashc长度拓展攻击/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。