工具從原理到實(shí)現(xiàn):手寫C語(yǔ)言版md5.exe)
簡(jiǎn)介這套MD5驗(yàn)證工具由可執(zhí)行程序與C語(yǔ)言源碼組成面向需要深入理解MD5算法內(nèi)部實(shí)現(xiàn)的學(xué)生、開發(fā)者及安全測(cè)試人員可用于驗(yàn)證輸入字符串的哈希結(jié)果并逐個(gè)步驟查看每一輪運(yùn)算產(chǎn)生的中間狀態(tài)。工具通過命令行傳入?yún)?shù)即可運(yùn)行例如在終端切換到工具目錄后運(yùn)行工具并傳入待計(jì)算字符串程序會(huì)打印MD5處理過程中每一步的結(jié)果對(duì)應(yīng)的C語(yǔ)言源碼完整給出了實(shí)現(xiàn)邏輯其中源碼實(shí)現(xiàn)始終將變量b作為當(dāng)前運(yùn)算對(duì)象便于對(duì)照代碼梳理MD5壓縮函數(shù)中寄存器的變化過程。壓縮包總共2個(gè)文件分別是一個(gè)可直接運(yùn)行的exe可執(zhí)行程序和一個(gè)C語(yǔ)言源碼文件整體大小僅28KB小巧易用適合作為密碼學(xué)實(shí)驗(yàn)的輔助工具或二次開發(fā)基礎(chǔ)。目前已有276人學(xué)習(xí)下載對(duì)打算通過調(diào)試和源碼對(duì)照來掌握MD5算法細(xì)節(jié)的讀者具有不錯(cuò)的參考價(jià)值也能幫助排查自己實(shí)現(xiàn)中的常見錯(cuò)誤。 很多人下載完軟件、固件或源碼包第一反應(yīng)是直接打開用直到某天文件損壞、解壓報(bào)錯(cuò)甚至被植入奇怪東西時(shí)才想起來校驗(yàn)這一步。我自己的習(xí)慣是凡是官方頁(yè)面上給了MD5值的文件下載完必須先過一遍hash再動(dòng)手。為了這件事我干脆寫了一個(gè)免安裝、體積只有幾十KB的Windows小工具md5.exe配套源碼就是md5.c總共也就三百來行邏輯清晰連C語(yǔ)言剛?cè)腴T的人都能讀懂。這篇文章就把這個(gè)工具從原理到實(shí)現(xiàn)、從編譯到使用完整拆開講一遍適合需要給文件做完整性校驗(yàn)、想搞懂MD5內(nèi)部機(jī)制、或者是想在嵌入式/服務(wù)器環(huán)境里自己編譯一個(gè)校驗(yàn)工具的朋友參考。1. 為什么需要一個(gè)離線自足的MD5校驗(yàn)工具先說清楚MD5在這個(gè)場(chǎng)景里的定位。MD5是一種消息摘要算法輸入任意長(zhǎng)度的數(shù)據(jù)輸出固定128位16字節(jié)的摘要值通常顯示成32位的十六進(jìn)制字符串。它的特點(diǎn)是只要原始內(nèi)容改動(dòng)一個(gè)比特摘要值就會(huì)面目全非所以被廣泛用來做文件完整性校驗(yàn)、下載一致性比對(duì)、版本發(fā)布指紋標(biāo)識(shí)。你可能會(huì)說Windows系統(tǒng)不是自帶certutil -hashfile嗎Linux也有md5sum為什么還要自己寫一個(gè)md5.exe我當(dāng)時(shí)的場(chǎng)景是這樣的在客戶內(nèi)網(wǎng)的一臺(tái)Windows機(jī)器上系統(tǒng)是精簡(jiǎn)版連PowerShell都不完整certutil被安全策略禁用了我又不能隨便裝第三方軟件唯一能用的就是U盤里一個(gè)gcc編譯出來的單文件exe。這種時(shí)候一個(gè)不依賴運(yùn)行時(shí)、不調(diào)用系統(tǒng)額外組件、雙擊即用的工具就是剛需。另外很多場(chǎng)景需要把校驗(yàn)邏輯寫到腳本或CI里比如下載Gradle發(fā)行包、Redis源碼安裝包、嵌入式內(nèi)核源碼歸檔時(shí)官方頁(yè)面通常會(huì)附帶MD5值你需要一個(gè)能在批處理或者命令行里返回明確退出碼的工具。自己做一個(gè)小工具輸出格式、退出碼、批量處理行為都能定制比依賴系統(tǒng)命令更可控。這個(gè)工具的核心就兩個(gè)文件md5.c是全部源碼md5.exe是用gcc編出來的可執(zhí)行文件。沒有資源文件沒有動(dòng)態(tài)鏈接庫(kù)唯一依賴是Windows系統(tǒng)自帶的kernel32.dll和msvcrt.dll所以兼容性極好從Windows 7到Windows 11都能跑。2. 挖開md5.c之前MD5算法內(nèi)部到底做了什么很多人用過MD5但不知道它內(nèi)部長(zhǎng)什么樣。如果你想理解源碼光看代碼是低效的得先看懂算法骨架。MD5的處理流程可以拆成四步填充、初始化、分塊壓縮、輸出。填充這一步非常關(guān)鍵。MD5要求輸入長(zhǎng)度按位計(jì)算最后必須對(duì)齊到512位64字節(jié)的整數(shù)倍。填充規(guī)則是先補(bǔ)一個(gè)0x80字節(jié)然后補(bǔ)0x00直到剩余長(zhǎng)度對(duì)64字節(jié)取模等于56最后8字節(jié)寫入原始數(shù)據(jù)的比特長(zhǎng)度小端序。即使原始數(shù)據(jù)恰好是64字節(jié)的倍數(shù)也仍然要填充這是很多人寫代碼時(shí)容易漏掉的地方。初始化階段用到了四個(gè)固定的32位魔數(shù)A0x67452301B0xEFCDAB89C0x98BADCFED0x10325476。這四個(gè)值是MD5算法規(guī)定的沒有任何推導(dǎo)過程直接背下來用。分塊壓縮是核心。每個(gè)64字節(jié)塊會(huì)被切分成16個(gè)32位字然后執(zhí)行四輪共64步運(yùn)算。四輪的非線性函數(shù)分別是F、G、H、I定義也很經(jīng)典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)每一步會(huì)做一次加法、一次非線性運(yùn)算、一次循環(huán)左移移位量是算法定死的常量表。再加上每輪固定的T表常量這套運(yùn)算的雪崩效應(yīng)非常好輸入微小的變化就能擴(kuò)散到整個(gè)摘要里。我在源碼注釋里把64步的移位量整理成了表這樣做的好處是代碼里可以直接復(fù)用不用每一步都手寫。輸出階段就是把state[0]到state[3]四個(gè)32位整數(shù)按小端序轉(zhuǎn)換成16字節(jié)再格式化成32位十六進(jìn)制字符串這就是用戶看到的MD5值。理解了這四步你再去看md5.c里的代碼會(huì)發(fā)現(xiàn)一切都對(duì)得上號(hào)。3. md5.c實(shí)現(xiàn)解析核心數(shù)據(jù)結(jié)構(gòu)、三個(gè)關(guān)鍵函數(shù)我的md5.c在組織上嚴(yán)格貼合RFC 1321的結(jié)構(gòu)分為MD5Init、MD5Update、MD5Final三個(gè)階段外加一個(gè)內(nèi)部用的MD5Transform。為什么這樣拆因?yàn)槲募r?yàn)通常要分塊讀取不是一次性把所有內(nèi)容塞進(jìn)內(nèi)存流式接口才能支持幾十GB的大文件。核心結(jié)構(gòu)體是這樣的typedef struct { unsigned int state[4]; // A/B/C/D四個(gè)鏈接變量 unsigned int count[2]; // 按64字節(jié)塊計(jì)數(shù)的總數(shù)支持64位長(zhǎng)度 unsigned char buffer[64]; // 當(dāng)前塊緩存 } MD5_CTX;注意count用了兩個(gè)32位無符號(hào)整數(shù)拼成64位計(jì)數(shù)。MD5規(guī)范里輸入長(zhǎng)度可以到$2^{64}$位如果用單個(gè)32位整數(shù)記錄字節(jié)數(shù)超過4GB的文件就會(huì)溢出這是實(shí)現(xiàn)里很容易踩的坑。初始化函數(shù)負(fù)責(zé)填充四個(gè)魔數(shù)并清零計(jì)數(shù)void MD5Init(MD5_CTX *ctx) { ctx-count[0] ctx-count[1] 0; ctx-state[0] 0x67452301; ctx-state[1] 0xEFCDAB89; ctx-state[2] 0x98BADCFE; ctx-state[3] 0x10325476; }MD5Update是核心入口負(fù)責(zé)處理輸入數(shù)據(jù)。它的邏輯是先看buffer里有沒有殘留數(shù)據(jù)把輸入拼滿64字節(jié)后調(diào)一次MD5Transform然后對(duì)剩余數(shù)據(jù)循環(huán)分塊處理。count在這里同步累加。這個(gè)函數(shù)寫得好不好直接決定大文件的處理性能。很多網(wǎng)上流傳的簡(jiǎn)化版MD5實(shí)現(xiàn)不管這個(gè)直接一次性把整個(gè)文件讀進(jìn)內(nèi)存小文件還行大文件要么內(nèi)存爆掉要么效率奇差。MD5Final做兩件事填充和輸出摘要。填充時(shí)會(huì)先算還有多少字節(jié)到56邊界然后補(bǔ)0x80和若干個(gè)0x00最后把count里的位長(zhǎng)度以小端序?qū)戇M(jìn)末尾8字節(jié)。真正寫長(zhǎng)度時(shí)記得要把count值右移3位轉(zhuǎn)換成比特?cái)?shù)unsigned int bits ctx-count[0] 3; unsigned int carry ctx-count[1] 3 | ctx-count[0] 29;MD5Transform里就是四個(gè)輪函數(shù)每輪16步。我實(shí)現(xiàn)的時(shí)候用了宏定義來減少重復(fù)代碼#define F(x, y, z) (((x) (y)) | ((~(x)) (z))) #define G(x, y, z) (((x) (z)) | ((y) (~(z)))) #define H(x, y, z) ((x) ^ (y) ^ (z)) #define I(x, y, z) ((y) ^ ((x) | (~(z))))每輪的操作可以濃縮成一個(gè)通用步驟宏#define STEP(f, a, b, c, d, x, t, s) \ (a) f((b), (c), (d)) (x) (t); \ (a) (((a) (s)) | ((a) (32 - (s)))); \ (a) (b);這樣做不是為了炫技是因?yàn)?4步迭代如果全部展開代碼會(huì)膨脹到兩千行用宏可以把四輪壓縮成40行可讀性反而更高。主函數(shù)main里做的事就簡(jiǎn)單了打開文件用8KB緩沖區(qū)循環(huán)調(diào)用MD5Update最后調(diào)MD5Final拿摘要再按%02x逐字節(jié)打印。我為啥選8KB而不是一次讀完因?yàn)?KB是C標(biāo)準(zhǔn)庫(kù)文件流讀寫效率比較甜點(diǎn)的值太大對(duì)緩存不友好太小系統(tǒng)調(diào)用太頻繁。當(dāng)然你改成64KB也沒問題實(shí)測(cè)性能差別不大。4. 編譯、使用與實(shí)測(cè)輸出編譯這一步非常簡(jiǎn)單。開發(fā)環(huán)境我用的是MinGW-w64自帶的gcc命令行一條命令搞定gcc -O2 -Wall -o md5.exe md5.c-O2是開優(yōu)化-Wall是開警告一個(gè)干凈的項(xiàng)目除了可能的賦值截?cái)嗵崾就獠粦?yīng)該有任何warning。如果你用Visual Studio也可以直接把md5.c拖進(jìn)工程編譯但MinGW編出來的exe更干凈、不依賴VCRUNTIME那套再發(fā)行包。用起來也很直接命令行執(zhí)行md5.exe 文件路徑輸出格式我設(shè)計(jì)成和Linux md5sum一致8a3f4b6c7d8e9f0a1b2c3d4e5f6a7b8c file.iso后面再接文件名這樣在批處理腳本里用for /f解析起來很方便。多文件校驗(yàn)也支持一次性把多個(gè)路徑都傳進(jìn)去就行md5.exe gradle-8.13-src.zip redis-7.2.4.tar.gz實(shí)測(cè)下來一個(gè)1GB左右的鏡像文件這臺(tái)普通辦公電腦上計(jì)算耗時(shí)基本就是一兩秒CPU單核跑滿內(nèi)存占用穩(wěn)定在十幾MB。對(duì)比官方公布的MD5值逐字符比對(duì)一致完全符合預(yù)期??紤]到Windows命令行經(jīng)常跟抽屜一樣亂我在程序里還做了幾個(gè)細(xì)節(jié)處理文件打不開時(shí)向stderr輸出錯(cuò)誤信息并返回非零退出碼路徑帶空格時(shí)要求調(diào)用方自己加引號(hào)校驗(yàn)值統(tǒng)一小寫輸出避免和官方大小寫混用導(dǎo)致比對(duì)失敗。下面用一張表總結(jié)常用場(chǎng)景使用場(chǎng)景推薦做法校驗(yàn)單個(gè)軟件安裝包官方給32位MD5直接運(yùn)行md5.exe比對(duì)批量校驗(yàn)?zāi)夸浵露鄠€(gè)包for %f in (*.zip) do md5.exe %f集成到CI腳本判斷構(gòu)建產(chǎn)物腳本里比對(duì)輸出非零退出碼表示不一致驗(yàn)證嵌入式固件/內(nèi)核源碼包先確認(rèn)下載頁(yè)給的是MD5還是SHA別混用大文件一致性確認(rèn)流式分塊處理不吃內(nèi)存5. 關(guān)于MD5安全性反查、彩虹表與加鹽既然標(biāo)題里有“md5彩虹表查詢”這類熱詞我就把這塊也講透。很多人一聽到MD5就扣上“不安全”的帽子其實(shí)需要區(qū)分用途。MD5的弱點(diǎn)主要體現(xiàn)在兩個(gè)維度一是抗碰撞性已經(jīng)被攻破有人能構(gòu)造出兩個(gè)內(nèi)容不同但MD5相同的文件二是針對(duì)密碼這種短低熵場(chǎng)景攻擊者可以用彩虹表做反查也就是把常見密碼的MD5值預(yù)先算好存起來拿到摘要后直接查表還原原文。所以在用戶密碼存儲(chǔ)這個(gè)場(chǎng)景里MD5確實(shí)不建議再用了。正確做法是加鹽每個(gè)用戶獨(dú)立隨機(jī)鹽值之后再用bcrypt、scrypt、Argon2這類專門設(shè)計(jì)成“計(jì)算慢”的哈希算法。慢在這里反而是優(yōu)點(diǎn)因?yàn)楣粽吲扛F舉的成本會(huì)成倍上升。但如果你是做文件完整性校驗(yàn)、下載一致性確認(rèn)、內(nèi)容尋址存儲(chǔ)MD5依舊是性價(jià)比非常高的選擇。它的計(jì)算速度是所有主流哈希里最快的碰撞攻擊在這個(gè)場(chǎng)景里沒有實(shí)際威脅——誰(shuí)會(huì)費(fèi)勁造一個(gè)MD5相同的惡意安裝包來騙你成本遠(yuǎn)高于直接替換下載鏈接。我自己在嵌入式開發(fā)里也經(jīng)常用MD5給固件包做指紋每次量產(chǎn)前比對(duì)一次簡(jiǎn)單可靠。這里還是給個(gè)建議如果項(xiàng)目沒有歷史包袱新開發(fā)的完整性校驗(yàn)功能直接上SHA-256也行畢竟現(xiàn)代CPU計(jì)算SHA-256也很快。但如果只是為了應(yīng)急校驗(yàn)MD5完全夠用。6. 常見問題與排查技巧實(shí)錄我自己在不同環(huán)境里跑這個(gè)工具包括幫朋友在他們電腦上編譯踩了不少坑整理幾類典型的編譯報(bào)錯(cuò)undefined reference to main多半是編譯命令里漏了md5.c或者寫成了gcc -c md5.c只編譯不鏈接。完整命令應(yīng)該是gcc -O2 -o md5.exe md5.c。校驗(yàn)值和官方對(duì)不上優(yōu)先級(jí)排查下面幾項(xiàng)第一確認(rèn)官方頁(yè)面給的是MD5而不是SHA1/SHA256很多人把這三者搞混第二確認(rèn)下載過程沒被下載工具截?cái)嗫次募笮『凸俜绞欠褚恢碌谌_認(rèn)文件是不是文本格式Windows和Linux換行符差異也會(huì)導(dǎo)致二進(jìn)制內(nèi)容不同這時(shí)候需要比對(duì)二進(jìn)制模式第四確認(rèn)是不是看錯(cuò)了大小寫我的工具統(tǒng)一輸出小寫不影響比對(duì)結(jié)果。在Windows命令行里運(yùn)行報(bào)“不是內(nèi)部或外部命令”是因?yàn)楫?dāng)前目錄不在PATH里需要用.\md5.exe或者把exe放到系統(tǒng)PATH中的某個(gè)目錄。處理超大文件時(shí)有沒有內(nèi)存風(fēng)險(xiǎn)我的實(shí)現(xiàn)里不存在這個(gè)問題因?yàn)槭冀K只用一個(gè)8KB緩沖區(qū)。網(wǎng)上有些一次性把整個(gè)文件讀進(jìn)buffer的MD5工具源碼遇到十幾個(gè)GB的大文件會(huì)直接內(nèi)存爆掉這也是我堅(jiān)持自己實(shí)現(xiàn)一個(gè)流式版本的原因。集成到批處理腳本里希望校驗(yàn)失敗時(shí)自動(dòng)中斷我的程序遇到文件打不開時(shí)退出碼是2校驗(yàn)完成正常是0。你可以在批處理里這樣用md5.exe myfile.bin || exit /b 2這樣構(gòu)建腳本會(huì)在文件校驗(yàn)失敗時(shí)立刻停止不會(huì)帶著壞包繼續(xù)跑。還有一個(gè)小細(xì)節(jié)因?yàn)檩敵龈袷嚼铩癶ash 兩個(gè)空格 文件名”和md5sum對(duì)齊你在Linux上寫腳本解析也能直接套用現(xiàn)成的AWK邏輯跨平臺(tái)復(fù)用腳本很省事。個(gè)人的一點(diǎn)體會(huì)是寫這個(gè)MD5工具的收獲其實(shí)不在于“又造了一個(gè)輪子”而在于把這個(gè)高頻接觸的算法徹底弄明白了。以前用現(xiàn)成命令時(shí)MD5對(duì)我來說就是個(gè)黑盒填充、魔數(shù)、輪函數(shù)這些東西看了十遍也記不住。等到親手把md5.c一行行敲出來、編譯成exe跑通之后再看Linux內(nèi)核源碼里那些hash相關(guān)的實(shí)現(xiàn)理解速度明顯不一樣。如果你也想練手強(qiáng)烈建議按RFC 1321自己實(shí)現(xiàn)一遍別急著抄網(wǎng)上的代碼遇到對(duì)齊問題、長(zhǎng)度溢出問題、字節(jié)序問題自己排查一遍的效果遠(yuǎn)超看十篇教程。如果你需要擴(kuò)展這個(gè)工具下一步可以考慮加SHA-256支持、遞歸遍歷目錄、輸出JSON格式、右鍵菜單集成甚至套一層GUI殼。底子已經(jīng)打好了怎么玩都行。本文還有配套的精品資源點(diǎn)擊獲取