ByteScope

Android DEX ファイル形式

Dalvik Executable 形式 — Android ランタイム向けのコンパイル済みバイトコード。

MIME タイプ
application/octet-stream
拡張子
.dex
カテゴリ
実行ファイル・バイトコード

マジックナンバー(ファイル署名)

0x000000006465780Adex·'dex' + newline

Android DEX 形式について

DEX ファイルは、Android アプリのコンパイル済みクラスをすべて 1 つのファイルに統合したものです。それこそがこの形式の狙いでした。小さな class ファイルが何千個もあって、それぞれが触れる型の名前を何度も書き直すかわりに、DEX は共有のテーブル(文字列、型、プロトタイプ、フィールド、メソッド)を 1 組だけ持ち、各クラス定義はそこへインデックスで参照します。ヘッダは 112 バイト固定で、それらのテーブルを指すサイズとオフセットが並ぶので、読み手は必要なテーブルへ直接飛べます。バイトコード自体はスタックベースではなくレジスタベースで、DEX の命令が JVM のものとまったく似ていないのはそのためです。

ヘッダーの構造(バイト単位)

オフセット長さフィールド意味
08magic (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 を拒否します。
84checksumこのフィールドより後ろのファイル全体に対する Adler-32。ランタイムが最初に検証するのがこれなので、DEX のどこを編集しても最後にこの値を計算し直さなければなりません。1 バイトを手でパッチすると、インストールできないファイルができあがるのはそのためです。
1220signatureこれより後ろすべての SHA-1。セキュリティ用の署名ではなく、ランタイムやビルドキャッシュが「2 つの DEX が同一である」と気づくための内容識別子です。これも変更のたびに計算し直す必要があります。
324file_sizeDEX 全体のバイト数。実際のファイル長と比べるのが最も安上がりな切り詰めの検査であり、壊れた APK から取り出した classes.dex を捕まえるのもこれです。
364header_sizeヘッダ自身の長さで、0x70(112)に固定されています。以下のテーブルのオフセットはすべてファイル先頭からの位置であり、データ本体はこのヘッダの後ろから始まります。0x70 以外の値なら、素のランタイムが受け付ける DEX ではありません。
404endian_tag0x12345678 を、そのファイル自身のバイト順で書いたもの。0x78563412 が入っているファイルはビッグエンディアンで、仕様上は合法ながら実際にはまず見かけません。実用上の役割は、オフセットを読み始める前にパーサーのバイト順が正しいことを証明することです。
448link_size + link_off静的リンクされたデータ用の 4 バイトフィールドが 2 つ。まともなツールチェーンはどれも使わないので両方ゼロです。非ゼロなら、d8 でも dx でもない何かが作ったファイルだという強い手掛かりになります。
524map_offmap_list のオフセット。ファイル内の全セクションを型コード・個数・オフセットとともに並べたテーブルで、DEX における目次に一番近いものです。call site やメソッドハンドルのように、ヘッダ自身が名前を持たないセクションを探すときはここを見ます。
568string_ids_size + string_ids_off文字列識別子テーブルの個数とオフセット。アプリ内のクラス名・メソッド名・フィールド名・リテラルがすべて、modified UTF-8 で入っています。切り分けの用途では DEX で断然役に立つテーブルで、これを読むだけで、1 行も逆アセンブルせずにアプリが何に触れているかが分かります。
6424type_ids, proto_ids, field_idsオフセット 64、72、80 にある、さらに 3 組の個数とオフセットのペアで、型ディスクリプタ、メソッドのプロトタイプ、フィールド参照を索引します。どれも 4 バイト 2 つが「サイズ→オフセット」の同じ順で並びます。id テーブルには厳しい上限があり、multidex を強いる 65536 メソッド制限は、これらのテーブルが使うインデックス幅に由来します。
8816method_ids + class_defsオフセット 88 にメソッド参照テーブルの個数とオフセット、96 にクラス定義テーブルの個数とオフセット。アプリのクラスを列挙する処理はどれも class_defs テーブルから始まります。各エントリが型・スーパークラス・インターフェース・ソースファイル・データの所在を示します。
1048data_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 APK50 4B 03 04.apk
Java class ファイルCA FE BA BE.class
ELF 実行ファイル7F 45 4C 46.elf

よくある質問

ファイルのマジックナンバーとは何ですか?

ファイル先頭の数バイトで、名前が何であろうとその形式を示すものです。PNG は必ず 89 50 4E 47 で始まり、ZIP は 50 4B 03 04 で始まります。OS もブラウザも file コマンドも、拡張子を信用せずこのバイト列を読みます。だからファイル名を変えても中身の正体は変わりません。

マジックナンバーを自分で確認する方法は?

macOS や Linux なら xxd -l 16 ファイル名 で先頭 16 バイトが表示されます。Windows なら PowerShell で Format-Hex -Path ファイル名 -Count 16 です。あるいはこのサイトのファイル形式判別ツールにドロップすれば、ローカルでバイト列を読んで形式を教えてくれます。アップロードはありません。

2 つの形式が同じマジックナンバーを持つのはなぜ?

一方が他方の上に作られているからです。docx、xlsx、pptx、apk、jar、epub はすべて中身が ZIP アーカイブなので ZIP の署名で始まり、区別するには内部のエントリを見る必要があります。Java の class ファイルと macOS のユニバーサルバイナリはどちらも CA FE BA BE で始まり、直後のフィールドで見分けます。

マジックナンバーがまったくないファイルもある?

あります。XML、HTML、JSON、SVG、CSV といったテキストベースの形式には固定のバイト署名がないため、構造から判別します。先頭の <?xml 宣言、<svg 要素、あるいは { に続く引用符付きのキーなどです。これは確定ではなく推測なので、このリファレンスではそうした形式をプローブ判別として示しています。

マジックナンバーが一致すればそのファイルは安全?

いいえ。わかるのは形式であって、中身が無害かどうかではありません。正規の PDF や docx でも悪意のあるペイロードを含んでいることがあります。特に拡張子とバイト列が食い違うときに真の形式を知るのは有用な第一段階ですが、マルウェアスキャンではありません。

Android DEX かもしれないファイルをお持ちですか?ファイル形式判別ツールで調べる — マジックナンバーをローカルで読み取ります。アップロードは一切ありません。またはすべての形式を見る。