Android DEX ファイル形式
Dalvik Executable 形式 — Android ランタイム向けのコンパイル済みバイトコード。
- MIME タイプ
- application/octet-stream
- 拡張子
- .dex
- カテゴリ
- 実行ファイル・バイトコード
マジックナンバー(ファイル署名)
| 0x00000000 | 6465780A | dex· | 'dex' + newline |
Android DEX 形式について
DEX ファイルは、Android アプリのコンパイル済みクラスをすべて 1 つのファイルに統合したものです。それこそがこの形式の狙いでした。小さな class ファイルが何千個もあって、それぞれが触れる型の名前を何度も書き直すかわりに、DEX は共有のテーブル(文字列、型、プロトタイプ、フィールド、メソッド)を 1 組だけ持ち、各クラス定義はそこへインデックスで参照します。ヘッダは 112 バイト固定で、それらのテーブルを指すサイズとオフセットが並ぶので、読み手は必要なテーブルへ直接飛べます。バイトコード自体はスタックベースではなくレジスタベースで、DEX の命令が JVM のものとまったく似ていないのはそのためです。
ヘッダーの構造(バイト単位)
| オフセット | 長さ | フィールド | 意味 |
|---|---|---|---|
| 0 | 8 | magic (DEX_FILE_MAGIC) | 64 65 78 0A の 4 バイト、続いて ASCII の数字 3 桁、そして NUL。たとえば 'dex\n039\0' です。このバージョン数字には意味があって、035 は Android 7 より前、037 が Android 7 とデフォルトメソッド、038 が Android 8、039 が Android 9、040 が Android 10。端末は自分の知っているより新しいバージョンの DEX を拒否します。 |
| 8 | 4 | checksum | このフィールドより後ろのファイル全体に対する Adler-32。ランタイムが最初に検証するのがこれなので、DEX のどこを編集しても最後にこの値を計算し直さなければなりません。1 バイトを手でパッチすると、インストールできないファイルができあがるのはそのためです。 |
| 12 | 20 | signature | これより後ろすべての SHA-1。セキュリティ用の署名ではなく、ランタイムやビルドキャッシュが「2 つの DEX が同一である」と気づくための内容識別子です。これも変更のたびに計算し直す必要があります。 |
| 32 | 4 | file_size | DEX 全体のバイト数。実際のファイル長と比べるのが最も安上がりな切り詰めの検査であり、壊れた APK から取り出した classes.dex を捕まえるのもこれです。 |
| 36 | 4 | header_size | ヘッダ自身の長さで、0x70(112)に固定されています。以下のテーブルのオフセットはすべてファイル先頭からの位置であり、データ本体はこのヘッダの後ろから始まります。0x70 以外の値なら、素のランタイムが受け付ける DEX ではありません。 |
| 40 | 4 | endian_tag | 0x12345678 を、そのファイル自身のバイト順で書いたもの。0x78563412 が入っているファイルはビッグエンディアンで、仕様上は合法ながら実際にはまず見かけません。実用上の役割は、オフセットを読み始める前にパーサーのバイト順が正しいことを証明することです。 |
| 44 | 8 | link_size + link_off | 静的リンクされたデータ用の 4 バイトフィールドが 2 つ。まともなツールチェーンはどれも使わないので両方ゼロです。非ゼロなら、d8 でも dx でもない何かが作ったファイルだという強い手掛かりになります。 |
| 52 | 4 | map_off | map_list のオフセット。ファイル内の全セクションを型コード・個数・オフセットとともに並べたテーブルで、DEX における目次に一番近いものです。call site やメソッドハンドルのように、ヘッダ自身が名前を持たないセクションを探すときはここを見ます。 |
| 56 | 8 | string_ids_size + string_ids_off | 文字列識別子テーブルの個数とオフセット。アプリ内のクラス名・メソッド名・フィールド名・リテラルがすべて、modified UTF-8 で入っています。切り分けの用途では DEX で断然役に立つテーブルで、これを読むだけで、1 行も逆アセンブルせずにアプリが何に触れているかが分かります。 |
| 64 | 24 | type_ids, proto_ids, field_ids | オフセット 64、72、80 にある、さらに 3 組の個数とオフセットのペアで、型ディスクリプタ、メソッドのプロトタイプ、フィールド参照を索引します。どれも 4 バイト 2 つが「サイズ→オフセット」の同じ順で並びます。id テーブルには厳しい上限があり、multidex を強いる 65536 メソッド制限は、これらのテーブルが使うインデックス幅に由来します。 |
| 88 | 16 | method_ids + class_defs | オフセット 88 にメソッド参照テーブルの個数とオフセット、96 にクラス定義テーブルの個数とオフセット。アプリのクラスを列挙する処理はどれも class_defs テーブルから始まります。各エントリが型・スーパークラス・インターフェース・ソースファイル・データの所在を示します。 |
| 104 | 8 | data_size + data_off | データセクションのサイズとオフセット。可変長のものはすべてここに入ります(コードアイテム、文字列の実体、アノテーション、符号化された配列)。ヘッダが記述するファイルの末尾まで続き、上の id テーブル群は事実上このセクションへの索引です。 |
似た形式との見分け方
| 紛らわしい形式 | 見分け方 |
|---|---|
| Java class ファイル | どちらもコンパイル済みの Java で、DEX は class ファイルから作られますが、バイトは 1 つも共有していません。class ファイルは CA FE BA BE で始まり、スタックベースの命令セットで型をちょうど 1 つ持ちます。DEX は 'dex' と改行で始まり、レジスタベースの命令セットでアプリの全型を持ちます。APK に入っているのは DEX で、.class のエントリは 1 つもありません。 |
| Android APK | 人が取り違えるのは両者の関係です。DEX はアーカイブではなく、APK はバイトコードではありません。ZIP なのは APK のほうで、classes.dex や classes2.dex はその中のエントリです。オフセット 0 が 50 4B 03 04 なら手にしているのはパッケージで、DEX のマジックナンバーはもっと奥、各エントリを展開したデータの先頭に現れます。 |
| odex/vdex/oat | 端末上にある事前コンパイル済みの成果物は、フィールドが増えた DEX ではなく、それぞれ独自のマジックナンバーを持つ別形式です(ツールは緩く「最適化された dex」と呼びますが)。端末から吸い出して DEX のパーサーに読ませようとしても通りません。パーサーが欲しいのは APK の中の classes.dex です。 |
| Multidex classes2.dex | 別形式ではありません。ヘッダもバージョンも同一で、メソッド id テーブルが 65536 エントリを超えられないという理由から、同じ APK にもう 1 ファイル置いてあるだけです。最初のファイルのクラスしか報告しないツールは、classes.dex を読んでそこで止まっています。残りは classes2.dex、classes3.dex と、ゼロ埋めなしで続きます。 |
よくある壊れ方
- 編集後にチェックサムや署名が古いまま。バイトコードを 1 バイト変えると、オフセット 8 の Adler-32 とオフセット 12 の SHA-1 がもうそのファイルを説明しなくなるので、ランタイムはインストール時に拒否します。まともな DEX 書き換えツールはどれも両方を計算し直します。DEX を手で編集してもほぼ絶対に動かないのはそのためです。
- 途中切れ。オフセット 32 の file_size がファイルの長さを宣言しているので、展開に失敗して短くなった DEX は、何もパースせずに 4 バイトだけで検出できます。それ以外の症状としては、テーブルのオフセットがファイル末尾より先を指すという形で現れます。
- 挿入後にテーブルのオフセットが合わなくなった。どのテーブルもファイル先頭からの絶対オフセットで位置が決まっているので、文字列を 1 つ挿入・削除しただけで後ろがすべてずれます。ヘッダを完全に書き直していなければ、パーサーは別のテーブルの途中に飛び込み、もっともらしい個数を読んでからでたらめな値で落ちます。
- 端末には古すぎるバージョン。破損ではないのに、壊れたファイルとして現れます。ランタイムが対応する以上の DEX バージョンでビルドされた APK は、バージョンについてではなく「ファイルが無効です」というメッセージでインストールに失敗します。本当の答えはオフセット 4 の ASCII 数字 3 桁にあります。
この形式の現在地
Android が実際に走らせているのは DEX です。どの APK にも classes.dex が最低 1 つ入っていて、javac や Kotlin コンパイラが吐いた class ファイルから d8 が生成したものです。ART はそれを解釈実行するか、端末上で事前コンパイルします。Android の外に生息域はなく、他に読み手はいませんし、他のプラットフォームが採用したこともありません。ただし Android の中では避けようがなく、コードの縮小も難読化も、multidex を強いる 65536 メソッド制限も、すべてこの形式のテーブルから来ています。解析する側から見れば、ストリップされたネイティブバイナリに比べて恵まれた対象です。難読化されたアプリでも、文字列と型のテーブルがクラス名・メソッド名・リテラルを平文で綴っているので、同等の ELF よりはるかに速く APK を理解できます。
Android DEX ファイルをこのサイトで開く
どれもブラウザ内で動きます。ファイルが端末から出ることはありません。
関連する形式
Android DEX と混同しやすい形式です。先頭バイトを見比べてください。
| 形式 | 先頭バイト | 拡張子 |
|---|---|---|
| Android APK | 50 4B 03 04 | .apk |
| Java class ファイル | CA FE BA BE | .class |
| ELF 実行ファイル | 7F 45 4C 46 | .elf |