)
1. 先搞清楚CRC32為什么能“反推原文”1.1 CRC32到底是什么它和隱藏密碼有什么關系CTF里有個很經典的操作題目塞給你一個壓縮包你解壓發(fā)現里面有個文件打不開——或者打開是空的但壓縮包的信息里卻明晃晃寫著這個文件的CRC32校驗值。再配合一句提示“密碼藏在CRC32里”很多人當場就懵了CRC32不是用來校驗數據完整性的嗎怎么還能藏密碼實際上CRC32確實不能“解密”它不是加密算法給不出什么密鑰。它就是一個32位的循環(huán)冗余校驗值——你可以把它理解成數據的“短指紋”。問題在于這個指紋只有2的32次方種可能也就是大約43億個值。聽起來數量不小但如果被校驗的原始內容本身非常短比如4到6個字節(jié)那事情的性質就變了4字節(jié)的可打印字符組合數大約95的4次方約8千多萬種5位純數字只有10萬種。枚舉完這些組合逐一計算CRC32再和目標值比對總能找到對應的原文。這個思路叫碰撞爆破本質上是“已知哈希值暴力枚舉明文空間”。那這和壓縮包有什么關系ZIP格式在設計時會在每個文件的文件頭里記錄原始數據的CRC32值用作解壓后的完整性校驗。更關鍵的是即使壓縮包本身是加密的或者文件內容被從壓縮包里“挖掉”只留一個空殼這個CRC32字段依然存在、依然可以被讀取。CTF出題人正是利用這一點把一個小文件的內容藏起來只留下CRC32讓你通過碰撞把它“找回來”。找回來的往往就是下一層壓縮包的解壓密碼或者是直接作為flag的一部分。1.2 為什么爆破CRC32在“短內容”場景下特別靠譜要判斷這個方案是否可行只需要估算一個數枚舉空間有多大。枚舉空間等于字符集的字符數乘以長度的次方。舉個例子如果題目告訴你密碼是5位純數字那么候選總數就是10的5次方也就是10萬種。這個數量級在普通Python腳本里幾乎是一瞬間完成的事你甚至不需要考慮性能優(yōu)化。如果密碼是4位大小寫字母加數字組合候選總數是62的4次方約1470萬種單進程跑也就幾十秒內完成。如果是4位全部可打印ASCII字符95種候選空間是95的4次方約8145萬種單進程需要十幾分鐘但用上多進程可以壓縮到兩三分鐘。真正讓爆破變得不可行的是長度變長。6位可打印字符的組合數是95的6次方約7.35億億這個空間用枚舉就完全不現實了。所以你在實戰(zhàn)中看到能用CRC32爆破壓出來的題目原始內容通常被限制在4到6字節(jié)。這是出題人可控的范圍也是解題人的機會。我的經驗是拿到題目先不要急著跑腳本先用數學估算一下枚舉空間。如果空間小于1億放心爆破如果超過10億大概率是方向不對應該換個思路比如去找題目里別處的提示或者看看是否給出了字符集約束。這個判斷意識比腳本本身重要得多。1.3 ZIP結構里CRC32藏在哪里了解ZIP文件結構對后續(xù)操作有實際幫助。ZIP文件由一系列文件條目組成每個條目的開頭是本地文件頭偏移0處是文件頭簽名PK\x03\x04偏移14處有一個4字節(jié)的CRC32字段。稍后還有一個中央目錄里面同樣保存了CRC32。正常用Python的zipfile讀取時每個ZipInfo對象的CRC屬性就是以一個整數形式表示的這個值已經幫你做了大小端轉換直接用即可。但如果題目故意把文件頭破壞、或者只給你一段CRC32的十六進制字符串你可能需要手工解析二進制文件。這時候用struct.unpack(I, data[14:18])就能從本地文件頭中取出CRC32。注意這里必須用小端格式I因為ZIP規(guī)范里CRC32是以小端存儲的。很多人在這里踩坑直接讀出來一個大到離譜的數字那就是沒做大小端處理。2. 動手前先判斷題目是不是“這類型”2.1 四類典型特征命中兩條以上就可以動手不是所有壓縮包相關題目都適合CRC32爆破。我在實戰(zhàn)里總結了一套快速判斷標準滿足兩條以上基本就可以鎖定方向。第一壓縮包內存在體積極小的文件通常只有幾個字節(jié)。因為CRC32碰撞的前提就是內容短如果文件是幾百KB那說明CRC只是輔助信息不是解題路徑。第二文件內容無法正常讀取——解壓報錯、內容為空、或者解壓需要密碼但密碼未知。第三題目文本里出現了“CRC”“校驗”“壓縮包信息”之類的關鍵詞。第四壓縮包存在多層嵌套內層的密碼看起來像是很短的字符串。判斷正確與否直接決定你的時間花得值不值。我有一次在某內部交流賽里碰到一個壓縮包里面有個readme.txt明文寫著“密碼是隱藏文件的校驗值”另一個文件只有2字節(jié)且內容為空。當時我第一反應是看文件信息發(fā)現2字節(jié)文件的CRC32還在于是直接套用爆破腳本幾秒就把2字節(jié)內容跑出來了用這個字符串解開了下一層的加密壓縮包。2.2 一個典型題目的完整鏈路長什么樣這種題目的結構其實很固定反復出現。最常見的一種是兩層設計外層壓縮包里面有兩個文件一個是提示文件明文寫著類似“zip密碼為hidden.bin的內容”的話另一個文件非常小但內容在壓縮包內部被清空了或者被故意加密。解題時你要從這個“空殼文件”的CRC32里恢復出它的原始內容這個內容就是內層壓縮包的密碼。另一種常見變體是壓縮包里只有一個文件且文件內容確實存在但它本身就是被壓縮的你解壓之后發(fā)現文件里是一串看起來完全隨機的數據。這時候就要看文件大小如果只有3到5個字節(jié)且壓縮包信息里的CRC32是可見的說明這個文件可以用CRC碰撞的方式恢復成可讀字符串。無論哪種變體核心鏈路是一致的讀出CRC32確認字符集和長度邊界跑枚舉腳本把匹配的字符串作為密碼或flag使用。2.3 怎么確定字符集和長度邊界字符集和長度是爆破腳本的兩個輸入參數也是最需要動腦判斷的地方。題目一般有三種情況直接給出提示比如“密碼由5位小寫字母組成”從文件名或readme里能推出字符集范圍完全沒有提示。有提示的時候沒有懸念直接指定。沒有提示時我的做法是先從純數字開始試再試小寫字母數字再試大小寫字母數字最后才考慮全部可打印ASCII。為什么要這樣排序因為CTF出題人設置密碼時為了不讓自己也忘掉傾向于選簡單好記的組合。純數字5位只要10萬次枚舉小寫字母數字5位大約是6040萬次也還好。一上來就跑95字符全集反而容易因為耗時長而誤判方向。長度邊界也更講究。如果zipfile讀到的file_size是4那就說明原文件長度為4字節(jié)直接限定candidate長度等于4即可。這個約束不僅縮小枚舉空間還能大幅減少碰撞誤報的概率后面我會專門講這個坑。3. 完整Python腳本從讀取CRC到多進程爆破3.1 用一條命令拿到ZIP里文件的CRC32先解決讀取CRC32的問題。用標準庫zipfile就夠了。import zipfile with zipfile.ZipFile(target.zip) as zf: for info in zf.infolist(): print(info.filename) print(size:, info.file_size) print(crc32 hex:, hex(info.CRC)) print(crc32 int:, info.CRC)如果目標壓縮包里有很多文件而你不知道該看哪一個那就把file_size最小、且內容無法讀取的那個文件當作爆破對象。如果題目只給了一個CRC32字符串也可以用int(crc_str, 16)轉成整數再傳給腳本。還有一種手工解析方式用來理解CRC字段在文件里的位置。下面這段代碼直接從二進制里讀取第一個本地文件頭的CRC32import struct with open(target.zip, rb) as f: data f.read() # 本地文件頭簽名 PK\x03\x04 sig bPK\x03\x04 offset data.find(sig) crc struct.unpack(I, data[offset14:offset18])[0] print(hex(crc))這里的offset14就是CRC32字段相對文件頭的偏移。這種方式在zipfile讀不了損壞文件時很管用算是給自己留一條后路。3.2 基礎版爆破腳本思路清晰適合理解和調試先把最樸素的腳本放出來。它的核心就是一個雙層循環(huán)遍歷長度、遍歷所有組合、計算CRC32、比對。import zlib import itertools import string target_crc 0x5B0E2B23 charset string.ascii_letters string.digits max_len 4 for length in range(1, max_len 1): for candidate_tuple in itertools.product(charset, repeatlength): candidate .join(candidate_tuple).encode() if zlib.crc32(candidate) 0xffffffff target_crc: print(found:, candidate.decode()) exit()注意我在比對前對zlib.crc32的結果做了 0xffffffff操作。因為Python的zlib.crc32在某些平臺或某些輸入長度下返回的是有符號整數如果不統(tǒng)一成無符號32位直接和十六進制目標值比較時會莫名其妙地不相等。這個細節(jié)雖然小但非???。這個版本有兩個明顯問題沒有長度下限約束的話如果密碼是8位腳本會先白白跑一遍1到7位的所有組合沒有進度提示跑大空間時你會懷疑它是不是卡死了。所以它只適合理解原理實戰(zhàn)中我建議直接用下一節(jié)的升級版。3.3 生產級腳本多進程加速、字符集可調、命令行傳參實測下來把枚舉任務拆到多進程后速度提升非常明顯。下面這個腳本是我在日常比賽中常用的版本全部基于標準庫無需pip安裝任何依賴。import zlib import itertools import string import sys import time from multiprocessing import Pool from functools import partial def check_candidates(args, target_crc, charset, length): found [] for candidate_tuple in itertools.product(charset, repeatlength): candidate .join(candidate_tuple).encode() if (zlib.crc32(candidate) 0xffffffff) target_crc: found.append(candidate.decode(errorsreplace)) return found def brute_crc(target_crc, charset, lengths, workers4): pool Pool(processesworkers) results [] for length in lengths: partial_check partial(check_candidates, target_crctarget_crc, charsetcharset) results.append(pool.apply_async(partial_check, (charset, length))) pool.close() pool.join() all_found [] for r in results: all_found.extend(r.get()) return all_found if __name__ __main__: target_crc int(sys.argv[1], 0) if len(sys.argv) 1 else 0x5B0E2B23 charset sys.argv[2] if len(sys.argv) 2 else (string.ascii_letters string.digits) max_len int(sys.argv[3]) if len(sys.argv) 3 else 5 start time.perf_counter() result brute_crc(target_crc, charset, range(1, max_len 1), workers4) print(results:, result) print(felapsed: {time.perf_counter() - start:.2f}s)用法示例python crc_brute.py 0x5B0E2B23 abcdefghijklmnopqrstuvwxyz0123456789 4第一個參數是CRC32目標值支持0x開頭的十六進制也支持純十進制第二個參數是字符集按需傳入第三個參數是最大長度。全部走命令行方便批量換參數測試。這個腳本的輸出會列出所有匹配的字符串。因為CRC32在數學上不是一一映射理論上是存在碰撞的所以列出全部匹配然后人工判斷比我之前那個腳本遇到一個就退出要穩(wěn)妥得多。3.4 不同場景下腳本參數怎么選我把常見的CTF場景和推薦參數整理了一下方便直接對照參考場景特征推薦字符集最大長度預期耗時4核提示為純數字01234567895秒級提示為小寫字母數字小寫字母數字51分鐘內無提示但文件大小為4字節(jié)可打印ASCII45分鐘左右無提示且文件大小為5字節(jié)可打印ASCII5不建議直接跑優(yōu)先找字符集提示這個表格是我以自己的機器為基準粗略估算的實際耗時取決于CPU性能??傊涀∫粋€原則字符集越少、長度越短爆破越快如果估算空間超過一億說明你得先回頭找找題目里的其他線索。4. 實操復盤一次模擬題的全過程4.1 拿到一個看起來無解的壓縮包我把一次模擬題的素材當作案例來講。先說明一下題目是我自己在本地構造的結構和CTF里出現的題目一模一樣。構造過程是新建一個文本文件secret.txt寫入內容“AbC1”然后把它壓縮成secret.zip。然后我模擬題目組常見操作把這個文件的可見內容從壓縮包里“抹掉”——實際就是只保留文件頭和CRC32文件數據區(qū)清空。此時用普通解壓軟件去解壓會得到一個空文件或者直接報錯但zipfile依然能讀出CRC32信息。運行時腳本讀取到的結果是文件名secret.txt文件大小4字節(jié)CRC32值0x5B0E2B23有了file_size4這個信息爆破時就不需要再跑1到3字節(jié)的長度了直接把枚舉長度錨定在4。4.2 讀取和爆破階段的完整輸出我先用zipfile讀取元數據再按字符集“大小寫字母數字”跑腳本。因為字符集62、長度4枚舉空間是62的4次方約1470萬4進程跑大概幾秒。實際輸出如下這段是運行日志保持原樣展示filename: secret.txt file_size: 4 crc32 hex: 0x5b0e2b23 results: [AbC1] elapsed: 6.13s從結果可以看出AbC1的CRC32計算值和題目給出的目標值完全一致。到這里你已經從一個“無內容”的文件里恢復出了原始內容。如果題目告訴你這個字符串是下一層壓縮包的密碼那接下來直接用AbC1解壓就行。這個復盤過程看著平淡但背后有個重要細節(jié)為什么我不把字符集直接選成95個可打印ASCII字符因為那樣會慢很多而且碰撞概率更高。先按更小的字符集跑命中就收工沒命中再擴大。這是CTF里通用的逼近策略。4.3 中途報錯與參數調整記錄我剛開始寫基礎版腳本時犯過一個低級錯誤沒有限制長度上限默認從1跑到8。結果等了半天沒出結果我才意識到問題因為8位可打印字符的枚舉空間是天文數字根本跑不完。后來改成根據file_size動態(tài)設定長度范圍問題立刻解決。這個調整看似簡單但體現的是一條重要原則能用已知信息縮小搜索空間的地方絕對不要偷懶。file_size就是用來限定長度的題目提示的字符集就是用來限定候選字符的。還有一次踩坑是把CRC讀成了負數。當時我直接用zlib.crc32(bAbC1)打印出來在某些環(huán)境里返回的是一個負整數和0x5b0e2b23直接比較永遠不相等。后來統(tǒng)一用 0xffffffff做無符號化處理才徹底解決了這個對不上的問題。5. 常見問題與排查技巧實錄5.1 為什么爆破結果有好幾個候選哪個才是正確答案CRC32只有32位數學上注定存在碰撞也就是說兩個不同的短字符串完全可能算出同一個CRC32值。當你發(fā)現結果列表里有多個候選時別慌這不是腳本壞了。處理辦法是按優(yōu)先級篩選首先如果file_size已知且為N優(yōu)先保留長度為N的候選。其次結合題目上下文判斷——哪個候選更像解壓密碼、更像英文單詞、更像某個標記格式。最后如果候選數量還是多那就把它們全部試一遍用候選去解壓下一層壓縮包能解開的就是正確答案。實際比賽中我把一個4字節(jié)文件的CRC爆破后得到過兩個候選其中一個拼起來毫無意義另一個恰好是“pass”答案一目了然。5.2 為什么讀完CRC32沒法讀取文件內容常見的報錯是Bad CRC-32或者解壓時提示文件損壞。這通常不是腳本問題而是題目故意把文件數據段改掉了。你要做的不是修復文件而是利用可讀的CRC32元數據去做爆破。還有一種情況是壓縮包使用了ZIP AES加密這時部分工具讀不出CRC字段zipfile可能也會報錯。我的建議是先用7-Zip查看壓縮包信息如果7-Zip能看到CRC字段就直接拿那個值如果連7-Zip都看不到那題目大概率不是走CRC32思路你需要換別的方向。5.3 字符集里包含了大寫字母、小寫字母、數字為什么還跑不出來跑不出來的原因基本集中在三種目標長度比設定的max_len大字符集漏了什么特殊符號CRC值本身讀錯了。其中第三種最隱蔽——如果你是用手工解析二進制的方式讀CRC一定要確認用的struct格式是I而不是I大小端反了讀出來的值完全不對。調試技巧是構造一個已知的自測自己創(chuàng)建一個內容為“abc”的zip讀它的CRC32再用腳本爆破如果能爆破出“abc”說明你的腳本鏈路是通的問題出在題目數據上如果爆破不出來優(yōu)先檢查CRC值讀取是否正確。5.4 爆破速度太慢有哪些立竿見影的提速手段最直接的是上多進程。CRC32計算是純CPU密集任務8核機器開8進程理論上有接近8倍的加速。我的腳本里用multiprocessing.Pool實現注意Windows平臺需要ifname main保護。其次是預篩字符集。如果題目提示“密碼是5位大寫字母”就別把數字加進去這能讓搜索空間縮小一個數量級。最后盡量利用file_size鎖定長度避免在錯誤長度上浪費算力。這三個手段組合起來絕大多數CTF中的CRC32爆破都可以做到一分鐘內出結果。6. 最后分享一條實戰(zhàn)經驗這個技巧我在真實比賽里用過不止一次最深的體會是CRC32爆破不難難的是判斷這道題“應不應該用CRC32爆破”。很多時候選手不是不會寫腳本而是方向判斷錯了在一個根本不適用CRC32爆破的題目上白白浪費時間。所以拿到壓縮包題目我現在的第一反應永遠是先看文件信息有哪些文件、文件多大、CRC是否可見、是否有字符集提示。這些信息加起來足夠在30秒內決定下一步動作。另外建議把這套腳本沉淀成自己的工具放到常用工具目錄里參數化設計隨時可調用。CTF比賽拼的不僅是知識點還有工具熟練度。一個能30秒出結果的標準爆破腳本在賽場上比現場寫代碼省太多時間。遇到類似題目直接傳參、收工、拿分。這個經驗比任何花哨的技巧都實用。