行うこと
・apkファイルを元のJava形式のソースコードへデコンパイル
・apkファイルを再コンパイル可能なSmali形式コードへデコンパイル
・デコンパイルしたSmaliコードに修正を入れ再コンパイル
使用するツール
・apktool
https://ibotpeaches.github.io/Apktool/
・dex2jar
https://github.com/pxb1988/dex2jar
・jad
http://www.javadecompilers.com/jad
検証用テストアプリ
正しいパスワード(possword)を入力するとログイン処理を行う
パスワード入力欄とログインボタンを備えた簡易アプリを検証に使用
・ソースコード
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形式のソースコードへデコンパイル
- apkファイルの拡張子をzipへ変更して解凍
- 1で解凍したフォルダ内にあるclasses.dexをjarへ変換
> <dex2jar.batのフォルダパス>\dex2jar.bat <classes.dexのフォルダパス>\classes.dex
- 2で出力されたclasses-dex2jar.jarの拡張子をzipへ変更して解凍
- 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