)
在 Linux 下跟文件打交道多了你就會發(fā)現(xiàn)一個顛撲不破的真相擴(kuò)展名只是給人類看的備注不是文件真實身份的憑證。從網(wǎng)上下載的“教程.txt”可能是個壓縮包從設(shè)備導(dǎo)出的“firmware.bin”也許是 TAR 歸檔更別說各種腳本、日志和圖片混在一個目錄里根本看不出誰是誰。這時候file文件類型檢測就是我最常用也最順手的第一道識別工具。它不看你文件名而是直接讀文件內(nèi)容里的字節(jié)特征再跟系統(tǒng)內(nèi)置的規(guī)則庫比對快速告訴你這個文件到底是什么。file命令最大的價值是“快”和“準(zhǔn)”。它不需要你把文件打開不需要裝什么重型軟件也不需要知道文件之前叫什么名字一條命令下去幾毫秒內(nèi)就能給出結(jié)論。不管你是運(yùn)維排查日志目錄、逆向分析固件、清理磁盤垃圾還是做安全樣本分類都會反復(fù)用到它。這篇文章我會從原理講到實戰(zhàn)把file命令的常用參數(shù)、底層機(jī)制、腳本集成和常見坑一次性說透。1. 為什么需要 file 命令1.1 擴(kuò)展名不可靠改個后綴騙不了 file很多人剛接觸 Linux 時會有一個慣性思維文件類型看后綴就行。.txt是文本.jpg是圖片.sh是腳本。但真實場景里擴(kuò)展名是最不值錢的信息因為隨便一個人都能用mv把文件后綴改掉很多下載工具、傳輸協(xié)議也會把文件名寫成無意義的哈希串。我自己就撞到過這么一件事某臺服務(wù)器上的日志目錄里躺著一堆沒后綴的文件是某采集程序定期打包上傳的文件名全是時間戳加隨機(jī)數(shù)字。開發(fā)同事說這些是文本日志我第一反應(yīng)就是拿file驗一下結(jié)果里面一半是 gzip 壓縮數(shù)據(jù)一半是 SQLite 數(shù)據(jù)庫真正純文本的反而沒幾個。要是當(dāng)時直接按.log后綴去處理后面全都得返工。換一個更簡單的例子$ mv report.pdf report.txt $ file report.txt report.txt: PDF document, version 1.4看到?jīng)]有后綴從.pdf改成.txtfile依然能通過文件頭識別出這是 PDF。這也是我經(jīng)常跟新人強(qiáng)調(diào)的不要相信文件名要相信文件內(nèi)容。1.2 文件指紋魔數(shù)Magic Numberfile憑什么能做到這一點靠的是文件內(nèi)容里的“指紋”也就是常說的魔數(shù)Magic Number。程序文件、圖片、壓縮包、可執(zhí)行文件在設(shè)計格式時都會在文件開頭或者某個固定偏移位置寫入一段固定的字節(jié)序列用來標(biāo)記自己的格式。file就是靠匹配這些字節(jié)序列來判型。舉幾個最典型的文件類型十六進(jìn)制特征人類可讀標(biāo)記PDF25 50 44 46%PDFJPEG 圖片F(xiàn)F D8 FF無二進(jìn)制特征PNG 圖片89 50 4E 47 0D 0A 1A 0A以不可見控制字符起始ELF 可執(zhí)行文件7F 45 4C 46\x7fELFZIP 壓縮包50 4B 03 04PK\x03\x04gzip 壓縮數(shù)據(jù)1F 8B無二進(jìn)制特征常見 shell 腳本23 21#!你可以在文件開頭上做文章也可以在某一段結(jié)構(gòu)的固定偏移處找特征。比如 tar 歸檔的特征字符串ustar并不在文件頭部而是出現(xiàn)在偏移 257 字節(jié)的位置。所以file的匹配規(guī)則不是簡單地“對前幾個字節(jié)”而是支持指定偏移量、指定類型、指定正則表達(dá)式的一套完整規(guī)則引擎。理解了這一點再看后面的章節(jié)就會很順。1.3 它到底“看不看”文件內(nèi)容一句話file從不依賴文件名它內(nèi)部會讀取文件內(nèi)容按偏移量取片段、做二進(jìn)制比對、做字符串匹配、做編碼試探最后把命中的規(guī)則對應(yīng)的人類可讀描述輸出給你。對普通文件它讀的是文件字節(jié)流對特殊文件比如塊設(shè)備它用-s參數(shù)直接去讀設(shè)備原始數(shù)據(jù)而不是只看 stat 輸出里的文件類型。2. 基礎(chǔ)用法與高頻參數(shù)2.1 最常用的幾種調(diào)用方式最基礎(chǔ)用法直接給文件名$ file unknown.bin unknown.bin: gzip compressed data, from Unix, original size modulo 2^32 10240輸出格式一般是“文件名: 類型描述”。如果嫌文件名礙事用-b只顯示描述部分$ file -b unknown.bin gzip compressed data, from Unix, original size modulo 2^32 10240想拿到更適合程序解析的 MIME 類型用-i有的版本里寫作--mime$ file -i unknown.bin unknown.bin: application/gzip更穩(wěn)定的做法是用--mime-type只輸出 MIME 類型不帶 charset$ file --mime-type unknown.bin unknown.bin: application/gzip一次查多個文件也很自然直接全部堆在后面即可。如果文件列表放在一個文本文件里可以用-f$ file -f list.txt每條記錄一行file會逐行讀取并當(dāng)作文件名去檢測。配合-0還能讓輸出以空字符分隔方便接到xargs -0里處理奇葩文件名。2.2 參數(shù)速查表參數(shù)這塊不需要死記用的時候查表就行。我整理了一個自己經(jīng)常翻的速查表參數(shù)作用使用示例-b只顯示類型描述不顯示文件名file -b test.sh-i輸出 MIME 類型含 charset 時也一起帶file -i test.sh--mime-type只輸出 MIME 類型適合腳本判斷file --mime-type test.sh-f FILE從 FILE 讀取待檢測文件列表file -f list.txt-L跟隨符號鏈接檢測鏈接指向的真實文件file -L /usr/bin/python3-h不跟隨符號鏈接只看鏈接本身file -h /usr/bin/python3-s檢測特殊文件如設(shè)備文件的原始內(nèi)容file -s /dev/sda-z對壓縮文件嘗試查看內(nèi)部內(nèi)容file -z archive.zip-k不命中第一條就停下輸出所有匹配結(jié)果file -k sample.bin-m FILE使用自定義 magic 規(guī)則文件file -m myrule.mgc test.bin-C編譯 magic 規(guī)則文件為 .mgcfile -C -m myrule-d調(diào)試模式打印匹配過程file -d -m myrule test.bin-0文件名后追加空字符方便 xargs -0file -0 -f list.txt2.3 符號鏈接與特殊文件-L / -h / -s / -z這里有一個容易踩的小坑。不同發(fā)行版對符號鏈接的默認(rèn)行為并不完全一致很多系統(tǒng)上你直接執(zhí)行file /usr/bin/python3看到的是一行/usr/bin/python3: symbolic link to python3.11也就是說它默認(rèn)停在“這是一個符號鏈接”這一步不再往下追。如果你想知道鏈接背后的目標(biāo)文件真實類型加-L$ file -L /usr/bin/python3 /usr/bin/python3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.17, stripped如果你就是想知道鏈接本身的信息可以顯式加-h但大多數(shù)發(fā)行版默認(rèn)就是這個行為所以記一條經(jīng)驗看到 symbolic link 輸出且想繼續(xù)深挖時補(bǔ)一個 -L。-s參數(shù)平時用得少但查塊設(shè)備或者裸設(shè)備時很管用。對/dev/sda這類設(shè)備file默認(rèn)只顯示block special不會真的去讀盤內(nèi)容。加上-s后它會嘗試讀取設(shè)備的原始數(shù)據(jù)識別文件系統(tǒng)類型或分區(qū)簽名。-z則用來處理壓縮包的雙層結(jié)構(gòu)。壓縮文件本身類型是gzip compressed data但里面裝的是什么-z會嘗試解壓一層再繼續(xù)識別類似“剝洋蔥”。$ file -z app.tar.gz app.tar.gz: POSIX tar archive (gzip compressed data)3. 從字節(jié)到結(jié)論file 的內(nèi)部機(jī)制3.1 magic 規(guī)則庫藏在系統(tǒng)里的“字典”file之所以能識別成千上萬種文件格式不是因為它硬編碼了這些格式而是因為它讀取了一個名為 magic 的規(guī)則庫。這個規(guī)則庫在大部分 Linux 發(fā)行版上位于/usr/share/file/magic.mgc或/usr/share/misc/magic.mgc是編譯后的二進(jìn)制索引文件。想直接看人類可讀的規(guī)則源碼可以找同目錄下的magic文件。每條規(guī)則大致長這樣0 string PK\003\004 Zip archive data逐列拆開解釋第一列是偏移量說明要匹配的位置第二列是數(shù)據(jù)類型string表示按字符串匹配其他常見類型還有byte1字節(jié)、short2字節(jié)、long4字節(jié)、date時間戳等第三列是期望匹配到的內(nèi)容第四列是匹配成功后輸出的描述文本。file執(zhí)行時的行為就是打開 magic 庫逐條規(guī)則與文件內(nèi)容比對一旦匹配上輸出對應(yīng)的描述然后停下。默認(rèn)匹配到第一條就停所以輸出通常是一個“最可能的類型”加上-k才繼續(xù)把所有匹配結(jié)果全部列出來。3.2 自定義規(guī)則和編譯有些場景下系統(tǒng)自帶規(guī)則不夠用比如團(tuán)隊內(nèi)部有一種自定義的二進(jìn)制固件格式文件頭固定是0x434F4445對應(yīng)字符串CODE想在file里也能識別出來怎么辦自己寫規(guī)則即可。先創(chuàng)建一個文本文件假設(shè)叫myrule.magic0 string CODE MyCompany Firmware Image然后直接用-m指定這個規(guī)則文件測試$ file -m myrule.magic firmware_v1.bin firmware_v1.bin: MyCompany Firmware Image確認(rèn)規(guī)則沒問題想把規(guī)則合并進(jìn)正式的 magic 庫里用-C編譯$ file -C -m myrule.magic這會在當(dāng)前目錄生成myrule.magic.mgc之后使用file -m myrule.magic.mgc就能讀編譯后的規(guī)則。如果機(jī)器上還有其他工具也想復(fù)用這套規(guī)則可以把.mgc文件放到/usr/share/file/之類的目錄里或者放進(jìn)MAGIC環(huán)境變量指定的路徑。規(guī)則還可以更復(fù)雜比如附加二級匹配來輸出更多信息0 string CODE MyCompany Firmware Image 4 byte x version %d第一行匹配到CODE之后第二行的表示在已經(jīng)命中主規(guī)則的基礎(chǔ)上繼續(xù)檢查偏移 4 處的字節(jié)并把版本號帶出來。這種嵌套寫法在生產(chǎn)環(huán)境很實用自定義文件類型能一眼看出版本信息。3.3 用 -d 觀察 file 內(nèi)部在干嘛想知道file到底是怎么匹配上的可以開調(diào)試模式$ file -d -m myrule.magic firmware_v1.bin調(diào)試模式下它會把每一條規(guī)則的匹配過程和結(jié)果打到標(biāo)準(zhǔn)錯誤輸出包括嘗試的偏移量、讀取的字節(jié)、比對結(jié)果。我第一次看輸出的時候有點震驚原來它能識別這么多格式靠的就是一條條傻笨的規(guī)則硬比對。但也正因為這種“規(guī)則遍歷”的工作方式規(guī)則寫得越多執(zhí)行耗時越長。對海量文件做批處理時如果機(jī)器性能緊張這點值得留意。4. 實戰(zhàn)典型文件檢測過程實錄4.1 先看字節(jié)xxd 把魔數(shù)貼出來file是黑盒結(jié)論但排查問題的時候我習(xí)慣把證據(jù)也拉出來雙管齊下。最常用的輔助工具是xxd或hexdump。$ xxd unknown.dat | head -n 2 00000000: 504b 0304 1400 0000 0000 0000 0000 0000 PK.............. 00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................看到PK\x03\x04這個開頭基本可以斷定是個 ZIP 系壓縮包。這時候再看file的輸出$ file unknown.dat unknown.dat: Zip archive data, at least v2.0 to extract結(jié)論和字節(jié)證據(jù)對上了后面處理起來就踏實。4.2 ELF 可執(zhí)行文件與腳本的差別絕大多數(shù)的 Linux 可執(zhí)行文件都是 ELF 格式。file對 ELF 的輸出很豐富能看出架構(gòu)、位數(shù)、是否動態(tài)鏈接、是否去除符號表$ file /bin/ls /bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.17, BuildID[sha1]..., stripped如果是靜態(tài)編譯的二進(jìn)制它會顯示statically linked如果沒 strip會顯示not stripped。這些信息在排查“為什么這個程序在這臺機(jī)器上跑不起來”時非常有用比如拿到一個 ARM 架構(gòu)的二進(jìn)制放到 x86 機(jī)器上file一眼就能拆穿。腳本類文件的檢測就更有意思了。帶 shebang 的腳本通常會被識別為對應(yīng)語言的腳本$ file deploy.py deploy.py: Python script, ASCII text executable $ file run.sh run.sh: Bourne-Again shell script, ASCII text executable注意這里的識別依據(jù)是文件第一行的#!以及緊隨其后的解釋器名稱。如果腳本第一行寫的是#!/usr/bin/env python3有些file版本會輸出一個比較奇怪的描述因為規(guī)則庫里未必專門適配過env這種間接寫法。4.3 壓縮包與復(fù)合文檔不止一層皮壓縮文件是重災(zāi)區(qū)。ZIP、gzip、bzip2、xz、7z 各有各的特征用file一眼分清壓縮格式文件頭特征file 輸出ZIP50 4B 03 04Zip archive datagzip1F 8Bgzip compressed databzip242 5A 68bzip2 compressed dataxzFD 37 7A 58 5A 00XZ compressed data7z37 7A BC AF 27 1C7-zip archive data復(fù)合文檔更有意思。.docx、.xlsx、.pptx這些 Office 文件本質(zhì)上是 ZIP 容器里面是 XML 文件。新版本file會進(jìn)一步解析容器內(nèi)部結(jié)構(gòu)輸出Microsoft Word 2007之類的結(jié)果老版本則可能只報Zip archive data。遇到版本比較舊的系統(tǒng)時別急著下結(jié)論可以結(jié)合unzip -l查看內(nèi)部文件確認(rèn)。5. 在腳本中集成 file自動化判斷文件類型5.1 條件分支用 MIME 類型做分流file真正的威力在自動化場景。比如我有一個上傳目錄用戶傳上來的文件亂七八糟但業(yè)務(wù)只收圖片。寫腳本時用--mime-type做條件判斷非常穩(wěn)定#!/usr/bin/env bash for f in /upload/unknown/*; do [ -f $f ] || continue mime$(file -b --mime-type $f) case $mime in image/jpeg|image/png|image/gif) echo [OK] $f - $mime mv $f /upload/images/ ;; application/pdf) echo [OK] $f - $mime mv $f /upload/pdf/ ;; text/*) echo [SKIP] $f - $mime ;; *) echo [??? ] $f - $mime ;; esac done這里用-b去掉文件名前綴直接拿 MIME 字符串做比對。實測下來--mime-type比-i更適合腳本因為輸出更簡潔不會有text/plain; charsetus-ascii這種帶分號尾巴的情況case 匹配寫起來省心很多。5.2 批量清理未知擴(kuò)展名文件另一個高頻需求是把一堆無擴(kuò)展名文件按真實內(nèi)容分類改名。比如設(shè)備導(dǎo)出的日志文件有的是純文本、有的是 gzip 壓縮、有的是 JSON、有的是 SQLite 數(shù)據(jù)庫。用find加循環(huán)處理find /data/raw -type f -print0 | while IFS read -r -d f; do mime$(file -b --mime-type $f) case $mime in text/*) ext.log ;; application/gzip) ext.gz ;; application/json) ext.json ;; application/vnd.sqlite3) ext.db ;; *) ext.unknown ;; esac mv $f ${f}${ext} done我在測試時特意用了帶空格和特殊字符的文件名做驗證所以用-print0搭配read -d 這比for f in $(find ...)安全得多不會把文件名按空格劈成好幾段。5.3 性能與集成注意點每次調(diào)用file都會啟動一個子進(jìn)程批量處理幾千個文件時沒問題但如果面對幾十萬個文件進(jìn)程啟動開銷就凸顯了。更高效的方案是用libmagic直接內(nèi)嵌調(diào)用比如用 Python 的python-magic庫import magic m magic.Magic(mimeTrue) print(m.from_file(sample.bin))很多系統(tǒng)依賴這個庫做文件類型識別。命令行版本和庫版本底層的 magic 規(guī)則庫基本一致但庫方式省去了反復(fù) fork 進(jìn)程的消耗對大規(guī)模掃描更友好。不過庫版本輸出的細(xì)節(jié)控制沒命令行那么直觀需要自己處理異常、路徑等邊界情況。6. 常見問題與避坑實錄6.1 問題速查表平時群里被問得最多的問題基本都能歸進(jìn)下面這張表現(xiàn)象常見原因解決思路file輸出data規(guī)則庫沒匹配到已知格式用xxd看文件頭搜索特征或補(bǔ)充自定義規(guī)則腳本識別成ASCII text而不是 Pythonfile版本規(guī)則不全或 shebang 寫法特殊升級file或用--mime-type輔助判斷符號鏈接輸出symbolic link to默認(rèn)未跟隨鏈接加-L查看目標(biāo)真實類型大文件檢測很慢規(guī)則庫太大或文件需讀多次用-k減少批處理次數(shù)時反而更慢建議換libmagic或優(yōu)化流程gzip 壓縮包內(nèi)層類型未識別file默認(rèn)不收內(nèi)層加-z嘗試解壓并識別內(nèi)層老版本報表不識別 docx/xlsxOffice 格式是 ZIP 容器舊規(guī)則庫沒解析內(nèi)部 XML更新 magic 庫或臨時用unzip -l輔助6.2 為什么腳本識別不出腳本語言我碰到最多的一個具體場景是寫好的 TCL 腳本、Ruby 腳本file只報ASCII text卻不報具體語言。原因通常是系統(tǒng)自帶的 magic 規(guī)則庫里沒有對應(yīng)語言的規(guī)則或者規(guī)則匹配條件比較嚴(yán)格。不同版本的file差異也很大有的能識別 Python有的只能籠統(tǒng)說script text executable。這種情況下別慌。先用head -n 1 file看 shebang再決定用什么解釋器執(zhí)行。file只能作為參考線索不能把它當(dāng)成絕對真理。如果確實需要讓file識別出錯可以按 3.2 的方法寫自定義規(guī)則把#!/usr/bin/env foo這種開頭映射到自定義類型。6.3 安心使用但別盲目相信這個坑要重點說魔數(shù)是可以偽造的。file匹配的是特征字節(jié)不是密碼學(xué)驗證。往一個文本文件開頭塞一串%PDF字符file就會把它識別成 PDF往一個二進(jìn)制文件頭塞PK\x03\x04它就會被認(rèn)為是 ZIP。這不是 bug而是文件格式識別技術(shù)本身的先天限制。我在做安全分析的時候從不會把file的結(jié)果當(dāng)作唯一依據(jù)。收到一個可疑文件正確姿勢是file先看類型再用xxd觀察文件頭用strings看字符特征必要時交給binwalk、clamscan這類專業(yè)工具深入分析。把它當(dāng)成“第一個線索”而不是“最終結(jié)論”才能避免被精心構(gòu)造的文件繞進(jìn)去。最后分享一個小技巧如果你長期靠file處理各種新式文件格式記得定期升級file軟件包并更新 magic 規(guī)則庫。新格式層出不窮系統(tǒng)自帶的規(guī)則庫更新往往滯后。遇到識別成data的文件先別著急下結(jié)論查一下該格式最新的魔數(shù)特征自己補(bǔ)一條規(guī)則往往比到處找轉(zhuǎn)換工具更快。