「密碼學」雜湊為什麼要將鹽加在明文後面?

2023-03-10 18:00:26

眾所周知,在做訊息認證或者簽名時,僅使用hash函數安全性是不高的,容易遭受字典和暴力破解(https://www.cmd5.com/)。所以通常會使用帶金鑰或加鹽的雜湊演演算法作為訊息認證或者口令儲存,正如標題所說,我們在檢索網際網路上關於加鹽的實現時,內容往往都是在明文後面加上隨機值:

那做訊息認證的金鑰或者鹽可不可以加在明文前面呢?

這就引出本文的攻擊方式。

MD5 演演算法計算邏輯

為了清楚這個攻擊方式原理,需要先了解下md5的計算邏輯。

md5演演算法本質上是一種壓縮演演算法,將長度小於2^64bit的任意字元壓縮成128bit固定長度字元。同AES之類的分組加密演演算法一樣,md5也需要進行分組計算。

如圖所示,md5的計算需要經過兩個步驟:

  1. 分組&填充
  2. 具體計算

1.分組&填充

首先會對明文按照512bit的長度進行分組,最後一個分組可能會發生長度不足512bit,或者剛剛512bit。

無論最後一個分組的長度是否剛好等於512bit,按照填充規則都需要進行填充,具體細節:

  • 假設明文剛好能被512整除,需要新增一個分組,在末尾8*8=64bit 按照小端儲存放入原始明文的長度,分組中間剩餘的bit 按照10000000的方式進行填充,形成一個總長512bit的新分組。
  • 假設整除512bit餘數大於0
    • 且餘數大於512-8*8 =448 bit 則需要繼續填充10000(0x80000.....)至下一個分組的448bit,剩餘的bit按照小端儲存填充原始明文長度
    • 餘數小於448bit,末尾填充原始明文長度,中間剩餘部分填充填充10000(0x80000.....)

以上兩個步驟用程式碼錶示即為:

        m_l = len(message)  # 原始訊息長度
        length = struct.pack('<Q', (m_l) * 8)  # 長度轉化為小端 unsigned long long 8B
        blank_padding = b""
        message += b'\x80'  # 10000000
        # 此分組不足以填充長度時
        if 56 < len(message) < 64:
            blank_padding += b'\x00' * (56 + 64 - len(message))  # 填充至下一個分組
        # 分組能填充長度
        else:
            blank_padding = b'\x00' * (56 - len(message) % 64)  # 本分組填充
        if len(blank_padding) > 0:
            # 填充10000
            message += blank_padding
        # 填充長度
        message += length

2. 具體計算

其實具體的計算過程,我們不用關注,把這個過程當做一個黑盒(關注細節的可以關注文末github地址)就可以:

在上一步分組的基礎上,第一組的分組的明文會和128bit的初始序列(幻數)做為輸入進行壓縮計算,初始序列是一組固定的值:

計算後會產生新的序列,做為下一組的「初始序列」和下一組的明文再次進行壓縮計算,接下來的分組重複這種「上一組的輸出作為下一組的輸入」,最後一組的128bit輸出即為最終的md5值。

雜湊長度拓展攻擊

瞭解了md5的計算邏輯,再回到這張圖,上一次的的輸出作為下一次的輸入這種方式可能會導致一個問題。假設存在明文分組abc,明文分組產生md5的過程可以簡化為:

  • 分組a:h.a = md5(iv,a), md5計算需要兩個引數,iv 為初始序列,h.a 為壓縮計算結果
  • 分組b:h.ab = md5(h.a,b)
  • 分組c或者最終md5: h.abc = md5(h.ab,c)

在這個過程當中,如果金鑰被放在a分組當中,bc為原訊息認證的明文,那攻擊者可以在不知道金鑰的情況下,擴充套件明文長度,如增加明文d,計算abcd的hash,只需要知道基於abc的hash值,即可生成新的hash。

即:h.abcd = md5(h.abc,d)

這個過程即為hash長度擴充套件攻擊。

接下來我們根據實際的例子來實操下

實操

假設存在一個商城訂單支付場景,訂單的確認是通過前端引數給出,存在一個邏輯漏洞可以通過前端引數來控制商品價格,從而實現「零元購」或者越權購買。

正常情況下進行購買,因為預設此使用者只有300積分,所以會購買失敗:

但如果進行引數價格good_price修改,會因為簽名校驗不通過:

所以需要進行簽名破解,先看下後端驗證的邏輯:

<?php
	$total_score = 300;
	$flag = 'xxxxxxxxxxxx';
	$secret_key = "??????????????????????????????????????";  // 前端未知
	$post_data =urldecode(file_get_contents("php://input"));
	$user_sign = $_GET['signature'];
	$sign = md5($secret_key.$post_data);
	if ($user_sign === $sign) {
		$price = $_POST['good_price'];
		if ($price > $total_score){
			echo '對不起,您的積分餘額不足,交易失敗!';
		}else{
			echo "恭喜,購買成功!$flag";
		}
	}else{
		echo '簽名資料被篡改!';
	}

?>

後端存在一個簽名邏輯,會驗證使用者的post引數加上金鑰的md5值,如果使用者修改了post引數,但因為不知道金鑰也就沒發生成合法的md5,所以驗證會不通過。

如果沒有了解過雜湊長度擴充套件攻擊,這個程式碼是沒啥問題的,所以知識面決定攻擊面。

而且這個地方金鑰被放在了明文前面拼接,針對雜湊長度擴充套件攻擊,利用起來還挺簡單的,可以使用現成的工具,比如hashpump,按照提示輸入內容即可:

最後的明文中十六進位制部分需要url編碼,但因為hashpump需要編譯,在win平臺編譯比較麻煩,所以我自己實現了一個md5版本的利用工具(https://github.com/shellfeel/hash-ext-attack)

最後把得到的結果貼上到burp,成功購買。

總結

文章分析了下md5的計算邏輯,以及雜湊長度擴充套件的攻擊原理,對於此類攻擊的修復,其實很簡單隻需要把金鑰由加在明文前面改為明文後面,或者使用標準的hmac演演算法,hmac演演算法裡面會用金鑰和明文做移位互斥或操作,從而增強hash的安全性,本文是以md5為例,其實對於有著類似M-D結果的hash演演算法都是可以這樣利用的,比如sha-0,sha-1,sha-2 等。

參考

公眾號

歡迎大家關注我的公眾號,這裡有乾貨滿滿的硬核安全知識,和我一起學起來吧!