Androidバイトコードの難読化に関する課題

先日DexLabsの@thuxnder がAndroidの課題を投稿しました。これはDexの逆アセンブラを欺く方法を示すものであり、課題としても概念実証としても興味深いものです。

基本的に彼の戦略では、不透明ではあるものの実際に本物であるブランチ条件を使うことにあります。これがDalvikのfill-array-data-payloadインストラクションである次のインストラクションにジャンプします。fill-array-data-payloadの後は、さらにDalvikのインストラクションがあります。

たいていの逆アセンブラは、1つずつインストラクションを逆アセンブルしていきます。そうして最終インストラクションを、インストラクションではなく意味のない配列データとして理解するのです。こうした難読化戦略はx86上ではすでに使用されていますが、Dalvikの逆アセンブラはまだ初期段階にあるため、私たちのケースではこれが有効でしょう。

詳細は@thuxnderの記事と解説をご覧ください。

しかし、コードを逆アセンブルするアルゴリズムにはいくつか種類があります。例えば、最初のブランチは常に本物であることに気づけば、2つ目のインストラクションは実行されず、結果として後のコードが本物のインストラクションとして解釈されます。

幸いなことに、IDA Proは多かれ少なかれ、コードを正確に理解します。下の図1はdownload()という名前の難読化したメソッドに関してIDA Proが理解している内容を示したものです。

最初のテストとしてif-eqがあります。これは常に本物であり、バイト群にジャンプします。Cキーを打ち、IDA ProにこれらのバイトがDalvikのインストラクションであり、純粋なデータではないと伝えます。図2で違いをご覧ください。

図1
図1.IDA Proで難読化を解除したメソッド

図2
図2.Dalvikのバイトコードが可読となっている状態
課題はパスワードを探すことです(図3をご覧ください)。

図3
図3.課題の起動時メイン画面

この興味深い課題に挑戦させてくれた@thuxnderに感謝します。しかし、以下の部分は楽しみを台なしにしてしまうのでお気をつけください。この課題に挑戦するつもりなら、ここで読むのをやめてまず挑戦してみてください。

DropActivityクラスのメソッドには興味深いコードがあるので、これを見てみましょう。7つのメソッドがあります。down_exec、download、exec、genchecksum、onCreate、onCreateOptionsMenu、testです。

Dexlabの記事でexec()メソッドに触れているため、ここから分析を始めます。難読化を解除されたコードを見ると、このメソッドが下記のことを行うことが示されています。

  1. パスストリング/temp/fileを構築
  2. このアドレスでDexを読み込み
  3. 「bad」と名づけられたクラスを読み込み
  4. そのクラスで「getFlag」という名前のメソッドを取得(これは図には出てきませんが、後から出てきます)
  5. そのメソッドを呼び出し、これが文字列を返す

もちろん、私はこの課題でどのDexファイルが読み込まれるのか非常に興味がありました。しかも、この課題には「dexdropper」: org.dexlabs.poc.dexdropperという名前がついているのです。そこで、download()メソッドを見ました。

このメソッドは「payload.apk」というファイル名を加えたURLを構築します。

invoke-direct                   {v10, v8}, (ref) imp. @ _def_String__init_@VL>
new-instance                    v11,
new-instance                    v12,
invoke-static                   {v10},
move-result-object              v13
invoke-direct                   {v12, v13}, (ref) imp. @ _def_StringBuilder__init_@VL>
const-string                    v13, aPayload_apk # "payload.apk"
invoke-virtual                  {v12, v13},

しかし、このURLの始まりはどこでしょうか? 少し前で解読されています。

; initializing the encrypted array data
  new-array                       v7, v11,
  fill-array-data                 v7, encrypted_arraydata
..
; decrypting each byte of the URL.
; v5 is the loop index: goes from 0 to the length of the array
; v7 is the encrypted array
loop_decrypt_url:
  array-length                    v11, v7
  if-lt                           v5, v11, decrypt_url
..
; performs an XOR with 0x23
decrypt_url:
  aget-byte                       v11, v7, v5
  xor-int/lit8                    v11, v11, 0x23
  int-to-byte                     v11, v11
  aput-byte                       v11, v8, v5
  add-int/lit8                    v5, v5, 1
  goto                            loop_decrypt_url

図4では、簡単なIDCスクリプトを使って暗号化された配列をXORし、クリアテキストのURL http://www.dexlabs.org/poc/を表示します。

図4
図4.プレーンテキストのURLを表示するためにXORスクリプトを適用

そこで、http://www.dexlabs.org/poc/payload.apkから別のパッケージをダウンロードし、もちろんそれを分析します。実際ペイロードは非常に小さく、「payload has been downloaded and executed(ペイロードはダウンロードされ、実行されました)」という文字列を返すbad.getFlag()メソッド以外、興味深い情報は何も入っていません。これは課題であって、マルウェアではありませんからね。良かったです。

つまり、基本的にこの課題はペイロードをダウンロード、それを実行、そしてその文字列を表示するようにつくられていることがわかりました。しかし、どうすればこの機能を引き出すことができるのでしょうか?当然、パスワードを見つけなければなりません。

genchecksum()を見てみると、ファイルのMD5ハッシュを計算し、返しているのがわかります。この時点では、MD5はどうでもいいものであると言わざるを得ません。しかし、これはチェックサムではなくハッシュ機能です。それでも、genchecksum()の呼び出し元はdown_exec()です。このメソッドが情報を総合して、ペイロードをダウンロードし(download())、MD5ハッシュを計算し(genchecksum())、そしてハッシュが期待値と一致した場合にはそれを実行します(exec())。

パスワードはどこでしょうか? 正直、私はその後、test()メソッドを分析しました。test機能は怪しく見えてしまうのです。

図5
図5.test()メソッドの難読化を解除したコード

図5の最上部はバイト配列に書かれている暗号化データを示しています。そして、このバイト配列は解読されています。単純なXORではなく、連鎖したXORです。最初のバイトを0×00でXORすると、他のすべてのバイトが前の暗号化したバイトと連鎖します。暗号理論において、これは暗号ブロック連鎖(Cipher-block chaining、CBC)と呼ばれています。

簡単なXORプログラムを書いて、値を解読してみました。

http://www.dexlabs.org/poc/flag_dexloaderobf_07201

そのファイルをダウンロードし、次のメッセージを読み込みました。

$ cat flag_dexloaderobf_07201
this is not the password you are looking for ;)

if you are interested in Android RE and obfuscation techniques, send us an email
info@dexlabs.org

ああ! これはパスワードではないということですね。

図5をもう一度見てみましょう。配列がいったん解読されると、プレーンテキストがv1に置かれます。ですが、使用されていません。しかしこのメソッドは私たちがテキストフィールドに入力したパスワードを取得し、DropActivity_exi__に保管されている想定値と比較します。

このフィールドはクラスコンストラクタ - 5バイト配列 - でfillが実行されていますが暗号化はされていません。

const/4                         v0, 5
new-array                       v0, v0,
fill-array-data                 v0, arraydata_3597C
iput-object                     v0, this, DropActivity_exi___

onCreate()メソッドで解読されています。ここでもアルゴリズムはパスワードの長さ(つまり0×5)とともにXORにあります。

loop_decrypt_pass:
  iget-object                     v1, this, DropActivity_exi___
  array-length                    v1, v1
  if-lt                           v0, v1, decrypt_password ; decrypt each byte
  ...

decrypt_password:
  iget-object                     v1, this, DropActivity_exi___
  iget-object                     v2, this, DropActivity_exi___
  aget-byte                       v2, v2, v0
  iget-object                     v3, this, DropActivity_exi___
  array-length                    v3, v3
  xor-int/2addr                   v2, v3 ; XOR with the length of the array
  int-to-byte                     v2, v2
  aput-byte                       v2, v1, v0
  add-int/lit8                    v0, v0, 1 ; increment counter
  goto                            loop_decrypt_pass

つまり、0×05で暗号文0×73, 0x6a, 0×62, 0×64, 0×77をXORしなければならないということです。結果は「vogar」でした。これはアイスランドにある小さな街の名前です。

図6
図6.課題が解けました!

楽しさはさておき、この課題により、ほとんどの逆アセンブラを欺く簡単な方法が示されています。IDA Proは完全には欺かれていませんが、この課題が公開されたので、Androguardにはパッチが適用されました。

拍手!

- the Crypto Girl

7月30日最新情報:@thuxnderのおかげで最新情報があります。重要なDalvikインストラクションはfill-array-data-payloadです(fill-array-dataではありません)。