戰(zhàn):二進(jìn)制文件逆向與批量腳本指南)
簡(jiǎn)介010 Editor 是一款面向程序員、逆向工程師與系統(tǒng)管理員的專業(yè)十六進(jìn)制編輯器可處理文本、XML、HTML、Unicode/UTF-8 編碼文件及 C/C、PHP 等源碼解決二進(jìn)制數(shù)據(jù)分析與大規(guī)模文本編輯中的效率問(wèn)題。資源包共 35 個(gè)文件以 16 個(gè) dll 動(dòng)態(tài)庫(kù)、2 個(gè) exe 主程序、2 個(gè) zip 及 qhc/qch 幫助文檔、conf/xml/dat 配置數(shù)據(jù)等為主整體約 19.64MB屬于綠色便攜版本解壓即可使用。其核心能力包括無(wú)限撤銷、支持正則表達(dá)式的搜索替換、多行批量編輯、列模式矩形區(qū)域編輯以及對(duì)超過(guò) 50GB 大文件的穩(wěn)定處理內(nèi)置 SCL 腳本語(yǔ)言并可通過(guò)插件擴(kuò)展 Python、Perl配合二進(jìn)制模板系統(tǒng)可直觀解析磁盤映像、內(nèi)存轉(zhuǎn)儲(chǔ)與網(wǎng)絡(luò)流量捕獲等復(fù)雜結(jié)構(gòu)。目前已有 733 人學(xué)習(xí)下載適合需要深入分析二進(jìn)制數(shù)據(jù)、開(kāi)展逆向工程或軟件調(diào)試的讀者參考使用。1. 010 Editor 到底解決什么問(wèn)題從一次固件頭解析翻車說(shuō)起手里拿到一個(gè) 2MB 的固件 bin 文件用普通文本編輯器打開(kāi)全是亂碼用 HxD 十六進(jìn)制編輯器能看到字節(jié)卻沒(méi)法批量改結(jié)構(gòu)這時(shí)候 010 Editor 就是那個(gè)能救場(chǎng)的工具。它不是簡(jiǎn)單的十六進(jìn)制查看器核心能力在于「模板驅(qū)動(dòng)解析」——你可以寫一個(gè) Binary Template把二進(jìn)制文件按結(jié)構(gòu)體字段一層層展開(kāi)頭部、長(zhǎng)度、校驗(yàn)、偏移全部變成可讀的樹(shù)形結(jié)構(gòu)。做嵌入式、逆向、文件格式分析、協(xié)議逆向的人基本都會(huì)在某個(gè)階段撞上它。我第一次用它是因?yàn)橐粋€(gè) OTA 升級(jí)包校驗(yàn)總失敗用 HxD 十六進(jìn)制編輯器逐字節(jié)比對(duì)了兩小時(shí)沒(méi)找到問(wèn)題換成 010 Editor 加載模板后三分鐘定位到是頭部 magic 字段被多寫了一個(gè)字節(jié)。這篇就把 010 Editor 從安裝、模板編寫、批量腳本到避坑按我實(shí)際干活的順序講清楚讓你拿到一個(gè)二進(jìn)制文件時(shí)知道從哪下手。2. 010 Editor 的模板機(jī)制與最小可復(fù)現(xiàn)解析流程2.1 為什么不是 HxD 十六進(jìn)制編輯器而是模板解析HxD 十六進(jìn)制編輯器在「看字節(jié)」這件事上做得很好啟動(dòng)快、界面干凈、支持基本編輯和校驗(yàn)和計(jì)算。但它的定位是通用十六進(jìn)制查看沒(méi)有結(jié)構(gòu)感知能力。你打開(kāi)一個(gè) PNG它不會(huì)告訴你哪四個(gè)字節(jié)是寬度、哪四個(gè)字節(jié)是高度你打開(kāi)一個(gè)自定義協(xié)議包它不會(huì)把字段名標(biāo)出來(lái)。010 Editor 的差異就在 Binary Template模板本質(zhì)是一段類 C 語(yǔ)法的腳本聲明字段類型和順序運(yùn)行時(shí)按聲明順序從文件當(dāng)前偏移讀取字節(jié)并映射成變量。模板跑完左側(cè)是文件十六進(jìn)制右側(cè)是解析出來(lái)的結(jié)構(gòu)樹(shù)點(diǎn)任意字段能雙向定位到字節(jié)位置。這個(gè)機(jī)制帶來(lái)的直接好處有三個(gè)。第一格式文檔和實(shí)際文件能對(duì)齊驗(yàn)證文檔說(shuō)偏移 0x10 是版本號(hào)模板里寫uint version;放在對(duì)應(yīng)位置跑一下就知道對(duì)不對(duì)。第二批量文件可以用同一模板跑腳本里循環(huán)打開(kāi)文件、應(yīng)用模板、導(dǎo)出字段值比手工比對(duì)快幾個(gè)數(shù)量級(jí)。第三模板可以帶條件分支和循環(huán)處理變長(zhǎng)結(jié)構(gòu)、TLV 格式、數(shù)組字段都不在話下。常見(jiàn)做法是先拿一個(gè)已知正確的樣本文件對(duì)著格式文檔寫模板跑通后再拿異常樣本對(duì)比差異字段就是問(wèn)題所在。選型上如果你只是偶爾看一眼字節(jié)HxD 十六進(jìn)制編輯器足夠如果你要反復(fù)解析同一類格式、要寫自動(dòng)化校驗(yàn)、要把解析結(jié)果導(dǎo)出成 CSV 做分析010 Editor 的模板加腳本組合是更省時(shí)間的路徑。它的學(xué)習(xí)曲線主要卡在模板語(yǔ)法和腳本 API 上但一旦寫過(guò)兩三個(gè)模板后面就是復(fù)制改字段的事。2.2 寫第一個(gè) Binary Template解析一個(gè)自定義文件頭假設(shè)有一個(gè)自定義二進(jìn)制格式頭部結(jié)構(gòu)如下magic 4 字節(jié)固定為0x4D 0x59 0x46 0x4Dversion 2 字節(jié)小端flags 1 字節(jié)reserved 1 字節(jié)dataSize 4 字節(jié)小端然后緊跟 dataSize 字節(jié)的數(shù)據(jù)區(qū)。用 010 Editor 模板寫出來(lái)是這樣// MyFormat.bt // 自定義文件頭解析模板 struct FILE_HEADER { char magic[4]; // 固定 4D 59 46 4D ushort version; // 小端版本號(hào) uchar flags; // 標(biāo)志位 uchar reserved; // 保留 uint dataSize; // 數(shù)據(jù)區(qū)長(zhǎng)度小端 }; FILE_HEADER header; // 從偏移 0 開(kāi)始讀 // 根據(jù) dataSize 讀取數(shù)據(jù)區(qū) uchar data[header.dataSize]; // 打印關(guān)鍵字段到輸出窗口 Printf(magic%s\n, header.magic); Printf(version%d\n, header.version); Printf(dataSize%d\n, header.dataSize);這段模板的邏輯很直接struct定義字段順序和類型010 Editor 按聲明順序從文件當(dāng)前偏移依次讀取。char magic[4]讀 4 字節(jié)ushort version讀 2 字節(jié)并按小端解釋010 Editor 默認(rèn)小端大端需顯式聲明BigEndianuint dataSize讀 4 字節(jié)。最后uchar data[header.dataSize]用前面讀到的長(zhǎng)度動(dòng)態(tài)聲明數(shù)組這是模板比固定結(jié)構(gòu)強(qiáng)的地方——變長(zhǎng)字段能跟著實(shí)際值走。參數(shù)說(shuō)明幾個(gè)關(guān)鍵點(diǎn)。類型映射char1 字節(jié)、uchar1 字節(jié)無(wú)符號(hào)、ushort2 字節(jié)、uint4 字節(jié)、int648 字節(jié)。字節(jié)序默認(rèn)小端需要大端時(shí)在 struct 前加BigEndian;或在字段前單獨(dú)聲明。Printf是模板內(nèi)置輸出函數(shù)結(jié)果出現(xiàn)在下方輸出面板方便快速驗(yàn)證。如果 magic 讀出來(lái)不是預(yù)期值先檢查文件是否從偏移 0 開(kāi)始、有沒(méi)有文件頭前置了別的數(shù)據(jù)。跑模板的步驟010 Editor 打開(kāi)目標(biāo)文件 → 菜單 Templates → Run Template → 選MyFormat.bt→ 看右側(cè)結(jié)構(gòu)樹(shù)和輸出面板。如果模板報(bào)錯(cuò)常見(jiàn)原因是字段類型和實(shí)際字節(jié)數(shù)不匹配比如把 4 字節(jié)字段寫成ushort后面所有字段偏移都會(huì)錯(cuò)位結(jié)構(gòu)樹(shù)里字段值會(huì)明顯不對(duì)。這時(shí)候用十六進(jìn)制視圖對(duì)照文檔逐個(gè)字段核對(duì)偏移比盲改模板快。2.3 用腳本批量跑模板并導(dǎo)出字段單個(gè)文件解析通了之后下一步是批量。010 Editor 內(nèi)置腳本引擎語(yǔ)法類似 C可以循環(huán)打開(kāi)目錄下所有文件、應(yīng)用模板、讀取字段值、寫入 CSV。下面這段腳本遍歷指定目錄的.bin文件對(duì)每個(gè)文件應(yīng)用上面的模板把 magic、version、dataSize 導(dǎo)出到 CSV// batch_parse.bs // 批量解析 bin 文件并導(dǎo)出字段 string dir C:\\samples\\; string outFile C:\\samples\\result.csv; FileHandle fh FileOpen(outFile, w); FileWrite(fh, filename,magic,version,dataSize\n); FindFiles(dir, *.bin); int count GetNumFiles(); for (int i 0; i count; i) { string path GetFileName(i); FileOpen(path); RunTemplate(C:\\templates\\MyFormat.bt); // 從模板變量讀取字段值 string magic ReadString(0, 4); ushort ver ReadUShort(4); uint size ReadUInt(8); FileWrite(fh, FileName(path) , magic , ver , size \n); FileClose(); } FileClose(fh); Printf(done, %d files\n, count);邏輯說(shuō)明FindFiles按通配符收集文件列表GetNumFiles拿數(shù)量循環(huán)里FileOpen打開(kāi)每個(gè)文件RunTemplate應(yīng)用模板然后用ReadString、ReadUShort、ReadUInt按偏移直接讀值。這里沒(méi)有依賴模板變量導(dǎo)出而是用偏移讀取好處是腳本獨(dú)立性強(qiáng)模板改了字段名也不影響。FileWrite逐行寫 CSV最后關(guān)閉句柄。參數(shù)注意ReadUShort默認(rèn)小端大端用ReadUShortBE。偏移從 0 開(kāi)始算如果文件有前置頭需要加偏移量。RunTemplate的路徑用絕對(duì)路徑相對(duì)路徑容易找不到。批量跑之前先用兩三個(gè)文件驗(yàn)證 CSV 輸出對(duì)不對(duì)確認(rèn)字段值跟手工看的一致再全量跑。如果某個(gè)文件解析報(bào)錯(cuò)腳本會(huì)中斷可以在循環(huán)里加if (!RunTemplate(...)) { continue; }跳過(guò)異常文件但更穩(wěn)妥的做法是先單獨(dú)看那個(gè)文件為什么不符合模板預(yù)期。3. 模板進(jìn)階變長(zhǎng)結(jié)構(gòu)、條件分支與校驗(yàn)字段處理3.1 處理 TLV 和變長(zhǎng)數(shù)組的模板寫法真實(shí)格式很少是固定長(zhǎng)度。TLVType-Length-Value結(jié)構(gòu)里每個(gè)字段先讀 Type 和 Length再按 Length 讀 Value循環(huán)直到文件結(jié)束。010 Editor 模板支持while循環(huán)和局部變量寫法如下// TLV.bt // 解析 TLV 流 struct TLV_ENTRY { uchar type; ushort length; uchar value[length]; }; while (!FEof()) { TLV_ENTRY entry; Printf(type0x%02X len%d\n, entry.type, entry.length); }FEof()判斷是否到文件末尾循環(huán)里每次聲明一個(gè)TLV_ENTRY模板引擎自動(dòng)按結(jié)構(gòu)體大小推進(jìn)偏移。uchar value[length]用前面讀到的length動(dòng)態(tài)定長(zhǎng)這是模板里處理變長(zhǎng)字段的標(biāo)準(zhǔn)手法。注意length字段本身占 2 字節(jié)讀完后當(dāng)前偏移已經(jīng)跳過(guò)它value從正確位置開(kāi)始讀。如果 TLV 的 Length 包含頭部自身長(zhǎng)度那value數(shù)組長(zhǎng)度要寫成length - 31 字節(jié) type 2 字節(jié) length。這種細(xì)節(jié)必須對(duì)著格式文檔確認(rèn)寫錯(cuò)了后面所有 entry 都會(huì)錯(cuò)位。調(diào)試方法在循環(huán)里加Printf打印當(dāng)前偏移FTell()跟手工計(jì)算的偏移對(duì)比很快能定位是哪個(gè)字段長(zhǎng)度算錯(cuò)了。3.2 條件字段與聯(lián)合體的處理有些格式根據(jù)前面字段的值決定后面讀什么。比如一個(gè)消息包msgType為 1 時(shí)后面跟 4 字節(jié)整數(shù)為 2 時(shí)跟 8 字節(jié)字符串。模板里用if-else處理// conditional.bt uchar msgType; if (msgType 1) { uint intValue; Printf(int%d\n, intValue); } else if (msgType 2) { char strValue[8]; Printf(str%s\n, strValue); } else { Printf(unknown type %d\n, msgType); }條件分支在模板里按實(shí)際讀到的值走不會(huì)預(yù)讀。這意味著如果msgType本身讀錯(cuò)了后面分支全錯(cuò)。所以模板調(diào)試順序永遠(yuǎn)是先確認(rèn)第一個(gè)字段再確認(rèn)第二個(gè)逐字段往下推。010 Editor 的結(jié)構(gòu)樹(shù)會(huì)顯示每個(gè)字段的偏移和值對(duì)照文檔逐個(gè)核對(duì)是最快的排查方式。聯(lián)合體union場(chǎng)景比如同一個(gè)偏移根據(jù)上下文解釋為不同字段模板里可以用union關(guān)鍵字但更常見(jiàn)的做法是用條件分支分別聲明因?yàn)?union 在模板里對(duì)偏移推進(jìn)的處理容易讓人困惑。我一般用 if-else 顯式寫可讀性更好調(diào)試也直觀。3.3 校驗(yàn)和字段的驗(yàn)證與計(jì)算二進(jìn)制格式里校驗(yàn)和checksum、CRC字段很常見(jiàn)。模板里可以先讀校驗(yàn)值再對(duì)數(shù)據(jù)區(qū)計(jì)算對(duì)比是否一致。010 Editor 內(nèi)置了Checksum函數(shù)支持 CRC32、CRC16、Sum 等算法// checksum.bt uint storedCrc; // 文件中存儲(chǔ)的 CRC // 假設(shè)數(shù)據(jù)區(qū)從偏移 8 開(kāi)始長(zhǎng)度 100 uint calcCrc Checksum(CRC32, 8, 100); if (storedCrc calcCrc) { Printf(CRC OK\n); } else { Printf(CRC MISMATCH: stored0x%08X calc0x%08X\n, storedCrc, calcCrc); }Checksum第一個(gè)參數(shù)是算法類型第二個(gè)是起始偏移第三個(gè)是長(zhǎng)度。算法類型要和格式文檔一致CRC32 有多種變體多項(xiàng)式、初值、異或值不同010 Editor 內(nèi)置的 CRC32 是標(biāo)準(zhǔn)多項(xiàng)式 0xEDB88320如果格式用的是別的變體結(jié)果會(huì)對(duì)不上。這時(shí)候要么用腳本自己實(shí)現(xiàn)要么用Checksum的其他變體參數(shù)。校驗(yàn)不通過(guò)時(shí)先確認(rèn)計(jì)算范圍對(duì)不對(duì)——很多格式的 CRC 不包含 CRC 字段本身起始偏移和長(zhǎng)度要排除那 4 字節(jié)。4. 避坑與排查010 Editor 用起來(lái)最容易翻車的五個(gè)地方4.1 模板字段偏移錯(cuò)位后面全亂現(xiàn)象結(jié)構(gòu)樹(shù)里第一個(gè)字段值就不對(duì)或者前幾個(gè)對(duì)、后面突然變成亂碼。原因某個(gè)字段類型大小和實(shí)際字節(jié)數(shù)不匹配比如文檔寫 2 字節(jié)但模板用了uint4 字節(jié)導(dǎo)致后續(xù)所有字段偏移整體后移。解決用十六進(jìn)制視圖對(duì)照文檔從偏移 0 開(kāi)始逐個(gè)字段核對(duì)字節(jié)數(shù)和值找到第一個(gè)不匹配的字段改類型。更穩(wěn)妥的做法是模板里每個(gè)字段后加Printf(offset%d\n, FTell());跑一遍看偏移推進(jìn)是否符合預(yù)期。4.2 字節(jié)序搞反數(shù)值全錯(cuò)但偏移沒(méi)錯(cuò)現(xiàn)象字段偏移都對(duì)但讀出來(lái)的數(shù)值跟預(yù)期差很遠(yuǎn)比如 0x0100 讀成 256 而不是 1。原因格式是大端但模板默認(rèn)小端。解決在 struct 開(kāi)頭加BigEndian;或?qū)蝹€(gè)字段用ReadUShortBE等大端讀取函數(shù)。判斷方法如果數(shù)值恰好是字節(jié)交換后的結(jié)果基本就是字節(jié)序問(wèn)題。嵌入式格式里大端很常見(jiàn)網(wǎng)絡(luò)協(xié)議默認(rèn)大端寫模板前先確認(rèn)格式文檔的字節(jié)序說(shuō)明。4.3 變長(zhǎng)數(shù)組長(zhǎng)度字段包含自身導(dǎo)致越界現(xiàn)象模板跑完結(jié)構(gòu)樹(shù)里最后一個(gè)數(shù)組長(zhǎng)度異常大或者直接報(bào)錯(cuò)。原因Length 字段的值包含了 Length 字段自身的字節(jié)數(shù)但模板里value[length]按純數(shù)據(jù)長(zhǎng)度讀多讀了幾個(gè)字節(jié)。解決確認(rèn) Length 的定義范圍如果包含頭部數(shù)組長(zhǎng)度寫成length - headerSize。這個(gè)坑在 TLV 格式里特別常見(jiàn)不同協(xié)議的 Length 定義不一樣必須對(duì)著文檔確認(rèn)。4.4 批量腳本里文件句柄沒(méi)關(guān)導(dǎo)致后續(xù)文件打開(kāi)失敗現(xiàn)象批量跑幾十個(gè)文件后腳本報(bào)錯(cuò)提示無(wú)法打開(kāi)文件。原因循環(huán)里FileOpen后沒(méi)有對(duì)應(yīng)的FileClose句柄耗盡。解決每次循環(huán)結(jié)束前確保FileClose()被調(diào)用包括異常分支。更穩(wěn)的寫法是用try-catch包住解析邏輯catch里也關(guān)句柄。010 Editor 腳本的句柄管理不像高級(jí)語(yǔ)言那么自動(dòng)手動(dòng)關(guān)是必須的。4.5 模板路徑含中文或空格導(dǎo)致 RunTemplate 失敗現(xiàn)象手工跑模板正常腳本里RunTemplate報(bào)找不到文件。原因路徑里有中文或空格腳本引擎對(duì)路徑轉(zhuǎn)義處理不完善。解決模板和樣本文件放在純英文無(wú)空格路徑下比如C:\work\bt\。如果必須用中文路徑試試雙反斜杠轉(zhuǎn)義或正斜杠但最省事的還是換路徑。這個(gè)坑不常遇到但遇到一次能查半天。5. 用模板做格式逆向從已知樣本反推字段邊界5.1 差異對(duì)比法定位字段拿到一個(gè)沒(méi)有文檔的二進(jìn)制格式最實(shí)用的方法是找多個(gè)樣本做差異對(duì)比。010 Editor 自帶文件對(duì)比功能Tools → Compare Files能高亮兩個(gè)文件的字節(jié)差異。操作步驟打開(kāi)兩個(gè)同類型文件 → Tools → Compare Files → 選第二個(gè)文件 → 看差異區(qū)域。如果兩個(gè)文件只有某個(gè)配置項(xiàng)不同差異字節(jié)就是那個(gè)字段的位置。再結(jié)合值的變化范圍推斷字段類型1 字節(jié)變化的是 uchar2 字節(jié)的是 ushort4 字節(jié)的是 uint。我一般會(huì)準(zhǔn)備三到五個(gè)樣本兩兩對(duì)比把差異偏移記下來(lái)然后在模板里按這些偏移聲明字段跑通后再用更多樣本驗(yàn)證。這個(gè)方法對(duì)固件、配置文件、存檔格式都有效比盲猜快得多。5.2 用腳本自動(dòng)掃描候選字段樣本多了之后手工對(duì)比效率低可以寫腳本自動(dòng)掃描。思路是讀入多個(gè)文件逐字節(jié)比較把值有變化的偏移和變化范圍輸出。下面這段腳本輸出所有文件在同一偏移上值不同的位置// diff_scan.bs string dir C:\\samples\\; FindFiles(dir, *.bin); int n GetNumFiles(); if (n 2) { Printf(need at least 2 files\n); return; } // 以第一個(gè)文件為基準(zhǔn) FileOpen(GetFileName(0)); int fileSize GetFileSize(); uchar base[fileSize]; ReadBytes(base, 0, fileSize); FileClose(); for (int i 1; i n; i) { FileOpen(GetFileName(i)); uchar cur[fileSize]; ReadBytes(cur, 0, fileSize); FileClose(); for (int j 0; j fileSize; j) { if (base[j] ! cur[j]) { Printf(offset 0x%04X: 0x%02X - 0x%02X (file %d)\n, j, base[j], cur[j], i); } } }邏輯先讀第一個(gè)文件全部字節(jié)作基準(zhǔn)然后逐個(gè)文件讀入并逐字節(jié)比較不同就打印偏移和值。ReadBytes把指定范圍讀進(jìn)數(shù)組GetFileSize拿文件大小。輸出結(jié)果里連續(xù)變化的偏移段就是候選字段區(qū)域再結(jié)合值的變化規(guī)律推斷類型和含義。參數(shù)注意文件大小不一致時(shí)以第一個(gè)為準(zhǔn)超出部分不比較。如果文件很大幾十 MB逐字節(jié)比較會(huì)慢可以按 4 字節(jié)對(duì)齊跳著比先定位大致區(qū)域再細(xì)看。這個(gè)腳本輸出的是原始差異字段語(yǔ)義還得結(jié)合格式知識(shí)判斷但它能把需要人工看的范圍從整個(gè)文件縮小到幾個(gè)偏移段。5.3 模板迭代從能跑到能復(fù)用第一版模板通常只求跑通字段名可能是field1、field2。跑通之后要做的是重命名、加注釋、補(bǔ)條件分支讓它能復(fù)用到同類文件上。我的習(xí)慣是模板里每個(gè)字段后面跟一行注釋寫清楚含義和取值范圍比如uchar flags; // bit0: 加密, bit1: 壓縮。這樣過(guò)兩周再看還能看懂。模板文件按格式名命名放在統(tǒng)一目錄腳本里引用絕對(duì)路徑換機(jī)器時(shí)改一個(gè)路徑變量就行。驗(yàn)證模板是否可靠的方法拿一個(gè)已知答案的文件跑字段值全對(duì)再拿一個(gè)異常文件跑能定位到異常字段。兩個(gè)都過(guò)模板基本可用。如果只對(duì)了一個(gè)樣本換個(gè)文件就錯(cuò)位說(shuō)明模板里有硬編碼偏移或長(zhǎng)度假設(shè)需要改成動(dòng)態(tài)計(jì)算。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取