Androidアプリのデコンパイル⇒修正⇒再コンパイル

# 行うこと
・apkファイルを元のJava形式のソースコードへデコンパイル
・apkファイルを再コンパイル可能なSmali形式コードへデコンパイル
・デコンパイルしたSmaliコードに修正を入れ再コンパイル

# 使用するツール
・apktool
https://ibotpeaches.github.io/Apktool/
・dex2jar
https://github.com/pxb1988/dex2jar
・jad
http://www.javadecompilers.com/jad

# 検証用テストアプリ
正しいパスワード(possword)を入力するとログイン処理を行う
パスワード入力欄とログインボタンを備えた簡易アプリを検証に使用

<img width="400" alt="Screenshot_20170701-193328.png" src="https://qiita-image-store.s3.amazonaws.com/0/38382/5b87f53f-e48a-4d63-82f8-89c02d1cd139.png">

<img width="400" alt="Screenshot_20170701-193401.png" src="https://qiita-image-store.s3.amazonaws.com/0/38382/4b702218-374c-1b2a-545e-bb9ee028fd36.png">

・ソースコード

```java:MainActivity.java
package com.example.user.logintest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {

    private Button mLogInButton;
    private EditText mPasswordEditText;
    private TextView mStatusTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mPasswordEditText = (EditText)findViewById(R.id.editText);
        mStatusTextView = (TextView)findViewById(R.id.textView3);

        mLogInButton = (Button)findViewById(R.id.button);
        mLogInButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (passwordCheck(mPasswordEditText.getText().toString())) {
                    processLogInSuccess();
                } else {
                    processLogInFail();
                }
            }
        });
    }

    private boolean passwordCheck(String password) {
        if (("possword").equals(password)) {
            return true;
        } else {
            return false;
        }
    }

    private void processLogInSuccess() {
        mStatusTextView.setText("ログインしました");
        Toast.makeText(MainActivity.this, "ログインしました", Toast.LENGTH_LONG).show();
    }

    private void processLogInFail() {
        mStatusTextView.setText("パスワードが異なります");
        Toast.makeText(MainActivity.this, "パスワードが異なります", Toast.LENGTH_LONG).show();
    }
}
```

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="com.example.user.logintest.MainActivity">

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="PASSWORD : "
             />

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:text=""
            android:ems="10"
            android:id="@+id/editText"
            android:layout_weight="1" />

    </LinearLayout>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >

        <Button
            android:text="ログイン"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/button"
            android:layout_weight="1" />

        <TextView
            android:text="ログインしていません"
            android:textSize="20sp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/textView3"
            android:layout_weight="1"
            android:paddingTop="10dp"
            android:gravity="center_horizontal"/>
    </LinearLayout>

</LinearLayout>
```

# 1.apkファイルを元のJava形式のソースコードへデコンパイル

1. apkファイルの拡張子をzipへ変更して解凍
2. 1で解凍したフォルダ内にあるclasses.dexをjarへ変換

```text
> <dex2jar.batのフォルダパス>\dex2jar.bat <classes.dexのフォルダパス>\classes.dex
```

3. 2で出力されたclasses-dex2jar.jarの拡張子をzipへ変更して解凍
4. 3で解凍したフォルダ内にあるclassファイルをjavaファイルへ変換

```
> <jadのフォルダパス>\jad -s java -d src -r classes-dex2jar\**\*.class
```

-dで指定したsrcフォルダにjavaのソースが出力されます。

src\com\example\user\logintest

```java:MainActivity.java
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 

package com.example.user.logintest;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

public class MainActivity extends Activity
{

    public MainActivity()
    {
    }

    private boolean passwordCheck(String s)
    {
        return "possword".equals(s);
    }

    private void processLogInFail()
    {
        mStatusTextView.setText("\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u7570\u306A\u308A\u307E\u3059");
        Toast.makeText(this, "\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u7570\u306A\u308A\u307E\u3059", 1).show();
    }

    private void processLogInSuccess()
    {
        mStatusTextView.setText("\u30ED\u30B0\u30A4\u30F3\u3057\u307E\u3057\u305F");
        Toast.makeText(this, "\u30ED\u30B0\u30A4\u30F3\u3057\u307E\u3057\u305F", 1).show();
    }

    protected void onCreate(Bundle bundle)
    {
        super.onCreate(bundle);
        setContentView(0x7f04001a);
        mPasswordEditText = (EditText)findViewById(0x7f0b0055);
        mStatusTextView = (TextView)findViewById(0x7f0b0057);
        mLogInButton = (Button)findViewById(0x7f0b0056);
        mLogInButton.setOnClickListener(new android.view.View.OnClickListener() {

            public void onClick(View view)
            {
                if(passwordCheck(mPasswordEditText.getText().toString()))
                {
                    processLogInSuccess();
                    return;
                } else
                {
                    processLogInFail();
                    return;
                }
            }

            final MainActivity this$0;

            
            {
                this$0 = MainActivity.this;
                super();
            }
        }
);
    }

    private Button mLogInButton;
    private EditText mPasswordEditText;
    private TextView mStatusTextView;




}
```

# 2.apkファイルを再コンパイル可能なSmali形式コードへデコンパイル

```text
> java -jar <apktoll.jarがあるフォルダのパス>/apktool.jar d <LogInTest.apkがあるフォルダのパス>/LogInTest.apk
```

apkファイルと同じ名前のフォルダにsmaliファイルが出力されます。

LogInTest\smali\com\example\user\logintest

```smali:MainActivity.smali
.class public Lcom/example/user/logintest/MainActivity;
.super Landroid/app/Activity;
.source "MainActivity.java"


# instance fields
.field private mLogInButton:Landroid/widget/Button;

.field private mPasswordEditText:Landroid/widget/EditText;

.field private mStatusTextView:Landroid/widget/TextView;


# direct methods
.method public constructor <init>()V
    .locals 0

    .prologue
    .line 13
    invoke-direct {p0}, Landroid/app/Activity;-><init>()V

    return-void
.end method

.method static synthetic access$000(Lcom/example/user/logintest/MainActivity;)Landroid/widget/EditText;
    .locals 1
    .param p0, "x0"    # Lcom/example/user/logintest/MainActivity;

    .prologue
    .line 13
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity;->mPasswordEditText:Landroid/widget/EditText;

    return-object v0
.end method

.method static synthetic access$100(Lcom/example/user/logintest/MainActivity;Ljava/lang/String;)Z
    .locals 1
    .param p0, "x0"    # Lcom/example/user/logintest/MainActivity;
    .param p1, "x1"    # Ljava/lang/String;

    .prologue
    .line 13
    invoke-direct {p0, p1}, Lcom/example/user/logintest/MainActivity;->passwordCheck(Ljava/lang/String;)Z

    move-result v0

    return v0
.end method

.method static synthetic access$200(Lcom/example/user/logintest/MainActivity;)V
    .locals 0
    .param p0, "x0"    # Lcom/example/user/logintest/MainActivity;

    .prologue
    .line 13
    invoke-direct {p0}, Lcom/example/user/logintest/MainActivity;->processLogInSuccess()V

    return-void
.end method

.method static synthetic access$300(Lcom/example/user/logintest/MainActivity;)V
    .locals 0
    .param p0, "x0"    # Lcom/example/user/logintest/MainActivity;

    .prologue
    .line 13
    invoke-direct {p0}, Lcom/example/user/logintest/MainActivity;->processLogInFail()V

    return-void
.end method

.method private passwordCheck(Ljava/lang/String;)Z
    .locals 1
    .param p1, "password"    # Ljava/lang/String;

    .prologue
    .line 41
    const-string v0, "possword"

    invoke-virtual {v0, p1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v0

    if-eqz v0, :cond_0

    .line 42
    const/4 v0, 0x1

    .line 44
    :goto_0
    return v0

    :cond_0
    const/4 v0, 0x0

    goto :goto_0
.end method

.method private processLogInFail()V
    .locals 2

    .prologue
    .line 54
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity;->mStatusTextView:Landroid/widget/TextView;

    const-string v1, "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u7570\u306a\u308a\u307e\u3059"

    invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

    .line 55
    const-string v0, "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u7570\u306a\u308a\u307e\u3059"

    const/4 v1, 0x1

    invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 56
    return-void
.end method

.method private processLogInSuccess()V
    .locals 2

    .prologue
    .line 49
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity;->mStatusTextView:Landroid/widget/TextView;

    const-string v1, "\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3057\u305f"

    invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

    .line 50
    const-string v0, "\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3057\u305f"

    const/4 v1, 0x1

    invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 51
    return-void
.end method


# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 2
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;

    .prologue
    .line 21
    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    .line 22
    const v0, 0x7f04001a

    invoke-virtual {p0, v0}, Lcom/example/user/logintest/MainActivity;->setContentView(I)V

    .line 24
    const v0, 0x7f0b0055

    invoke-virtual {p0, v0}, Lcom/example/user/logintest/MainActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/widget/EditText;

    iput-object v0, p0, Lcom/example/user/logintest/MainActivity;->mPasswordEditText:Landroid/widget/EditText;

    .line 25
    const v0, 0x7f0b0057

    invoke-virtual {p0, v0}, Lcom/example/user/logintest/MainActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/widget/TextView;

    iput-object v0, p0, Lcom/example/user/logintest/MainActivity;->mStatusTextView:Landroid/widget/TextView;

    .line 27
    const v0, 0x7f0b0056

    invoke-virtual {p0, v0}, Lcom/example/user/logintest/MainActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/widget/Button;

    iput-object v0, p0, Lcom/example/user/logintest/MainActivity;->mLogInButton:Landroid/widget/Button;

    .line 28
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity;->mLogInButton:Landroid/widget/Button;

    new-instance v1, Lcom/example/user/logintest/MainActivity$1;

    invoke-direct {v1, p0}, Lcom/example/user/logintest/MainActivity$1;-><init>(Lcom/example/user/logintest/MainActivity;)V

    invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V

    .line 38
    return-void
.end method
```

```smali:MainActivity$1.smali
.class Lcom/example/user/logintest/MainActivity$1;
.super Ljava/lang/Object;
.source "MainActivity.java"

# interfaces
.implements Landroid/view/View$OnClickListener;


# annotations
.annotation system Ldalvik/annotation/EnclosingMethod;
    value = Lcom/example/user/logintest/MainActivity;->onCreate(Landroid/os/Bundle;)V
.end annotation

.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x0
    name = null
.end annotation


# instance fields
.field final synthetic this$0:Lcom/example/user/logintest/MainActivity;


# direct methods
.method constructor <init>(Lcom/example/user/logintest/MainActivity;)V
    .locals 0
    .param p1, "this$0"    # Lcom/example/user/logintest/MainActivity;

    .prologue
    .line 28
    iput-object p1, p0, Lcom/example/user/logintest/MainActivity$1;->this$0:Lcom/example/user/logintest/MainActivity;

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method


# virtual methods
.method public onClick(Landroid/view/View;)V
    .locals 2
    .param p1, "view"    # Landroid/view/View;

    .prologue
    .line 31
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity$1;->this$0:Lcom/example/user/logintest/MainActivity;

    iget-object v1, p0, Lcom/example/user/logintest/MainActivity$1;->this$0:Lcom/example/user/logintest/MainActivity;

    # getter for: Lcom/example/user/logintest/MainActivity;->mPasswordEditText:Landroid/widget/EditText;
    invoke-static {v1}, Lcom/example/user/logintest/MainActivity;->access$000(Lcom/example/user/logintest/MainActivity;)Landroid/widget/EditText;

    move-result-object v1

    invoke-virtual {v1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;

    move-result-object v1

    invoke-virtual {v1}, Ljava/lang/Object;->toString()Ljava/lang/String;

    move-result-object v1

    # invokes: Lcom/example/user/logintest/MainActivity;->passwordCheck(Ljava/lang/String;)Z
    invoke-static {v0, v1}, Lcom/example/user/logintest/MainActivity;->access$100(Lcom/example/user/logintest/MainActivity;Ljava/lang/String;)Z

    move-result v0

    if-eqz v0, :cond_0

    .line 32
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity$1;->this$0:Lcom/example/user/logintest/MainActivity;

    # invokes: Lcom/example/user/logintest/MainActivity;->processLogInSuccess()V
    invoke-static {v0}, Lcom/example/user/logintest/MainActivity;->access$200(Lcom/example/user/logintest/MainActivity;)V

    .line 36
    :goto_0
    return-void

    .line 34
    :cond_0
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity$1;->this$0:Lcom/example/user/logintest/MainActivity;

    # invokes: Lcom/example/user/logintest/MainActivity;->processLogInFail()V
    invoke-static {v0}, Lcom/example/user/logintest/MainActivity;->access$300(Lcom/example/user/logintest/MainActivity;)V

    goto :goto_0
.end method
```

# 3.デコンパイルしたSmaliコードに修正を入れ再コンパイル

## smaliファイル修正

MainActivityの

```java
                if (passwordCheck(mPasswordEditText.getText().toString())) {
                    processLogInSuccess();
                } else {
                    processLogInFail();
                }
```

でチェックを行いログイン成功or失敗時の処理を行っているため、
if判定処理をなくすことで、成功時処理のルートへ進むよう修正します。

smaliソースを追っていくと、MainActivity$1.smaliの下記あたりが
判定処理となっている感じです。

```smali
    # invokes: Lcom/example/user/logintest/MainActivity;->passwordCheck(Ljava/lang/String;)Z
    ## passwordCheckの実行
    invoke-static {v0, v1}, Lcom/example/user/logintest/MainActivity;->access$100(Lcom/example/user/logintest/MainActivity;Ljava/lang/String;)Z

    ## passwordCheckの戻り値をv0へ格納
    move-result v0

    ## passwordCheckの戻り値が0ならばcond_0(fail時の処理)へ
    if-eqz v0, :cond_0

    ## Success時の処理
    .line 32
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity$1;->this$0:Lcom/example/user/logintest/MainActivity;

    # invokes: Lcom/example/user/logintest/MainActivity;->processLogInSuccess()V
    ## processLogInSuccessの実行
    invoke-static {v0}, Lcom/example/user/logintest/MainActivity;->access$200(Lcom/example/user/logintest/MainActivity;)V

    .line 36
    ## リターン
    :goto_0
    return-void

    .line 34
    :cond_0
    ## Fail時の処理
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity$1;->this$0:Lcom/example/user/logintest/MainActivity;

    # invokes: Lcom/example/user/logintest/MainActivity;->processLogInFail()V
    ## processLogInFail時の処理
    invoke-static {v0}, Lcom/example/user/logintest/MainActivity;->access$300(Lcom/example/user/logintest/MainActivity;)V

    ## リターン
    goto :goto_0
```

if-eqzが判定処理で、直下に成功時の処理が続いているため、判定処理をコメントアウトします。

```diff
    invoke-static {v0, v1}, Lcom/example/user/logintest/MainActivity;->access$100(Lcom/example/user/logintest/MainActivity;Ljava/lang/String;)Z

    ## passwordCheckの戻り値をv0へ格納
    move-result v0

    ## passwordCheckの戻り値が0ならばcond_0(fail時の処理)へ
-    if-eqz v0, :cond_0
+    ## if-eqz v0, :cond_0

    ## Success時の処理
    .line 32
    iget-object v0, p0, Lcom/example/user/logintest/MainActivity$1;->this$0:Lcom/example/user/logintest/MainActivity;
```

## smali⇒apkへ再コンパイル

```text
>java -jar <apktools.jarがあるフォルダのパス>\apktool.jar b .\LogInTest -o .\LogInTestCustom.apk
```
## 署名

```text
> keytool -genkeypair -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -validity 10950 -dname "CN=Android Debug,O=Android,C=US" -keystore debug.keystore -storepass android
> jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA1 -tsa http://timestamp.digicert.com -keystore debug.keystore -storepass android LogInTestCustom.apk androiddebugkey
```

## インストール

```text
>adb install -r LogInTestCustom1.apk
```

## 動作確認

<img width="400" alt="Screenshot_20170701-201202.png" src="https://qiita-image-store.s3.amazonaws.com/0/38382/95dc5ae4-b977-3abe-f5e6-f81bd486eb3a.png">