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