DEX 版 strings

DEX ファイルから可読文字列を抽出するコードを,ちょっとした必要があって書いた。

もちろん classes.dex に strings をかけるだけでも目的は達成されるが,DEX ファイルには文字列識別子を集めたセクションが存在するので,そこをダンプするとより正確に文字列だけを抽出することができる。

DEX ファイルのフォーマットについては AOSP 公式文書として http://source.android.com/tech/dalvik/dex-format.html を参照すればよい。その他 Dalvik VMバイトコードなどについても公式の http://source.android.com/tech/dalvik/index.html からいくつか資料を読むことができる。

DEX ファイルでは文字列は MUTF-8 という UTF-8 をもとにした形式で保存されている。

  • かならず1〜3バイトのエンコーディングとなっている (4バイト以上のものは使用されていない)
  • そのかわり U+10000U+10FFFF の範囲のユニコード文字はサロゲートペアを用いてエンコードされる
  • 文字列中の U+0000 (NUL) は2バイト (0xC0 0x80) で表記される
  • 逆に単一バイトの NUL (0x00) は文字列終端子として用いられる

前半2つは,Java 由来のバイトコードらしく UTF-16 を基本としていることを示している。
後半2つは,末尾終端子としての 0 とは別に,文字列中に 0 (NUL) を含むことができることを示している。
逆に,これらの特徴をもつため厳密な意味では UTF-8 としては不正なシーケンスをとりうる *1

本プログラムでは,面倒だったので,MUTF-8 エンコーディングの文字列をそのまま出力している。(サロゲートペアを含まない) 通常の範囲内の文字列であれば問題なく端末やエディタで見ることができるであろう。

#!perl
use strict;
use warnings;
use autodie;

open my $file, '<', shift;
parse_handle($file);
close $file;
exit;

sub parse_handle {
    my ($file) = @_;
    my $buffer;

#   seek $file, 0, 0;               # magic
#   read $file, $buffer, 8;

    seek $file, 56, 0;              # string_ids_size
    read $file, $buffer, 4;
    my $string_ids_size = unpack 'V', $buffer;
    read $file, $buffer, 4;
    my $string_ids_off  = unpack 'V', $buffer;

    for my $i (0 .. $string_ids_size - 1) {
        seek $file, $string_ids_off + 4 * $i, 0;
        read $file, $buffer, 4;
        my $string_data_off = unpack 'V', $buffer;

        seek $file, $string_data_off, 0;
        my $str = parse_string_data_item($file);
        print $str, "\n";
    }
}

sub parse_string_data_item {
    my ($file) = @_;
    my $buffer;

    my $utf16_size = parse_uleb128($file);

    my $str = q{};
    while (1) {
        read $file, $buffer, 1;
        my $c = unpack 'C', $buffer;
        last if $c == 0;
        $str .= $buffer;
    }

    return $str;
}

sub parse_uleb128 {
    my ($file) = @_;
    my $n = 0;
    my $buffer;

    while (1) {
        read $file, $buffer, 1;
        my $c = unpack 'C', $buffer;
        $n = ($n << 8) + ($c & 0x7f);
        return $n  if ! ($c & 0x80);
    }
}

*1:とはいえ,ゆるい UTF-8 デコーダなら (サロゲートペアを除いて) 問題なくデコードできるであろう。厳密にはバイトシーケンスから一度 UTF-16 に変換し,ふたたび UTF-8 に変換するほうがよいだろう。