ByteScope

Android DEX 檔案格式

Dalvik Executable 格式——Android 執行環境用的編譯後 bytecode。

MIME 類型
application/octet-stream
副檔名
.dex
分類
執行檔與位元碼

Magic number(檔案簽章)

0x000000006465780Adex·'dex' + newline

關於 Android DEX 格式

DEX 檔案是把一個 Android 應用程式所有編譯後的類別合併成單一檔案,而這正是這個格式的重點:與其用好幾千個小小的 class 檔各自重複寫下它們碰到的型別名稱,DEX 只有一組共用的表——字串、型別、原型、欄位、方法——每個類別定義都用索引指進去。標頭是固定 112 個位元組的大小與位移,指向那些表,所以讀取端可以直接跳到需要的那一張;而 bytecode 本身是暫存器式而不是堆疊式的,這就是為什麼 DEX 指令和 JVM 指令長得完全不一樣。

檔頭結構,一個位元組一個位元組看

位移長度欄位意義
08magic (DEX_FILE_MAGIC)位元組 64 65 78 0A,接著三個 ASCII 數字,再一個 NUL——例如 'dex\n039\0'。那三個版本數字有意義:035 早於 Android 7、037 隨 Android 7 與預設方法登場、038 是 Android 8、039 是 Android 9、040 是 Android 10。裝置會拒絕版本比它認識的還新的 DEX。
84checksum這個欄位之後整個檔案的 Adler-32。它是執行環境第一個驗證的東西,所以在 DEX 任何地方做任何編輯,最後都必須重算這個值——這正是為什麼手動改一個位元組,做出來的檔案裝不起來。
1220signature它之後所有內容的 SHA-1。這不是安全性簽章——它是內容識別碼,執行環境與建置快取用它判斷兩個 DEX 是不是同一個,而且任何修改之後同樣要重算。
324file_sizeDEX 完整的位元組大小。把它和實際檔案長度比對,是最便宜的截斷測試,也是抓出「從壞掉的 APK 裡解出來的 classes.dex」的方法。
364header_size標頭自己的長度,固定為 0x70(112)。底下每個表的位移都是從檔案開頭起算,而真正的資料從這個標頭之後開始,所以出現 0x70 以外的值,代表這個檔案原廠執行環境不會接受。
404endian_tag以這個檔案自己的位元組序寫下的 0x12345678。存成 0x78563412 的檔案是大端序,規格允許但實際上等於看不到;它的實用價值是在讀任何位移之前,先證明解析器的位元組序判斷是對的。
448link_size + link_off兩個 4 位元組欄位,給靜態連結資料用。所有正常工具鏈都不用,所以兩個都是 0;非零的話,強烈暗示這個檔案不是 d8 或 dx 產生的。
524map_offmap_list 的位移,那是一張列出檔案裡每個區段及其型別碼、數量與位移的表。它是 DEX 裡最接近目錄的東西,工具要找標頭本身沒有指名的區段(例如 call site 與 method handle)就是查它。
568string_ids_size + string_ids_off字串識別碼表的數量與位移——應用程式裡每個類別名稱、方法名稱、欄位名稱與字面值,以 modified UTF-8 儲存。要快速掌握狀況,這張表是 DEX 裡最有用的東西,因為讀完它,一行反組譯都不用做就知道這個應用程式碰了什麼。
6424type_ids, proto_ids, field_ids位移 64、72、80 各一對數量與位移,分別索引型別描述子、方法原型與欄位參照。每一對都是兩個 4 位元組值、同樣是先大小後位移,而這些 id 表有硬上限——那個逼人用 multidex 的 65536 個方法限制,就來自這些表所用的索引寬度。
8816method_ids + class_defs位移 88 是方法參照表的數量與位移,位移 96 是類別定義表的。任何要列出應用程式類別的工具,入口都是 class_defs 表:每一筆指出型別、父類別、實作的介面、原始檔名稱,以及它的資料放在哪裡。
1048data_size + data_off資料區段的大小與位移,所有長度不定的東西都在裡面:code item、字串位元組、註解、編碼後的陣列。它一路延伸到標頭所描述的檔案結尾,而上面那些 id 表基本上就是它的索引。

怎麼跟長得像的格式分開

容易搞混的怎麼分
Java class 檔兩者都是編譯後的 Java,DEX 也確實是從 class 檔建出來的,但它們沒有共用任何位元組。class 檔開頭是 CA FE BA BE,只裝一個型別,用堆疊式指令集;DEX 開頭是 'dex' 加一個換行,裝著應用程式所有型別,用暫存器式指令集。APK 裡面裝的是 DEX,完全沒有 .class 項目。
Android APK大家最常搞錯的關係:DEX 不是壓縮檔,APK 也不是 bytecode。APK 才是那個 ZIP;classes.dex、classes2.dex 之類的是裡面的項目。位移 0 讀到 50 4B 03 04 就代表你手上是那個套件,DEX 的 magic 要再往裡面、在每個項目解壓縮後的資料開頭才會出現。
odex/vdex/oat裝置上預先編譯出來的產物是各自獨立、各有 magic 的格式,不是「多加了欄位的 DEX」,儘管工具常常籠統地叫它們「最佳化過的 dex」。從裝置上撈一個出來、指望 DEX 解析器讀得動,是行不通的;解析器要的是 APK 裡面那個 classes.dex。
Multidex classes2.dex它不是另一種格式——標頭一樣、版本一樣——只是同一個 APK 裡的第二個檔案,因為方法 id 表不能超過 65536 筆。只回報第一個檔案裡類別的工具,是讀完 classes.dex 就停了;其他的檔名是 classes2.dex、classes3.dex 這樣往上排,而且不補零。

常見的壞法

  • 編輯之後 checksum 或 signature 沒更新。改一個位元組的 bytecode,位移 8 的 Adler-32 和位移 12 的 SHA-1 就不再描述這個檔案,執行環境會在安裝時拒收。每個正經的 DEX 改寫工具都會把兩個都重算,這就是為什麼手改 DEX 幾乎沒有成功過。
  • 被截斷。位移 32 的 file_size 說明這個檔案應該多長,所以解壓縮失敗導致缺一截的 DEX,不用解析任何東西、看 4 個位元組就抓得到。不然的症狀就是某個表的位移指到檔案結尾之外。
  • 插入內容之後表的位移全部對不上。每張表都是用「從檔案開頭起算的絕對位移」定位的,所以插入或移除一個字串就會讓後面所有東西位移,而沒有整份重寫的標頭會把解析器送進另一張表的中間。它會讀到一個看似合理的數量,然後卡在一堆亂七八糟的東西上。
  • 版本比裝置還新。這不是損壞,但表現得像壞檔案:APK 用的 DEX 版本超過執行環境支援的範圍,安裝失敗時跳的訊息是說檔案無效、而不是說版本太新,而真正的答案在位移 4 那三個 ASCII 數字。

這個格式現在還在用嗎

Android 真正在跑的就是 DEX。每個 APK 裡至少有一個 classes.dex,由 d8 從 javac 或 Kotlin 編譯器吐出來的 class 檔產生,ART 則是直接解譯它、或在裝置上預先編譯。它在 Android 之外沒有生命——沒有別的東西吃它,也沒有別的平台採用它——但在 Android 裡面躲不掉:程式碼縮減、混淆,以及那個逼人用 multidex 的 65536 個方法上限,全都是這個格式的表帶來的後果。做分析的話,它比 strip 過的原生執行檔好處理太多,因為就算應用程式做了混淆,字串表與型別表照樣把類別名稱、方法名稱與字面值明明白白攤出來——這也是為什麼理解一個 APK 比理解同等規模的 ELF 快得多。

在這個站上打開 Android DEX 檔

這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。

相關格式

容易和 Android DEX 搞混的格式,可以比對它們的開頭位元組。

格式開頭位元組副檔名
Android APK50 4B 03 04.apk
Java class 檔CA FE BA BE.class
ELF 執行檔7F 45 4C 46.elf

常見問題

檔案的 magic number 是什麼?

檔案開頭的前幾個位元組,不管檔名寫什麼,它標明的是真正的格式。PNG 一定以 89 50 4E 47 開頭,ZIP 則是 50 4B 03 04。作業系統、瀏覽器和 file 指令讀的都是這幾個位元組,而不是相信副檔名 — 這就是為什麼把檔案改名並不會改變它實際上是什麼。

我要怎麼自己檢查 magic number?

在 macOS 或 Linux 上,xxd -l 16 檔名 會印出前 16 個位元組;在 Windows 上,PowerShell 的 Format-Hex -Path 檔名 -Count 16。或者把檔案丟進本站的檔案類型辨識工具,它會在本機讀取位元組並告訴你是什麼格式,不會上傳任何東西。

為什麼兩種格式的 magic number 會一樣?

因為其中一種是建立在另一種之上。docx、xlsx、pptx、apk、jar 與 epub 骨子裡都是 ZIP 壓縮檔,所以全都以 ZIP 的簽章開頭;要分辨它們得看裡面的項目。Java class 檔和 macOS 的通用二進位檔都以 CA FE BA BE 開頭,靠緊接其後的欄位區分。

有完全沒有 magic number 的檔案嗎?

有。XML、HTML、JSON、SVG、CSV 這類以文字為基礎的格式沒有固定的位元組簽章,所以改用結構來辨識:開頭的 <?xml 宣告、<svg 元素,或是 { 後面接著帶引號的鍵。這種偵測是合理推測而非確定,因此本參考頁會把這些格式標示為結構偵測。

magic number 相符就代表檔案安全嗎?

不代表。它告訴你的是格式,不是內容是否無害 — 一個貨真價實的 PDF 或 docx 仍然可能夾帶惡意內容。辨識真實類型是有用的第一道檢查,尤其在副檔名和位元組不一致的時候,但它不是惡意軟體掃描。

手上有檔案可能是 Android DEX?用檔案類型辨識工具查出來 — 它在本機讀取 magic number,不上傳任何東西。或者瀏覽所有格式。