はじめに
最近Android開発を始めて、絶賛Kotlin勉強中の者です。今回ソースコードを難読化してbuildすることがあり、本当に難読化できているのかを確かめてみたかったので、デコンパイルすることにしました。ただ、なかなか思うようにデコンパイルができなかったので、成功した方法を備忘録として残しておきます。
今回は、下図の通り、build前のソースコードをKotlinで書いたので、.kt
ファイルをbuildして.apk
ファイルになったAndroidのコードをデコンパイルし、.java
ファイルを作成しました。
環境・バージョン
- macOS Catalina バージョン 10.15.6
失敗した方法
調べるとよく出てくるdex2jar
とjad
を使う方法です。
この方法が定番なのかなと思ったので、この方法でデコンパイルをやってみましたが、わたしの場合上手くいきませんでした。
dex2jar
やファイルを展開する手順を踏むことで、.class
ファイルが生成されます。
手順詳細はこちらへ。
その後、jad
を使って.class
を.java
にデコンパイルします。
$ jad hoge.class
すると、次のようなエラーが出て、デコンパイルが進まなくなってしまいました。
Bad CPU type in executable
考えられる原因
調べた限り、確実な情報は出てこなかったので、推測です。
- 現在のMac OSでは64bitのアプリケーションしか動作しないが、jadは32bitのツール?
- jadは少し前からアップデートが止まっているみたい?
上記みたいなところが原因かなと思います。
きっと他にツールがあるはず、諦めます!
成功した方法
jadx
を使う方法を使うと簡単にデコンパイルすることができました。
この方法はJDKが入っていなくても実行できます。
インストール
macだとHomebrewを使って簡単にインストールすることができます。
$ brew install jadx
実行
実行はこのコマンドのみ!
特にPATHを通すこともなく、どこからでも開くことができます。
$ jadx-gui
実行すると、下記のような画面が開くので、デコンパイルしたいapkファイルを開きます。
これでデコンパイルは完了です。
非常に簡単ですね!
実行結果
元のコード(.kt)
package com.example.testqiita
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var message = findViewById<TextView>(R.id.text)
val buttonEnglish = findViewById<Button>(R.id.button_en)
val buttonJapanese = findViewById<Button>(R.id.button_jp)
buttonEnglish.setOnClickListener {
message.setText("Hello World!")
}
buttonJapanese.setOnClickListener {
message.setText("こんにちは 世界!")
}
}
}
デコンパイルしたコード(.java)
ファイルを開くと、下記のようなディレクトリ構成で出力されます。
MainActivity
は下記のように3つのファイルに分かれていますが、同じ処理が読み取れることがわかります。
package com.example.testqiita;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import kotlin.Metadata;
import kotlin.jvm.internal.Ref;
@Metadata(bv = {1, 0, 3}, d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0012\u0010\u0003\u001a\u00020\u00042\b\u0010\u0005\u001a\u0004\u0018\u00010\u0006H\u0014¨\u0006\u0007"}, d2 = {"Lcom/example/testqiita/MainActivity;", "Landroidx/appcompat/app/AppCompatActivity;", "()V", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "app_debug"}, k = 1, mv = {1, 1, 16})
/* compiled from: MainActivity.kt */
public final class MainActivity extends AppCompatActivity {
/* access modifiers changed from: protected */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView((int) R.layout.activity_main);
Ref.ObjectRef message = new Ref.ObjectRef();
message.element = (TextView) findViewById(R.id.text);
((Button) findViewById(R.id.button_en)).setOnClickListener(new MainActivity$onCreate$1(message));
((Button) findViewById(R.id.button_jp)).setOnClickListener(new MainActivity$onCreate$2(message));
}
}
package com.example.testqiita;
import android.view.View;
import android.widget.TextView;
import kotlin.Metadata;
import kotlin.jvm.internal.Ref;
@Metadata(bv = {1, 0, 3}, d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\u0010\u0000\u001a\u00020\u00012\u000e\u0010\u0002\u001a\n \u0004*\u0004\u0018\u00010\u00030\u0003H\n¢\u0006\u0002\b\u0005"}, d2 = {"<anonymous>", "", "it", "Landroid/view/View;", "kotlin.jvm.PlatformType", "onClick"}, k = 3, mv = {1, 1, 16})
/* compiled from: MainActivity.kt */
final class MainActivity$onCreate$1 implements View.OnClickListener {
final /* synthetic */ Ref.ObjectRef $message;
MainActivity$onCreate$1(Ref.ObjectRef objectRef) {
this.$message = objectRef;
}
public final void onClick(View it) {
((TextView) this.$message.element).setText("Hello World!");
}
}
package com.example.testqiita;
import android.view.View;
import android.widget.TextView;
import kotlin.Metadata;
import kotlin.jvm.internal.Ref;
@Metadata(bv = {1, 0, 3}, d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\u0010\u0000\u001a\u00020\u00012\u000e\u0010\u0002\u001a\n \u0004*\u0004\u0018\u00010\u00030\u0003H\n¢\u0006\u0002\b\u0005"}, d2 = {"<anonymous>", "", "it", "Landroid/view/View;", "kotlin.jvm.PlatformType", "onClick"}, k = 3, mv = {1, 1, 16})
/* compiled from: MainActivity.kt */
final class MainActivity$onCreate$2 implements View.OnClickListener {
final /* synthetic */ Ref.ObjectRef $message;
MainActivity$onCreate$2(Ref.ObjectRef objectRef) {
this.$message = objectRef;
}
public final void onClick(View it) {
((TextView) this.$message.element).setText("こんにちは 世界!");
}
}
おわりに
いろいろ躓きましたが、結果的に簡単にデコンパイルできることがわかりました。こんなに簡単にソースの中身を見ることってできるんですね。びっくりです。同時に、難読化や堅牢化の必要性も改めて感じました。
堅牢化できるツールってすごく高価なようですね。。なので、リバースエンジニアリングされることを想定して被害が最小限で済むような設計をする必要がありますし、そのあたりは今後勉強しないといけないところだなと思いました。
ちなみに、Windowsでもjadxは使えるそうです!(Windows環境がなかったので、試せていません。)
失敗した方法(dex2jar、jad)・手順詳細
それぞれのツールのインストール、デコンパイルの方法の詳細を一応残しておきます。
dex2jar
dex2jarのインストールとPATHの設定を行います。
このツールは.dex
を.jar
(.class
を圧縮したファイル)に変換するために使うものです。
$ brew install dex2jar
$ echo 'export PATH="$PATH:/usr/local/Cellar/dex2jar/2.0/bin"' >> ~/.bash_profile
$ source ~/.bash_profile
jad
jadのインストールを行います。
このツールは.class
を.java
に変換するために使うものです。
インストールに必要なcaskは、brewに標準で組み込まれるようになったそうです。
$ brew install homebrew/cask/jad
ターミナルでJDKを使えるようにする
d2j-dex2jar
を使うにあたりJDKをインストールする必要があるようです。ただ、今回はJDKをOracleのサイトからインストールするのではなく、Android studioで使っているJDKをターミナルで使えるようにするために、PATHを通しました。
$ echo 'export PATH=$PATH:/Applications/"Android Studio.app"/Contents/jre/jdk/Contents/Home/bin' >> ~/.bash_profile
$ echo 'export JAVA_HOME=/Applications/"Android Studio.app"/Contents/jre/jdk/Contents/Home' >> ~/.bash_profile
$ source ~/.bash_profile
デコンパイル
apkファイルを展開します。
$ unzip app-debug.apk
d2j-dex2jar
を使って生成されたclasses.dex
を.jar
に変換します。
$ d2j-dex2jar classes.dex
classes-dex2jar.jar
が生成されるので、展開します。
$ unzip classes-dex2jar.jar
(もともと難読化されているのかを確認していたので、気づかなかったのですが、このあたりで難読化設定を入れておくとMainActivity.class
が見つかるのですが、入れてないと見つからないという不思議な状況になっていました。)
とりあえず続きを進めてみます。
jad
を使って.class
を.java
にデコンパイルを行います。
$ jad hoge.class
この手順で、本来はデコンパイルが完了するはずです。