檔案的魔法 - 檔案格式的檢測
前言
本文探討的是計算機檔案, 計算機檔案 用於記錄資料到計算機裝置上,維基百科上有簡短的介紹:
A computer file is a computer resource for recording data discretely in a computer storage device. Just as words can be written to paper, so can information be written to a computer file.
當人們需要使用這些檔案的時候,需要從光碟,磁碟,快閃記憶體等裝置上將檔案讀取到記憶體,按照檔案的格式進行解析,然後供使用者使用。在這個過程中,正確的或得檔案格式資訊是非常重要的,只有在識別出文件格式之後,才能夠選擇正確的的處理程式對檔案進行解析。在 Windows 上通常是 Shell 外殼( Shell32.dll
)根據檔案字尾名在登錄檔中找到對應的關聯程式然後使用特定的程式處理相應的檔案,比如 .docx
的關聯程式往往是 Microsoft Word
。 .txt
的關聯程式是 Notepad
。但如果檔案沒有後綴名時,Windows Shell 就需要用使用者自己選擇對應的關聯程式了。
在 Unix 作業系統上,命令列下檢測檔案格式的檢測通常使用 file
( file — determine file type
) ,file 的原始碼在 Github 上有映象: https://github.com/file/file 。file 這樣的工具通過分析檔案魔數,檔案頭部特徵分析檔案格式,這樣的工具嚴重依賴 Magdir ,Magic 檔案越多支援的格式越豐富。file 這樣的命令與 Windows 資源管理器相比,已經有很大的進步。
在圖形系統中,檔案的檢測由檔案管理器實現,像 Gnome Nemo
這樣的檔案管理器會優先處理檔案字尾名,在識別不到檔案格式時才會去根據檔案特徵檢測檔案格式。Nemo 依賴 glib(gio _xdg_mime_magic_lookup_data
),和 file 的原理類似但沒有 file
強大。
file
程式目前已經被移植到 Windows 使用,比如 Cygwin
, MSYS2
的 Bash 環境中,均攜帶有 file
命令。
我最開始去了解檔案型別的檢測是在實現 LFS 伺服器的時候,基於 C++ 編寫的 LFS 伺服器使用的是 libmagic
, libmagic 即 file
的一部分,而基於 Golang
編寫的 LFS 伺服器使用的則是 https://github.com/h2non/filetype
在重構完 Privexec
之後,突然想寫一個檔案型別檢測工具,最開始叫做 FileView
後來改名為 Planck
,當 Planck
大概能用的時候,想把一些見解分享給大家,於是有了此篇文章。
背景知識
位元組序
位元組序: Endianness ,位元組順序,又稱端序或尾序(英語:Endianness),在電腦科學領域中,指儲存器中或在數字通訊鏈路中,組成多位元組的字的位元組的排列順序。
- x86、MOS Technology 6502、Z80、VAX、PDP-11等處理器為小端序;
- Motorola 6800、Motorola 68000、PowerPC 970、System/370、SPARC(除V9外)等處理器為大端序;
- ARM、PowerPC(除PowerPC 970外)、DEC Alpha、SPARC V9、MIPS、PA-RISC及IA64的位元組序是可配置的。
網路位元組序為 Big Endian
,目前 Windows x86, AMD64, ARM, ARM64 均為 Little Endian
。
Planck 中位元組序轉換程式碼在: https://github.com/fcharlie/Planck/blob/master/include/endian.hpp 。
檔案十六進位制檢視工具
檢視檔案資訊可以使用支援 16 進位制的工具檢視,GUI 的工具有 Sublime Text
, 010Editor
等,CLI 的工具有 hexdump
, xxd
,還有最近帶顏色高亮的
- Hastyhex: https://github.com/skeeto/hastyhex
- Hexyl: https://github.com/sharkdp/hexyl
Hastyhex 基於 C 編寫,但不支援指定長度,對 Windows 10 控制檯支援不太好,於是我 fork Hastyhex,對其改進,使其支援特定長度和從指定位置開始讀取。在 Windows 上改進了控制檯顏色輸出。
Unix 版本: HastyHex : a faster hex dumper
針對 Windows 10 控制檯改進的版本: https://github.com/fcharlie/Planck/tree/master/utils/hastyhex
檔案,硬連結,軟連線,快捷方式
在儲存裝置上,一個檔案通常是常規檔案,但檔案也有可能指向其它檔案。
硬連結與軟連結
Hard Link 通常意味著一個原始檔案可能存在有多個檔名,比如 Linux 一個 inode 對應多個路徑。
In computing, a hard link is a directory entry that associates a name with a file on a file system. All directory-based file systems must have at least one hard link giving the original name for each file. The term “hard link” is usually only used in file systems that allow more than one hard link for the same file.
Windows NTFS,Unix EXT4,ZFS,Btrfs 等檔案系統均支援硬連結,ReFS 暫時不支援硬連結。
在 Windows 中,硬連結被廣泛使用,尤其是 Side-by-side assembly 機制大量使用了硬連結 ,檢視 C:\Windows\System32
的檔案,基本都會有相應的硬連結存在於 C:\Windows\WinSxS
。
Git 在克隆本地儲存庫時, objects
目錄的物件檔案(主要是 pack)建立的是硬連結。這樣避免了複製,git 的 物件 檔名與其內容的 SHA1 一致,當檔案內容改變時,檔名也會改變,因此,使用硬連結不用當心互相修改破壞。
在 Windows 中,可以使用 GetFileInformationByHandle
, FindFirstFileNameW
, FindNextFileNameW
組合查詢檔案所有的硬連結。 在 POSIX 系統中,查詢硬連結需要解析對應的 inode,如果 inode 值相同,則互為硬連結。 struct stat
結構中有 st_nlink
表示此檔案有多少個硬連結。
Symbolic link 符號連結(軟連結)是一類特殊的檔案, 其包含有一條以絕對路徑或者相對路徑的形式指向其它檔案或者目錄的引用。
軟連結在 Unix 系統中被廣泛使用,在終端中輸入命令: ls -l /usr/bin
可以看到大量的軟連結。
早期符號連結的實現,採用直接分配磁碟空間來儲存符號連結的資訊,這種機制與普通檔案一致。這種符號連結檔案裡包含有一個指向目標檔案的文字形式的引用,以及一個指示自己為符號連結的標誌。
這樣的儲存方式被證明有些緩慢,並且早一些小型系統上會浪費磁碟空間。一種名為快速符號連結的新型儲存方式能夠將文字形式的連結儲存在用於存放檔案資訊的磁碟上的標準資料結構之中(inode)。為了表示區別,原先的符號連結儲存方式也被稱作慢速符號連結。NTFS 檔案系統的符號連結是基於 NTFS ReparsePoint 功能實現。
在 POSIX 系統中, readlink
可以解析符號連結獲得真實的目標路徑,在 Windows 中,則可以使用 GetFinalPathNameByHandleW
獲得檔案真實的路徑。
NTFS 系統還支援一些其他的重解析點,包括 MountPoint
, 與 UWP 快捷命令目標相關的 AppExecLink
, 與 Windows 10 Unix domain socket 相關的 AF Unix
, 與 OneDrive 相關的 OneDrive
, 與 Git VFS(GVFS) 相關的 ProjFS
, 以及與 WIM 掛載相關的 WimImage
等等。Planck 中實現了函式 ResolveTarget
用於分析重解析點。
快捷方式和桌面檔案
在 Windows 系統中,桌面快捷方式檔案的字尾名為 .lnk
,使用者只需要點選桌面上的快捷方式就可以很方便的開啟應用程式,網站或者檔案。快捷方式的格式名稱叫做 Shell Link
,是一種二進位制格式檔案,相應的規範在 [MS-SHLLINK]: Shell Link (.LNK) Binary File Format 。在 Planck 中,ShellLink 的定義和實現分別是 lib/inquisitive/shl.hpp 和 lib/inquisitive/shl.cc ,目前只支援解析 HasLinkInfo
以及 HasRelativePath
標誌的快捷方式。
在 X-Window 系統上,也存在一種類似桌面快捷方式的檔案,字尾名為 .desktop
,當檔案屬性為可執行時,檔案管理器會解析 Icon
, Name
然後讀取設定的圖示,名稱顯示。下面是我 Ubuntu 系統上的 wireshark.desktop
檔案內容。
#!/usr/bin/env xdg-open [Desktop Entry] Name=Wireshark Comment=Wireshark build GenericName=Demo Application Exec=/opt/wireshark/bin/wireshark Icon=wireshark Type=Application StartupNotify=true Categories=GNOME;GTK;Development;Documentation; MimeType=text/plain;
可以看出,這是一個標準的 Shebang
可執行檔案,在 Ubuntu 中, xdg-open
自身為 Shell
指令碼(在MacOS X下的Darwin中,直譯器指定的檔案必須是可執行的二進位制檔案,並且本身不能是指令碼。),雖然這是一個 Shebang
可執行檔案,但遺憾的是,在 Ubuntu 中,你無法直接從命令列中使用 xdg-open
啟動相應的程式,這是一個存在了超過 9 年的 BUG: xdg-open *.desktop opens text editor
文字檔案還是二進位制
在計算機中,文字檔案實際上支援二進位制檔案的一種,這種檔案幾乎只由可列印字元,控制字元組成,而二進位制檔案則包含大量的不可見字元。處理程式將按照定義的二進位制格式對二進位制檔案進行解析。
快速區分文字二進位制
實際上,文字檔案還是偶爾會攜帶不可見字元,這樣情況下我們很難 100% 區分一個檔案是否是文字檔案(二進位制檔案)。如果能夠容忍一些誤差, ,我們可以檢測檔案中是否存在 NUL
來區分檔案是文字檔案還是二進位制檔案。雖然這種方法可能誤差較大,但是檢測過程非常簡單,速度也非常可觀,這種方法也被 git
使用,用於在 diff 過程中判斷檔案是否是二進位制:
//https://github.com/git/git/blob/d166e6afe5f257217836ef24a73764eba390c58d/xdiff-interface.c#L188 int buffer_is_binary(const char *ptr, unsigned long size) { if (FIRST_FEW_BYTES < size) size = FIRST_FEW_BYTES; return !!memchr(ptr, 0, size); }
我們知道,在 C 語言的標準庫函式 strlen
中,字串的長度計算是通過判斷字元是否是 Null-terminated string ,這就意味著大多數時候,ASCII 文字檔案不應該有 NUL
,在 UTF-8 與 ASCII 相容,這種情況下是一致的。當然這種設計也保守批評: The Most Expensive One-byte Mistake
size_t strlen(const char *s) { const char *a = s; for (; *s; s++); return s-a; }
編碼的文字
ASCII 編碼的範圍是 0 ~127,這就意味著只能用於 A-Z;a-z;0-9,+-
數字,英文字母一些基本符號控制字元等少量的字元,如果儲存非英語國家的文字基本上是不現實的,就算把 128 ~ 255 全部用上,像中文這種有幾千上萬文字的語言是無法表示的。為了支援更多的文字,後來人們制定了國際標準化的 US4(UTF-32) US2(UTF-16),UTF-8,國內製定 GBK。當編碼的種類多起來的時候,問題又來了,如何確定檔案編碼?
例如 UTF-16
, UTF-32
這樣的編碼,由於是多位元組的,因此可能存在多位元組序,通過檢測多位元組序就可以簡單的獲得檔案編碼:
編碼 | 起始字元 |
---|---|
UTF-32 BE | 0x0,0x0,0xFE,0xFF |
UTF-32 LE | 0xFF,0xFE,0x00,0x00 |
UTF-16 BE | 0xFE,0xFF |
UTF-16 LE | 0xFF,0xFE |
UTF-8 with BOM | 0xEF,0xBB,0xBF |
UTF-8 是一種位元組序無關的可變位元組編碼(1 ~ 4 位元組),因此,不帶位元組序沒有任何問題,並且 ASCII 編碼 0 ~ 127 完全是 UTF-8 的子集,如果不攜帶位元組序,能夠很好的相容以前的 ASCII 文字。這也是 UTF-8 在 Unix 系統上被廣泛使用的原因之一。而 Windows 記事本採用 UTF-8 with BOM 也由於這一點廣受批評。
Windows 系統是一個國際化做的非常棒的作業系統,對於各國的本地字符集支援也非常好,比如,在中國大陸,文字編輯器的預設編碼是 ANSI,是 ASCII 擴充套件編碼,0 ~ 127 編碼與 ASCII 相同,0x80 ~ 0xFFFF 則表示對應內碼表的所有編碼。我們可以看到,ANSI 編碼的範圍小於 UTF-8,並且絕大多數 ANSI 字元的碼點相同數字的 UTF-8 碼點都是有效的 UTF-8 字元,因此如果要區分 UTF-8 without BOM
還是 ANSI
,實際上相當麻煩。
有效的 UTF-8 字元區間:
/* * legal utf-8 byte sequence * http://www.unicode.org/versions/Unicode6.0.0/ch03.pdf - page 94 * *Code Points1st2s3s4s * U+0000..U+007F00..7F * U+0080..U+07FFC2..DF80..BF * U+0800..U+0FFFE0A0..BF80..BF * U+1000..U+CFFFE1..EC80..BF80..BF * U+D000..U+D7FFED80..9F80..BF * U+E000..U+FFFFEE..EF80..BF80..BF * U+10000..U+3FFFFF090..BF80..BF80..BF * U+40000..U+FFFFFF1..F380..BF80..BF80..BF * U+100000..U+10FFFF F480..8F80..BF80..BF * */
另外,對於 ANSI 而言,不同字符集的都重複使用著 0x80 ~ 0xFFFF 編碼區間,這進一步加大了文字字元檢測的難度。
文字編碼的檢測有兩個比較流行的實現,一個是 IE 的 IMultiLanguage ,另一個是 Firefox 的 UniversalCharsetDetection ,後者的準確性更高,使用更加廣泛,比如 Notepad++
就是使用了 universalchardet
。
使用者通常不應直接使用 Mozilla 目錄中的 Universalchardet
, Universalchardet
與 Firefox 整合較為緊密,剝離稍微有點麻煩,最近的版本只有很少的幾個 LangModels
實現。如果要使用 Universalchardet
,可以使用 Freedesktop 維護的: uchardet ,這個庫基於 Universalchardet
發展起來的,能編譯成動態庫或者靜態庫供開發者整合到自己的程式之中。
但 uchardet
的許可證為 MPL 1.1
, GPL 2.0
LGPL 2.1
,程式在依賴 uchardet
時要考慮許可證的問題。如果僅僅只需要判斷文字是否是 UTF-8,可以按照上圖的 UTF-8 編碼區間對檔案進行分析,程式碼如下:
// Thanks // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2018/05/08/checkutf8.c static const uint8_t utf8d[] = { 0,0,0,0,0,0,0,0,0,0,0,// 0,0,0,0,0,0,0,0,0,0,0,// 0,0,0,0,0,0,0,0,0,0,// 00..1f 0,0,0,0,0,0,0,0,0,0,0,// 0,0,0,0,0,0,0,0,0,0,0,// 0,0,0,0,0,0,0,0,0,0,// 20..3f 0,0,0,0,0,0,0,0,0,0,0,// 0,0,0,0,0,0,0,0,0,0,0,// 0,0,0,0,0,0,0,0,0,0,// 40..5f 0,0,0,0,0,0,0,0,0,0,0,// 0,0,0,0,0,0,0,0,0,0,0,// 0,0,0,0,0,0,0,0,0,0,// 60..7f 1,1,1,1,1,1,1,1,1,1,1,// 1,1,1,1,1,9,9,9,9,9,9,// 9,9,9,9,9,9,9,9,9,9,// 80..9f 7,7,7,7,7,7,7,7,7,7,7,// 7,7,7,7,7,7,7,7,7,7,7,// 7,7,7,7,7,7,7,7,7,7,// a0..bf 8,8,2,2,2,2,2,2,2,2,2,// 2,2,2,2,2,2,2,2,2,2,2,// 2,2,2,2,2,2,2,2,2,2,// c0..df 0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, // 0x3, 0x3, 0x4, 0x3, 0x3,// e0..ef 0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // 0x8, 0x8, 0x8, 0x8, 0x8// f0..ff }; static const uint8_t utf8d_transition[] = { 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, // 0x6, 0x1, 0x1, 0x1, 0x1,// s0..s0 1,1,1,1,1,1,1,1,1,1,1,// 1,1,1,1,1,1,0,1,1,1,1,// 1,0,1,0,1,1,1,1,1,1,// s1..s2 1,2,1,1,1,1,1,2,1,2,1,// 1,1,1,1,1,1,1,1,1,1,1,// 1,2,1,1,1,1,1,1,1,1,// s3..s4 1,2,1,1,1,1,1,1,1,2,1,// 1,1,1,1,1,1,1,1,1,1,1,// 1,3,1,3,1,1,1,1,1,1,// s5..s6 1,3,1,1,1,1,1,3,1,3,1,// 1,1,1,1,1,1,3,1,1,1,1,// 1,1,1,1,1,1,1,1,1,1,// s7..s8 }; static inline uint32_t updatestate(uint32_t *state, uint32_t byte) { uint32_t type = utf8d[byte]; *state = utf8d_transition[16 * *state + type]; return *state; } bool validate_utf8(const char *c, size_t len) { const unsigned char *cu = (const unsigned char *)c; uint32_t state = 0; for (size_t i = 0; i < len; i++) { uint32_t byteval = (uint32_t)cu[i]; if (updatestate(&state, byteval) == UTF8_REJECT) { return false; } } return true; }
可執行檔案
在計算機中,可執行檔案是非常特殊的存在,現代計算機的執行離不開應用程式,而應用程式在磁碟上的形式就是可執行檔案,維基百科上有簡短的介紹:
可執行檔案在電腦科學上,指一種內容可被計算機解釋為程式的計算機檔案。通常可執行檔案內,含有以二進位制編碼的微處理器指令,也因此可執行檔案有時稱為二進位制檔。這些二進位制微處理器指令的編碼,於各種微處理器有所不同,故此可執行檔案多數要分開不同的微處理版本。一個計算機檔案是否為可執行檔案,主要由作業系統的傳統決定。例如根據特定的命名方法(如副檔名為exe)或檔案的元資料資訊(例如UNIX系統設定“可執行”許可權)。
可執行檔案的格式非常多,但目前應用比較廣泛的只有 PE(PE32+),ELF,Mach-O。主要的作業系統分別是 Windows,Linux,macOS。
可執行檔案的比較
不同的可執行檔案的特性有一些不同,維基百科上有個比較: Comparison of executable file formats 。
我這裡將 PE(PE32+),ELF,Mach-O 的格式比較貼出來:
格式名 | 作業系統 | 副檔名 | 顯式處理器宣告 | 任意節(Sections) | 元資料 | 簽名 | 字串表 | 符號表 | 64位 | 胖二進位制 | 可以包含圖示 |
---|---|---|---|---|---|---|---|---|---|---|---|
PE | Windows, ReactOS HX DOS Extender BeOS (>=R3) |
.EXE | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | :x: | :x: | ✔ |
PE32+ | Windows 64-bit | .EXE | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
ELF | Unix-like, OpenVMS | none | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | Extension | Extension |
Mach-O | NeXTSTEP macOS, iOS, watchOS tvOS |
none | ✔ | <=256 | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | :x: |
PE
PE 是 Windows NT 系統的可執行檔案格式,同樣還被 ReactOS 使用,PE32+ 是 PE 格式的一種改進,用於支援 64位處理器。要檢視 PE 檔案格式可以檢視: PE Format 。在 Windows SDK 中, winnt.h
已經定義了大量的 PE 結構,但並不完整,如果要獲得更加完整的結構,需要使用 Windows WDK 的 ntimage.h
,但一些新的硬體定義需要去 PE Format 查詢。
/// #define PROCESSOR_ARCHITECTURE_ARM32_ON_WIN6413 #ifndef IMAGE_FILE_MACHINE_ARM64 //// IMAGE_FILE_MACHINE_ARM64 is Windows #define IMAGE_FILE_MACHINE_ARM64 0xAA64 // ARM64 Little-Endian #endif #ifndef IMAGE_FILE_MACHINE_RISCV32 #define IMAGE_FILE_MACHINE_RISCV32 0x5032 #endif #ifndef IMAGE_FILE_MACHINE_RISCV64 #define IMAGE_FILE_MACHINE_RISCV64 0x5064 #endif #ifndef IMAGE_FILE_MACHINE_RISCV128 #define IMAGE_FILE_MACHINE_RISCV128 0x5128 #endif #ifndef IMAGE_FILE_MACHINE_CHPE_X86 #define IMAGE_FILE_MACHINE_CHPE_X86 0x3A64 /// defined in ntimage.h #endif #ifndef IMAGE_SUBSYSTEM_XBOX_CODE_CATALOG #define IMAGE_SUBSYSTEM_XBOX_CODE_CATALOG 17 // XBOX Code Catalog #endif
EXE,DLL 檔案的魔數是 {'M','Z',0x90,0x0}
這實際上是 IMAGE_DOS_HEADER
的 e_magic
,不同系統的簽名並不一樣:
#ifndef _MAC #include "pshpack4.h"// 4 byte packing is the default #define IMAGE_DOS_SIGNATURE0x5A4D// MZ #define IMAGE_OS2_SIGNATURE0x454E// NE #define IMAGE_OS2_SIGNATURE_LE0x454C// LE #define IMAGE_VXD_SIGNATURE0x454C// LE #define IMAGE_NT_SIGNATURE0x00004550// PE00 #include "pshpack2.h"// 16 bit headers are 2 byte packed #else #include "pshpack1.h" #define IMAGE_DOS_SIGNATURE0x4D5A// MZ #define IMAGE_OS2_SIGNATURE0x4E45// NE #define IMAGE_OS2_SIGNATURE_LE0x4C45// LE #define IMAGE_NT_SIGNATURE0x50450000// PE00 #endif
PE 格式的 IMAGE_NT_HEADERS
才是真正的 NT 頭,DOS 頭或者 OS2 頭,主要用於相容,畢竟 Windows 作業系統是從 16 位過來的。
IMAGE_FILE_HEADER
結構儲存了機器架構,可執行檔案特徵和可選頭大小等,解析到 IMAGE_OPTIONAL_HEADER
才算正式解析 PE。IMAGE_OPTIONAL_HEADER32 與 IMAGE_OPTIONAL_HEADER64 中的成員順序有一些差別,這樣的好處是在以 32位 IMAGE_OPTIONAL_HEADER 讀取 64 位 PE 時依然能夠解析到基本欄位(反之也是一樣)。解析 PE 很重要的一個函式是 ImageRvaToVa
在對映為檔案的檔案的映像頭中查詢相對虛擬地址(RVA),並返回檔案中相應位元組的虛擬地址。
解析 PE 檔案匯入匯出,資源等需要解析可選頭的 DataDirectory
陣列,陣列的序號對應的時不同的資源:
#define IMAGE_DIRECTORY_ENTRY_EXPORT0// Export Directory #define IMAGE_DIRECTORY_ENTRY_IMPORT1// Import Directory #define IMAGE_DIRECTORY_ENTRY_RESOURCE2// Resource Directory #define IMAGE_DIRECTORY_ENTRY_EXCEPTION3// Exception Directory #define IMAGE_DIRECTORY_ENTRY_SECURITY4// Security Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC5// Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_DEBUG6// Debug Directory //IMAGE_DIRECTORY_ENTRY_COPYRIGHT7// (X86 usage) #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE7// Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR8// RVA of GP #define IMAGE_DIRECTORY_ENTRY_TLS9// TLS Directory #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG10// Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT11// Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT12// Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT13// Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14// COM Runtime descriptor
解析 PE 檔案依賴需要解析 IMAGE_DIRECTORY_ENTRY_IMPORT
目錄,而 IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
對應的 14 在 .Net 中被使用,用於指向 IMAGE_COR20_HEADER
資訊。
解析 PE 檔案的庫非常多,有被 Avast Threat Labs
使用的 pelib
(沒錯,就是那個防毒軟體 Avast),還有 https://github.com/hasherezade/bearparser , https://github.com/lief-project/LIEF 等非常優秀的開源庫。在 .NET 平臺還有 PeNet 。其中 LIFF
還支援 ELF,Mach-O,ART,OAT 等格式。在 LLVM 的原始碼中 PE 檔案解析程式碼在 llvm/lib/Object/COFFObjectFile.cpp 檔案中。
關於 PE 檔案格式分析的文章非常多,這裡有一篇比較詳細的: x86 Disassembly/Windows Executable Files
分析 PE 的工具非常多,Windows Internal 7th 作者之一的 Pavel Yosifovich 也開發了一個 Portable Executable Explorer 。
Planck 分析了 PE 檔案的機器型別,子系統,依賴,特徵等。後來利用 Planck 的成果將 PEAnalyzer 重構了一番,截圖如下:
我有時候需要從 MSYS2 Mingw64
中提取 wget.exe
,經常需要手動檢視檔案依賴,非常麻煩,實現 Planck PE 解析模組後,於是編寫了 Nodeps 用於將 PE 檔案將同目錄下的所有依賴拷貝到目標目錄。
ELF
Executable and Linkable Format (ELF, formerly named Extensible Linking Format) 是一種運用非常廣泛的可執行檔案格式,目前 Unix-like 作業系統的可執行檔案格式絕大多數都是 ELF 。ELF 的魔數是 {0x7f,'E','L','F'}
。ELF 解析庫有前面的 LIFF 還有被 Avast Threat Labs
使用的 elfio 官方版本地址是: https://github.com/serge1/ELFIO
與 PE 顯著不同的是,ELF 檔案可以有 SONAME
RPATH
RUPATH
這樣的節。除了可執行檔案主動載入依賴動態庫,有作業系統或者可執行檔案載入器被動載入依賴時,PE 檔案依賴 dll 可以從 PATH 以及 PE 檔案所在目錄載入,而 ELF 只能載入 LD_LIBRARY_PATH 以及 RPATH RUPATH 指定目錄下的動態連結庫。PE 的機制容易帶來注入問題,而 Windows 作業系統目前也增加了 KnownDlls 機制減少此類問題的發生。而 ELF 的機制在分發二進位制時容易帶來一些麻煩,但目前很多作業系統已經支援 RUPATH=$ORIGIN/../lib
這樣的方式設定 RUPATH
。另外 ELF 計算真實地址時不像 P需要使用 ImageRvaToVa
換算,在 ELF 檔案的處理過程中,只需要將偏移地址與檔案對映的起始地址相加即可得到資料地址。
ELF 程式在安裝的時候可以主動修改 RPATH/RUPATH,cmake 也支援 CMAKE_INSTALL_RPATH
用於設定 RPATH/RUPATH
。RPATH 和 RUPATH 的區別有篇部落格有介紹: RPATH and RUNPATH ,不同作業系統連結器的處理也稍微有一些差別,大多數時候只要設定一個即可。
我將 cmake 中替換 RPATH 的功能抽出來,建立了專案: cmchrpath ,在 cmchrpath 中還有 elfinfo
用於檢視 ELF 的一些基本資訊。
Mach-O
我沒有任何 mac 裝置,因此沒有進一步分析 Mach-O 格式,實際上很多前輩們寫了非常不錯的文章,比如: PARSING MACH-O FILES 。
Mach-O 一個鮮明的特性就是它是一個支援 FatBinary的格式(PE32+ 實際上也支援,但使用較少),這意味著不同的處理器架構指令能夠儲存在同一檔案當中,在 Mac 將處理器從 PowerPC 架構遷移到 Intel 的過程中運用非常廣泛。
在 Planck 中,Mach-O 格式的定義目錄為: lib/inquisitive/macho.hpp
自解壓檔案和安裝程式
Self-extracting archive 是一種特殊的可執行檔案,執行自解壓檔案時,自解壓檔案將壓縮包解壓到使用者指定目錄,自解壓檔案不需要其他的壓縮軟體即可執行,並且還能執行一些列的動作,在 Windows 系統中通常被用來實現軟體安裝。很多安裝程式就是一個自解壓檔案,你如 NSIS 安裝包可以直接使用 7z 解壓。常見的 7z WinRAR 均支援建立自解壓檔案。
有些安裝程式並不是常規的自解壓檔案,比如 InstallShield
製作的安裝程式,它們將 MSI Package 儲存在 PE 檔案的資源目錄,執行時直接提取,然後呼叫 msiexec 進行安裝。
自解壓檔案和安裝程式,都存在一個非常大的缺點,即檔案的大小不能超過 CPU 定址長度,比如 32位系統不能超過 4 GB。這是因為作業系統在執行可執行檔案時,需要將可執行檔案 mmap
到記憶體,檔案大小不能超過程序的虛擬地址最大長度。
在 Unix 系統上,很少有使用 ELF 製作安裝包的,通常使用 Shell Script 來製作 STGZ 安裝包,比如 cmake 在 Unix 系統中執行 cpack 預設打包時會將模組 CPack.STGZ_Header.sh.in 與壓縮包合併製作成一個 .sh
的安裝程式。
可執行檔案的移植
Porting Windows Dynamic Link Libraries to Linux
文件格式
大家比較熟知的文件格式有 PDF
, RTF
, Microsoft WORD (.doc)