题目

2024 VNCTF SignAhead(完整代码) 中考察了一个情况。

print(known_msg)
print(md5(secret + known_msg))
nmsg = input()
nhash = input()
assert nmsg != known_msg
if md5(secret + nmsg ) == nhash :
    print(flag)

就是在未知一个前缀的情况下,已知前缀长度,hash,能否构造一个字符串并且计算出它和前缀拼接后的hash。

md5算法的部分细节

在这里我们并不需要了解完整的md5签名算法。我们只需要知道如下的操作即可。

这里我们以字符串"1234"为例。

第一部处理将它变成byte,这里为了方便讨论我们使用十六进制表示。

0x31323334

1.填充。

首先如果输入信息的长度对512取模的值不等于448,进行填充。填充内容包括一个0x80(0b10000000)和很多个0x00(0b00000000)。于是填充后长度为N*512+448(bit)

0x3132333480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

2.记录信息长度。

使用64位来记录信息。信息长度以小端序存储。操作方法是((8*4) & 0xffffffffffffffff) .to_bytes(8, byteorder='little').hex()。上面的例子中,长度为4,编码后为2000000000000000。添加在填充后的信息后面。

0x31323334800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000于是我们得到了一个(N+1)*512bit的填充后的块。

3.计算。

这个计算的过程完全可以当成黑盒。我们假设计算是由MD5(msg,[A,B,C,D])函数控制的。返回值为一个数组[AA,BB,CC,DD]

初始有r = [A,B,C,D]=[0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]

我们将填充后的信息分为N+1块,Bi。依次计算r = MD5(Bi,r)

最后得到的r。我们以小端序将其输出,得到md5的字符串。

上面的例子计算后得到

0xdb9bdc81 0xc24dd052 0xd8db3600 0x55d03e31

于是MD5值为81dc9bdb52d04dc20036dbd8313ed055

攻击方法

大概了解了计算过程,我们知道已知前缀的hash意味着知道这一块计算的结果(也就是下一块的初始值)。所以我们只需要补完整前缀那一块,然后在后面添加内容,这样初始[A,B,C,D]已知,字符串已知,可以直接计算出整串的md5。

举个例子,我们现在不知道这个字符串是1234。我们只知道它有四位,且md5值为81dc9bdb52d04dc20036dbd8313ed055

现在我们需要找到一个字符串,求出这个新字符串在前面拼接上1234的MD5值。

于是我们填充这个字符串:AAAA + 800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000。这是完整的一块,它计算的结果我们已知。于是可以直接计算MD5(new_text,[knownABCD])。这整个字符串的md5即可求出。

代码

https://gist.github.com/lbr77/8a36d584cb1ab791f200b22e49d1b1a0

参考

https://zu1k.com/posts/security/crypto/md5-hash-length-extension-attack/

https://zh.wikipedia.org/wiki/%E9%95%BF%E5%BA%A6%E6%89%A9%E5%B1%95%E6%94%BB%E5%87%BB