基于md5加密的hash长度扩展攻击介绍


md5算法介绍

$~~~~~~$md5将整个文件当做一个大文本信息,通过不可逆的字符串变换算法,产生一个唯一的MD5信息摘要。文件的md5类似于人的指纹,在世界上是独立无二的,如果任何人对文件做了任何改动,其md5的值也就是对应的“数字指纹”都会发生变化。
$~~~~~~$对MD5算法简要的叙述可以为:MD5以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。

填充

在MD5算法中,首先需要对信息进行填充,使其位长对512求余的结果等于448,并且填充必须进行,即使其位长对512求余的结果等于448。因此,信息的位长(Bits Length)将被扩展至N*512+448,N为一个非负整数,N可以是零。
填充的方法如下:

  1. 在信息的后面填充一个1和无数个0,直到满足上面的条件时才停止用0对信息的填充。
  2. 在这个结果后面附加一个以64位二进制表示的填充前信息长度(单位为Bit),如果二
    进制表示的填充前信息长度超过64位,则取低64位。
    经过这两步的处理,信息的位长=N512+448+64=(N+1)512,即长度恰好是512的整数倍。这样做的原因是为满足后面处理中对信息长度的要求。

初始化变量

初始的128位值为初试链接变量,这些参数用于第一轮的运算,以大端字节序来表示,他们分别为: A=0x01234567,B=0x89ABCDEF,C=0xFEDCBA98,D=0x76543210。
(每一个变量给出的数值是高字节存于内存低地址,低字节存于内存高地址,即大端字节序。在程序中变量A、B、C、D的值分别为0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476)

处理分组数据

每一分组的算法流程如下:
第一分组需要将上面四个链接变量复制到另外四个变量中:A到a,B到b,C到c,D到d。从第二分组开始的变量为上一分组的运算结果,即A = a, B = b, C = c, D = d。
主循环有四轮(MD4只有三轮),每轮循环都很相似。第一轮进行16次操作。每次操作对a、b、c和d中的其中三个作一次非线性函数运算,然后将所得结果加上第四个变量,文本的一个子分组和一个常数。再将所得结果向左环移一个不定的数,并加上a、b、c或d中之一。最后用该结果取代a、b、c或d中之一。
以下是每次操作中用到的四个非线性函数(每轮一个)。
F( X ,Y ,Z ) = ( X & Y ) | ( (X) & Z )
G( X ,Y ,Z ) = ( X & Z ) | ( Y & (
Z) )
H( X ,Y ,Z ) =X ^ Y ^ Z
I( X ,Y ,Z ) =Y ^ ( X | (Z) )
(&是与(And),|是或(Or),
是非(Not),^是异或(Xor))
这四个函数的说明:如果X、Y和Z的对应位是独立和均匀的,那么结果的每一位也应是独立和均匀的。
F是一个逐位运算的函数。即,如果X,那么Y,否则Z。函数H是逐位奇偶操作符。

输出

最后的输出是a、b、c和d的级联。

hash长度扩展攻击

通过以上的md5介绍,我们发现md5每次处理的都是512bit并且最后的hash值也是由A,B,C,D的值来决定,并且根据hash值我们还能逆推出最后一轮的ABCD的值。

那现在我们假设一种情况

我们先加密“admin“看看结果:

21232f297a57a5a743894a0e4a801fc3

同时加密的ABCD值为:

A:0x292f2321
B:0xa7a5577a
C:0xe4a8943
D:0xc31f804a

还有对admin进行填充的后的结果:(这里一个数代表4个字节)

[1768776801, 32878, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0]

那么现在我们思考一个问题,如果我对admin连续加密两次注意不是加密adminadmin
先对admin加密,不满512位的位置用1和0填充,然后再最后再加上admin,相当于生成一个如下的填充序列:

[1768776801, 32878, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0, 1768776801, 32878, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0]

对这个填充序列的加密结果如下:

58a6ff510dbbeb3d3ce8d971a9f77ff3

这个时候我们思考一下对于这个填充序列的加密方式,应该是将这个序列分为了两段,先对这一段**[1768776801, 32878, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0]**加密,再用新生成的ABCD值对剩下的加密。

那么,我们是不是可以理解成如下形式:

//有一个字符串str,由a和b组成
//变量a无法控制,但是我知道其长度还有str的md5加密值,变量b我能够控制
//这样的情况下我依然可以找到str的hash值
$str=$a+$b;

例如根据上面的md5加密的结果,我们可以逆推出ABCD的值为:

A = 0x292f2321
B = 0xa7a5577a
C = 0xe4a8943
D = 0xc31f804a

然后我们修改md5算法的初始ABCD值为上述值:再次计算admin的值:

58a6ff510dbbeb3d3ce8d971a9f77ff3

我们发现得到的结果是一样的,产生了hash长度扩展攻击。

CTF题目

这是一道CTF的关于hash长度扩展攻击的题目:

<?php

error_reporting(0);
$flag = file_get_contents('./flag.php');

if (isset($_GET["md5"]) && isset($_GET["i"]) && isset($_GET["s"])) {
    $fl4g = substr_replace($flag, $_GET["s"], $_GET["i"], 1);
    echo $_GET["md5"] === md5($fl4g);
} else {
    highlight_file(__FILE__);
    echo md5($flag . "yusa");

分析代码发现,其实需要解决的的就是这里:

 $fl4g = substr_replace($flag, $_GET["s"], $_GET["i"], 1);
    echo $_GET["md5"] === md5($fl4g);

根据我们之前讲的原理,先构造一个512bit的填充字符然后将flag+yusa的加密结果逆推出ABCD的值,最后根据计算出的超过512bit填充序列以外的加密结果与直接加密的结果比较,爆破出flag,这里结果就给大家了,把我自己的调试脚本放给大家。

exp


import binascii

A = 0x67452301
B = 0xefcdab89
C = 0x98badcfe
D = 0x10325476
K = [  0xd76aa478 , 0xe8c7b756 , 0x242070db , 0xc1bdceee ,
       0xf57c0faf , 0x4787c62a , 0xa8304613 , 0xfd469501 ,
       0x698098d8 , 0x8b44f7af , 0xffff5bb1 , 0x895cd7be ,
       0x6b901122 , 0xfd987193 , 0xa679438e , 0x49b40821 ,
       0xf61e2562 , 0xc040b340 , 0x265e5a51 , 0xe9b6c7aa ,
       0xd62f105d , 0x02441453 , 0xd8a1e681 , 0xe7d3fbc8 ,
       0x21e1cde6 , 0xc33707d6 , 0xf4d50d87 , 0x455a14ed ,
       0xa9e3e905 , 0xfcefa3f8 , 0x676f02d9 , 0x8d2a4c8a ,
       0xfffa3942 , 0x8771f681 , 0x6d9d6122 , 0xfde5380c ,
       0xa4beea44 , 0x4bdecfa9 , 0xf6bb4b60 , 0xbebfbc70 ,
       0x289b7ec6 , 0xeaa127fa , 0xd4ef3085 , 0x04881d05 ,
       0xd9d4d039 , 0xe6db99e5 , 0x1fa27cf8 , 0xc4ac5665 ,
       0xf4292244 , 0x432aff97 , 0xab9423a7 , 0xfc93a039 ,
       0x655b59c3 , 0x8f0ccc92 , 0xffeff47d , 0x85845dd1 ,
       0x6fa87e4f , 0xfe2ce6e0 , 0xa3014314 , 0x4e0811a1 ,
       0xf7537e82 , 0xbd3af235 , 0x2ad7d2bb , 0xeb86d391 ,]
maxInt=0x100000000
S = [ 7 , 12 , 17 , 22 , 7 , 12 , 17 , 22 ,
      7 , 12 , 17 , 22 , 7 , 12 , 17 , 22 ,
      5 ,  9 , 14 , 20 , 5 ,  9 , 14 , 20 ,
      5 ,  9 , 14 , 20 , 5 ,  9 , 14 , 20 ,
      4 , 11 , 16 , 23 , 4 , 11 , 16 , 23 ,
      4 , 11 , 16 , 23 , 4 , 11 , 16 , 23 ,
      6 , 10 , 15 , 21 , 6 , 10 , 15 , 21 ,
      6 , 10 , 15 , 21 , 6 , 10 , 15 , 21 ,]

def fill(sequence):
    '将字节序列按小端序填充成512位【16整数*4字节】的倍数'
    count=len(sequence)
    #print('输入序列长:{}'.format(count))
    multi_16s=((count+8)//64+1)*16              # 共需要整数的个数,每个整数存储4个字节的数据
    # print('需要整数个数,每个存储4个字节数据:{}'.format(multi_16s))
    sequence+=[0]*(multi_16s*4-count)           # 用 0 填充
    sequence[count] |= 128                      # 用一个 1 补在后面


    print('填充序列:{}'.format(sequence))
    multi_4bytes=[]
    print(f'sequence的长度{len(sequence)}')
    for i in range(len(sequence)//4):
        sequence[i*4+3],sequence[i*4+2],sequence[i*4+1],sequence[i*4]=tuple(sequence[i*4:(i+1)*4])              # 大端序存储
        multi_4bytes.append(int("".join(["{:08b}".format(ii) for ii in sequence[i*4:(i+1)*4]]),2))              # 每四个Ascii合并成一个4字节整数
    multi_4bytes[-2],multi_4bytes[-1]=int("{:064b}".format(count*8)[32:],2),int("{:064b}".format(count*8)[:32],2)
    print(multi_4bytes)
    print(len(multi_4bytes))
    return multi_4bytes

def shift(x,n):
    '循环左移'
    return (( x << n ) | (x >> (32-n)))

def F(X,Y,Z):return (X&Y)|((~X)&Z)
def G(X,Y,Z):return (X&Z)|(Y&(~Z))
def H(X,Y,Z):return X^Y^Z
def I(X,Y,Z):return Y^(X|(~Z))
def Go(a,b,c,d,fun,m,s,K):
    thesum=(a + fun(b, c, d) + int(m) + K)%maxInt
    return (b+shift(thesum,s))%maxInt

def int32ToHex(a):
    '32位整型集合转16进制'
    md5=''
    for i in a:
        x="{:08x}".format(i)          # 整型【32位2】->8位16
        md5+=x[6:]+x[4:6]+x[2:4]+x[:2]          # 每两位切割, 切割4刀->逆序【大端变小端】
    return md5

if __name__ == "__main__":
    text=input("请输入要摘要的字符串:")

    sequence=list(bytes(text,'utf-8'))      # 将unicode转换为字节序列
    #print(sequence)

    strm = "".join('{:08b}'.format(w) for w in sequence)
    print('输入的待测序列:{}'.format(strm))

    text_int4=fill(sequence)                # 将字节序列按小端序填充成4字节整数
    str=[1768776801, 32878, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 0]
    text_int4=text_int4+str
    #print(text_int4)
    for i in range(len(text_int4)//16):             # 主循环
        a,b,c,d=A,B,C,D
        M = [text_int4[i*16+ii] for ii in range(16)]        # 取出16个整数
        # print(M)
        for ii in range(64):
            if ii<16:   A,B,C,D=D,Go(A,B,C,D,F,M[ii],S[ii],K[ii]),B,C
            elif ii<32: A,B,C,D=D,Go(A,B,C,D,G,M[(ii*5+1)%16],S[ii],K[ii]),B,C
            elif ii<48: A,B,C,D=D,Go(A,B,C,D,H,M[((ii*3)+5)%16],S[ii],K[ii]),B,C
            else:       A,B,C,D=D,Go(A,B,C,D,I,M[ii*7%16],S[ii],K[ii]),B,C
        A,B,C,D=(A+a)%maxInt,(B+b)%maxInt,(C+c)%maxInt,(D+d)%maxInt                             # 此处还是大端字节
        print(f'第{i+1}轮的ABCD: \n A:{hex(A)} \n B:{hex(B)} \n C:{hex(C)} \n D:{hex(D)}')
    md5 = int32ToHex([A,B,C,D])
    # print([hex(A),hex(B),hex(C),hex(D)])
    print('链接变量A的32位二进制结果:{:032b}'.format(A))
    print("整个待测序列的MD5值:",md5)

文章作者: kento
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kento !
评论
  目录