Android反コンパイル学習-APKtoolを使用して登録クラスプログラムを解読
#1メカニズム
Androidプログラムを解読する通常の方法は、apkファイルをApkToolで逆コンパイルし、Smali形式の逆アセンブリコードを生成し、Smaliファイルのコードを読んでプログラムの実行メカニズムを理解し、プログラムの突破口を見つけて修正し、最後にApkToolを使って再コンパイルしてapkファイルを生成し署名し、最後にテストを実行し、このようにループします.プログラムが正常に解読されるまで.
2準備作業
APKtoolのインストール使用については筆者の前編ブログを参照してください
ソフトウェアの作成01テスト用
valueフォルダの下にあるString.xmlの内容は次の通りです.
onCreateメソッドは次のように変更されました.
MainActivityにクラスを追加
上のcheckSNで用いたbytes 2 string手法は以下の通りである.
レイアウトファイルは次のとおりです.
#3解読プロセス
逆コンパイルapkファイルが成功すると、現在のoutdirディレクトリの下で一連のディレクトリとファイルが生成されます.ここでsmaliディレクトリにはプログラムのすべての逆アセンブリコードが格納され、resディレクトリはプログラム内のすべてのリソースファイルであり、これらのディレクトリのサブディレクトリとファイルは開発時のソースコードディレクトリの組織構造と一致している.どのように突破口を探すかはプログラムを分析する鍵です.一般的なAndroidでは、エラーメッセージは通常、キーコードを導く風向標であり、エラーメッセージの近くには一般的にプログラムのコア検証コードがあり、アナリストはこれらのコードを読んでソフトウェアの登録プロセスを理解する必要があります.エラーメッセージはAndroidプログラムの文字列リソースです.Androidプログラムを開発する際、これらの文字列はソースコードにハードコーディングされるか、「resvalues」ディレクトリのstrings.xmlファイルから引用される可能性があります.apkファイルがパッケージ化されると、strings.xmlの文字列が暗号化されてresources.arscファイルがapkパッケージに保存されます.apkが逆コンパイルに成功した後、このファイルも復号された.
ソフトウェアの登録に失敗すると、「無効なユーザー名または登録コード」がポップアップされ、キーコードを探します.「resvaluesstring.xml」ファイルを開きます.内容は以下の通りです. Crackme0201 Hello world!name="menu_settings">Settings name="title_activity_main">crackme 02 Androidプログラム解読プレゼンテーションインスタンス ユーザー名: 登録コード: 登録 16ビットの登録コード プログラム未登録 プログラム登録 無効なユーザー名またはブックコードおめでとうございます!Androidプログラムの開発に成功した を登録した場合、String.xmlファイルのすべての文字列リソースは「gen//R.java」ファイルのStringクラスで識別され、各文字列には一意のintタイプインデックス値があり、Apktoolを使用してapkファイルを逆コンパイルした後、すべてのインデックス値はstring.xmlファイルとディレクトリの下のpublic.xmlファイルに保存されます.
unsuccessedのid値は0 x 7 f 05000 cであり、smaliディレクトリで0 x 7 f 05000 cの内容を含むファイルを検索したところ、MainActivity$1.smaliファイルのみが呼び出され、コードは以下の通りである:#virtual methods.method public onClick(Landroid/view/View;)V.locals 4.parameter"v".prologue const/4 v 3 0x0 …… .line 32 #calls: Lcom/droider/crackme0201/MainActivity;->checkSN(Ljava/lang/String; Ljava/lang/String;)Z invoke-static {v0, v1, v2}, Lcom/droider/crackme0201/MainActivity;-> #登録コードが正当なaccess$2(Lcom/droider/crackme 0201/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z move-result v 0 if-nez v 0、:cond_0#結果が0でない場合はcond_にジャンプ0ラベルで.line 34 iget-object v 0,p 0,Lcom/droider/crackme 0201/MainActivity$1;this$0:Lcom/droider/crackme0201/MainActivity; .line 35 const v 1,0 x 7 f 05000 c#unsuccessed文字列.line 34 invoke-static{v 0,v 1,v 3},Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast; move-result-object v0 .line 35 invoke-virtual {v0}, Landroid/widget/Toast;->show()V .line 42 :goto_0 return-void .line 37 :cond_0 iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;-> this$0:Lcom/droider/crackme0201/MainActivity; .line 38 const v 1,0 x 7 f 05000 d#successed文字列.line 37 invoke-static{v 0,v 1,v 3},Landroid/widget/Tast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast; move-result-object v0 .line 38 invoke-virtual {v0}, Landroid/widget/Toast;->show()V .line 39 iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;-> this$0:Lcom/droider/crackme0201/MainActivity; #getter for: Lcom/droider/crackme0201/MainActivity;->btn_register:Landroid/widget/Button; invoke-static {v0}, Lcom/droider/crackme0201/MainActivity;-> access$3(Lcom/droider/crackme0201/MainActivity;)Landroid/widget/Button; move-result-object v0 invoke-virtual {v0, v3}, Landroid/widget/Button;->setEnabled(Z)V#設定登録ボタンは使用できません.line 40 iget-object v 0,p 0,Lcom/droider/crackme 0201/MainActivity$1;this$0:Lcom/droider/crackme0201/MainActivity; const v 1,0 x 7 f 05000 b#registered文字列、アナログ登録成功invoke-virtual{v 0,v 1}、Lcom/droider/crackme 0201/MainActivity;setTitle(I)V goto :goto_0.end method Smaliコードに追加されたコメントは、井戸番号「#」で始まり、「.line 32」行はcheckSN()関数を呼び出して登録コードの合法的なチェックを行い、次の2行のコードがあります:move-result v 0 if-nez v 0,:cond_0 checkSN()関数はBooleanタイプの値を返します.ここでの第1行コードは、返された結果をv 0レジスタに保存し、第2行コードはv 0を判断し、v 0の値がゼロでない場合、すなわち条件が真である場合、cond_にジャンプする0番で、逆に、プログラムは順調に下へ実行されます.解析により,「.line 32」行のコード「if-nez v 0,:cond_0」がプログラムの解読点であることが分かった.if-nezはDalvik命令セットの条件ジャンプ命令であり、if-eqz、if-gez、if-lezなどがある.これらの命令は本書第3章で紹介されるが,if-nez命令機能とは逆の命令がif-eqzであり,比較結果が0または等しい場合にジャンプすることを示す.任意のテキストエディタでMainActivity$1.smaliファイルを開き、「.line 32」行のコード「if-nezv 0,:cond_0」を「if-eqz v 0,:cond_0」に変更し、保存後に終了します.コードは修正が完了しても終了します.Smaliファイルコードを修正した後、修正したファイルを再コンパイルしてapkファイルにパッケージし、署名してインストールし、勝手に入力する必要があり、登録に成功したことがわかります.
Androidプログラムを解読する通常の方法は、apkファイルをApkToolで逆コンパイルし、Smali形式の逆アセンブリコードを生成し、Smaliファイルのコードを読んでプログラムの実行メカニズムを理解し、プログラムの突破口を見つけて修正し、最後にApkToolを使って再コンパイルしてapkファイルを生成し署名し、最後にテストを実行し、このようにループします.プログラムが正常に解読されるまで.
2準備作業
APKtoolのインストール使用については筆者の前編ブログを参照してください
ソフトウェアの作成01テスト用
valueフォルダの下にあるString.xmlの内容は次の通りです.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Crackme0201</string>
<string name="hello_world">Hello world!</string>
<string name="menu_settings">Settings</string>
<string name="title_activity_main">crackme02</string>
<string name="info">Android </string>
<string name="username"> : </string>
<string name="sn"> : </string>
<string name="register"> </string>
<string name="hint_username"> </string>
<string name="hint_sn"> 16 </string>
<string name="unregister"> </string>
<string name="registered"> </string>
<string name="unsuccessed"> </string>
<string name="successed"> ! </string>
</resources>
onCreateメソッドは次のように変更されました.
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle(R.string.unregister); //
edit_userName = (EditText) findViewById(R.id.edit_username);
edit_sn = (EditText) findViewById(R.id.edit_sn);
btn_register = (Button) findViewById(R.id.button_register);
btn_register.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (!checkSN(edit_userName.getText().toString().trim(), edit_sn
.getText().toString().trim())) {
Toast.makeText(MainActivity.this, //
R.string.unsuccessed, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, //
R.string.successed, Toast.LENGTH_SHORT).show();
btn_register.setEnabled(false);
setTitle(R.string.registered); //
}
}
});
}
MainActivityにクラスを追加
private boolean checkSN(String userName, String sn) {
try {
if ((userName == null) || (userName.length() == 0))
return false;
if ((sn == null) || (sn.length() != 16))
return false;
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.reset();
digest.update(userName.getBytes());
byte[] bytes = digest.digest(); // MD5 Hash
String hexstr = bytes2HexString(bytes); //
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hexstr.length(); i += 2) {
sb.append(hexstr.charAt(i));
}
String userSN = sb.toString(); // SN
if (!userSN.equalsIgnoreCase(sn)) //
return false;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false;
}
return true;
}
上のcheckSNで用いたbytes 2 string手法は以下の通りである.
public String bytes2HexString(byte[] b) {
byte[] hex ="0123456789ABCDEF".getBytes();
byte[] buff = new byte[2 * b.length];
for (int i = 0; i < b.length; i++) {
buff[2 * i] = hex[(b[i] >> 4) & 0x0f];
buff[2 * i + 1] = hex[b[i] & 0x0f];
}
return new String(buff);
}
レイアウトファイルは次のとおりです.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/root">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="android 1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" :" />
<EditText
android:id="@+id/edit_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10" >
</EditText>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" :" />
<EditText
android:id="@+id/edit_sn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10" />
</LinearLayout>
<Button
android:id="@+id/button_register"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=" " />
</LinearLayout>
#3解読プロセス
逆コンパイルapkファイルが成功すると、現在のoutdirディレクトリの下で一連のディレクトリとファイルが生成されます.ここでsmaliディレクトリにはプログラムのすべての逆アセンブリコードが格納され、resディレクトリはプログラム内のすべてのリソースファイルであり、これらのディレクトリのサブディレクトリとファイルは開発時のソースコードディレクトリの組織構造と一致している.どのように突破口を探すかはプログラムを分析する鍵です.一般的なAndroidでは、エラーメッセージは通常、キーコードを導く風向標であり、エラーメッセージの近くには一般的にプログラムのコア検証コードがあり、アナリストはこれらのコードを読んでソフトウェアの登録プロセスを理解する必要があります.エラーメッセージはAndroidプログラムの文字列リソースです.Androidプログラムを開発する際、これらの文字列はソースコードにハードコーディングされるか、「resvalues」ディレクトリのstrings.xmlファイルから引用される可能性があります.apkファイルがパッケージ化されると、strings.xmlの文字列が暗号化されてresources.arscファイルがapkパッケージに保存されます.apkが逆コンパイルに成功した後、このファイルも復号された.
ソフトウェアの登録に失敗すると、「無効なユーザー名または登録コード」がポップアップされ、キーコードを探します.「resvaluesstring.xml」ファイルを開きます.内容は以下の通りです.
unsuccessedのid値は0 x 7 f 05000 cであり、smaliディレクトリで0 x 7 f 05000 cの内容を含むファイルを検索したところ、MainActivity$1.smaliファイルのみが呼び出され、コードは以下の通りである:#virtual methods.method public onClick(Landroid/view/View;)V.locals 4.parameter"v".prologue const/4 v 3 0x0 …… .line 32 #calls: Lcom/droider/crackme0201/MainActivity;->checkSN(Ljava/lang/String; Ljava/lang/String;)Z invoke-static {v0, v1, v2}, Lcom/droider/crackme0201/MainActivity;-> #登録コードが正当なaccess$2(Lcom/droider/crackme 0201/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z move-result v 0 if-nez v 0、:cond_0#結果が0でない場合はcond_にジャンプ0ラベルで.line 34 iget-object v 0,p 0,Lcom/droider/crackme 0201/MainActivity$1;this$0:Lcom/droider/crackme0201/MainActivity; .line 35 const v 1,0 x 7 f 05000 c#unsuccessed文字列.line 34 invoke-static{v 0,v 1,v 3},Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast; move-result-object v0 .line 35 invoke-virtual {v0}, Landroid/widget/Toast;->show()V .line 42 :goto_0 return-void .line 37 :cond_0 iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;-> this$0:Lcom/droider/crackme0201/MainActivity; .line 38 const v 1,0 x 7 f 05000 d#successed文字列.line 37 invoke-static{v 0,v 1,v 3},Landroid/widget/Tast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast; move-result-object v0 .line 38 invoke-virtual {v0}, Landroid/widget/Toast;->show()V .line 39 iget-object v0, p0, Lcom/droider/crackme0201/MainActivity$1;-> this$0:Lcom/droider/crackme0201/MainActivity; #getter for: Lcom/droider/crackme0201/MainActivity;->btn_register:Landroid/widget/Button; invoke-static {v0}, Lcom/droider/crackme0201/MainActivity;-> access$3(Lcom/droider/crackme0201/MainActivity;)Landroid/widget/Button; move-result-object v0 invoke-virtual {v0, v3}, Landroid/widget/Button;->setEnabled(Z)V#設定登録ボタンは使用できません.line 40 iget-object v 0,p 0,Lcom/droider/crackme 0201/MainActivity$1;this$0:Lcom/droider/crackme0201/MainActivity; const v 1,0 x 7 f 05000 b#registered文字列、アナログ登録成功invoke-virtual{v 0,v 1}、Lcom/droider/crackme 0201/MainActivity;setTitle(I)V goto :goto_0.end method Smaliコードに追加されたコメントは、井戸番号「#」で始まり、「.line 32」行はcheckSN()関数を呼び出して登録コードの合法的なチェックを行い、次の2行のコードがあります:move-result v 0 if-nez v 0,:cond_0 checkSN()関数はBooleanタイプの値を返します.ここでの第1行コードは、返された結果をv 0レジスタに保存し、第2行コードはv 0を判断し、v 0の値がゼロでない場合、すなわち条件が真である場合、cond_にジャンプする0番で、逆に、プログラムは順調に下へ実行されます.解析により,「.line 32」行のコード「if-nez v 0,:cond_0」がプログラムの解読点であることが分かった.if-nezはDalvik命令セットの条件ジャンプ命令であり、if-eqz、if-gez、if-lezなどがある.これらの命令は本書第3章で紹介されるが,if-nez命令機能とは逆の命令がif-eqzであり,比較結果が0または等しい場合にジャンプすることを示す.任意のテキストエディタでMainActivity$1.smaliファイルを開き、「.line 32」行のコード「if-nezv 0,:cond_0」を「if-eqz v 0,:cond_0」に変更し、保存後に終了します.コードは修正が完了しても終了します.Smaliファイルコードを修正した後、修正したファイルを再コンパイルしてapkファイルにパッケージし、署名してインストールし、勝手に入力する必要があり、登録に成功したことがわかります.