LoginSignup
24
29

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-07-01

行うこと

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

使用するツール

・apktool
https://ibotpeaches.github.io/Apktool/
・dex2jar
https://github.com/pxb1988/dex2jar
・jad
http://www.javadecompilers.com/jad

検証用テストアプリ

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

Screenshot_20170701-193328.png

Screenshot_20170701-193401.png

・ソースコード

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();
    }
}
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へ変換
> <dex2jar.batのフォルダパス>\dex2jar.bat <classes.dexのフォルダパス>\classes.dex
  1. 2で出力されたclasses-dex2jar.jarの拡張子をzipへ変更して解凍
  2. 3で解凍したフォルダ内にあるclassファイルをjavaファイルへ変換
> <jadのフォルダパス>\jad -s java -d src -r classes-dex2jar\**\*.class

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

src\com\example\user\logintest

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形式コードへデコンパイル

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

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

LogInTest\smali\com\example\user\logintest

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
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の

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

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

smaliソースを追っていくと、MainActivity$1.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が判定処理で、直下に成功時の処理が続いているため、判定処理をコメントアウトします。

    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へ再コンパイル

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

署名

> 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

インストール

>adb install -r LogInTestCustom1.apk

動作確認

Screenshot_20170701-201202.png

24
29
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
29