一般情况下我们想要了解别人的app怎么实现这个动画,这个效果的时候,总是会想到反编译一下,看下布局,看下代码实现。对,这对于有经验的玩家确实手到擒来了,但是初学者,根本就不知道怎么反编译,怎么看代码,甚至不知道什么是反编译。那就学一下吧。
简单写一个app
先简单写个app用作后面的反编译,当然可以直接拿现有的比较成熟的app,但是没有源码我们没办法好好比较了。好了,比较简单就直接上代码了,这里用了下databinding,具体以后也会写文章具体讲解databinding的。
xml界面代码:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data class="MainDataBinding">
</data>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Decompilation:"
android:textSize="20sp" />
<EditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:hint="@string/account"/>
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:hint="@string/password"/>
<Button
android:id="@+id/bt_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/login"
android:textAllCaps="false" />
</LinearLayout>
</layout>
java代码:
package com.jared.decompilationstudy;
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import com.jared.decompilationstudy.databinding.MainDataBinding;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private MainDataBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(MainActivity.this, R.layout.activity_main);
binding.btLogin.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.bt_login:
if (checkInfo()) {
Toast.makeText(MainActivity.this, getResources().getString(R.string.login_ok),Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, getResources().getString(R.string.login_failure),Toast.LENGTH_SHORT).show();
}
break;
}
}
private boolean checkInfo() {
if (!"admin".equals(binding.etAccount.getText().toString()))
return false;
if (!"123456".equals(binding.etPassword.getText().toString()))
return false;
return true;
}
}
其实主要实现就是一个简单的登录界面,判断用户名为admin,密码为123456才会显示登录成功。后面也会通过反编译之后重新打包破解之。那就继续吧。
Apktool工具–反编译资源
apktool工具是反编译资源用的,当然你也可以把apk的后缀名改为zip,然后解压文件,但是直接解压出来的文件只有图片资源可用,其他的都是乱码,为了查看layout等资源,所以我们就需要apktool工具了。
下载地址:http://ibotpeaches.github.io/Apktool/install/
apktool工具主要有三个文件,分别是aapt,apktool,apktools.jar。以mac为例,将三个文件拷贝到/usr/local/bin/目录下,必要的情况下设置可执行权限。之后在终端可以执行apktool,有如下信息表示ok。
Apktool v2.1.1 - a tool for reengineering Android apk files
with smali v2.1.2 and baksmali v2.1.1
Copyright 2014 Ryszard Wiśniewski <brut.alll@gmail.com>
Updated by Connor Tumbleson <connor.tumbleson@gmail.com>
usage: apktool
-advance,--advanced prints advance information.
-version,--version prints the version then exits
usage: apktool if|install-framework [options] <framework.apk>
-p,--frame-path <dir> Stores framework files into <dir>.
-t,--tag <tag> Tag frameworks using <tag>.
usage: apktool d[ecode] [options] <file_apk>
-f,--force Force delete destination directory.
-o,--output <dir> The name of folder that gets written. Default is apk.out
-p,--frame-path <dir> Uses framework files located in <dir>.
-r,--no-res Do not decode resources.
-s,--no-src Do not decode sources.
-t,--frame-tag <tag> Uses framework files tagged by <tag>.
usage: apktool b[uild] [options] <app_path>
-f,--force-all Skip changes detection and build all files.
-o,--output <dir> The name of apk that gets written. Default is dist/name.apk
-p,--frame-path <dir> Uses framework files located in <dir>.
For additional info, see: http://ibotpeaches.github.io/Apktool/
For smali/baksmali info, see: https://github.com/JesusFreke/smali
至于没有成功的,这里也不讲解了,相信google会给你答案。
接着我们开始反编译资源了。先把之前的android代码打包成decompilation.apk。执行如下命令:
apktool d decompilation.apk
I: Using Apktool 2.1.1 on decompilation.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /Users/jared/Library/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
有时候会出现问题类似如下:
Exception in thread "main" brut.androlib.err.UndefinedResObject: resource spec: 0x01010462
原因可能你的apktool版本很老,下载最新的,还有就是需要删除下/Users/用户名/Library/apktool/framework/1.apk
反编译成功后,会在同级目录下生成decompilation,cd进入decompilation目录,ls查看内容如下,有AndroidManifest.xml文件,res下就是我们需要的资源文件了,smali就是Dalvik的一些指令代码,之后有机会再学习学习。
->decompilation ls
AndroidManifest.xml original smali
apktool.yml res unknown
我们看下AndroidManifest.xml的内容:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.jared.decompilationstudy" platformBuildVersionCode="23" platformBuildVersionName="6.0-2438415">
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme ="@style/AppTheme">
<activity android:name="com.jared.decompilationstudy.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
在对比下源码Manifest.xml的内容:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jared.decompilationstudy">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
基本上保持了一致,这里我们没法看java源码,只是资源,那可以看源码吗?答案是肯定的,接着学习吧。
dex2jar&jd-gui工具–反编译源码
dex2jar是把dex文件反编译为jar文件,jd-gui是将jar文件转换为java代码。
dex2jar下载地址:http://sourceforge.net/projects/dex2jar/files/
jd-gui下载地址:http://jd.benow.ca/
dex2jar下载后是一个目录,内容如下:
➜ dex2jar-2.0 ls
classes-dex2jar.jar d2j-jar2jasmin.bat
classes.dex d2j-jar2jasmin.sh
d2j-baksmali.bat d2j-jasmin2jar.bat
d2j-baksmali.sh d2j-jasmin2jar.sh
d2j-dex-recompute-checksum.bat d2j-smali.bat
d2j-dex-recompute-checksum.sh d2j-smali.sh
d2j-dex2jar.bat d2j-std-apk.bat
d2j-dex2jar.sh d2j-std-apk.sh
d2j-dex2smali.bat d2j_invoke.bat
d2j-dex2smali.sh d2j_invoke.sh
d2j-jar2dex.bat lib
d2j-jar2dex.sh
这里我们需要的是d2j-dex2jar.sh脚本。至于jd-gui的话,就是安装好就行了,和一般的ide差不多的。
下面就开始反编译源码了。首先需要把decompilation.apk改为decompilation.zip,然后解压缩得到classes.dex文件。然后把classes.dex拷贝到dex2jar目录下:
➜ dex2jar-2.0 cp ../apk/decompilation2/classes.dex .
➜ dex2jar-2.0 ls
classes-dex2jar.jar d2j-jar2jasmin.bat
classes.dex d2j-jar2jasmin.sh
d2j-baksmali.bat d2j-jasmin2jar.bat
d2j-baksmali.sh d2j-jasmin2jar.sh
d2j-dex-recompute-checksum.bat d2j-smali.bat
d2j-dex-recompute-checksum.sh d2j-smali.sh
d2j-dex2jar.bat d2j-std-apk.bat
d2j-dex2jar.sh d2j-std-apk.sh
d2j-dex2smali.bat d2j_invoke.bat
d2j-dex2smali.sh d2j_invoke.sh
d2j-jar2dex.bat lib
d2j-jar2dex.sh
开始反编译了,执行如下所示:
➜ dex2jar-2.0 ./d2j-dex2jar.sh classes.dex --force
dex2jar classes.dex -> ./classes-dex2jar.jar
➜ dex2jar-2.0 ls
classes-dex2jar.jar
执行完后就生成了classes-dex2jar.jar文件。接着我们用jd-gui看下源码,打开jd-gui软件,打开classes-dex2jar.jar文件如下所示:
这个时候你可能会非常爽,可以看到源码了,当然也会fuck,辛辛苦苦写的代码就这样被盗了。其实一般app都会做混淆的,看得不是那么容易的,这里没有做混淆就很直白了。好了,基本上一个app的反编译分析也基本上到此结束了。
破解apk,重新打包
这里仅当做技术学习,毕竟别人也是辛辛苦苦写的代码,好了,继续吧。
上面已经反编译了资源,我们回到decompilation目录下,这里有smali目录,主要是一个davik指令的代码。
decompilation ls
AndroidManifest.xml original smali
apktool.yml res unknown
➜ decompilation cd smali
➜ smali ls
android com
➜ smali cd com
➜ com ls
android jared
➜ com cd jared/decompilationstudy/
➜ decompilationstudy ls
BR.smali R$bool.smali R$integer.smali R$styleable.smali
BuildConfig.smali R$color.smali R$layout.smali R.smali
MainActivity.smali R$dimen.smali R$mipmap.smali databinding
R$anim.smali R$drawable.smali R$string.smali
R$attr.smali R$id.smali R$style.smali
那我们要怎么破解呢?逐个击破吧,先看“登录成功”和“登录失败”,我们已“登录成功”为破解的开始吧:
➜ decompilation grep -nr "登录成功" res
res/values/strings.xml:39: <string name="login_ok">登录成功</string>
可以得知这个string的name为login_ok。然后我们继续查找这个login_ok怎么来的?
➜ decompilationstudy grep -nr 'login_ok' .
./R$string.smali:88:.field public static final login_ok:I = 0x7f060024
➜ decompilationstudy grep -nr '0x7f060024' .
./MainActivity.smali:117: const v1, 0x7f060024
./R$string.smali:88:.field public static final login_ok:I = 0x7f060024
可以得知login的I=0X7F060024,然后查找这个得到两个地方调用,一个是MainActivity.smali,后一个是string本身。显然我们的这个是在MainActivity.smali的第117行调用了。那我们继续去看看吧:
这里需要一点汇编基础才能看的懂代码了。
# virtual methods
.method public onClick(Landroid/view/View;)V
.locals 3
.param p1, "view" # Landroid/view/View;
.prologue
const/4 v2, 0x0
.line 25
invoke-virtual {p1}, Landroid/view/View;->getId()I
move-result v0
packed-switch v0, :pswitch_data_0
.line 34
:goto_0
return-void
.line 27
:pswitch_0
invoke-direct {p0}, Lcom/jared/decompilationstudy/MainActivity;->checkInfo()Z
move-result v0
if-eqz v0, :cond_0
.line 28
invoke-virtual {p0}, Lcom/jared/decompilationstudy/MainActivity;->getResources()Landroid/content/res/Resources;
move-result-object v0
const v1, 0x7f060024
invoke-virtual {v0, v1}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
move-result-object v0
invoke-static {p0, v0, v2}, 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
goto :goto_0
.line 30
:cond_0
invoke-virtual {p0}, Lcom/jared/decompilationstudy/MainActivity;->getResources()Landroid/content/res/Resources;
move-result-object v0
const v1, 0x7f060023
invoke-virtual {v0, v1}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
move-result-object v0
invoke-static {p0, v0, v2}, 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
goto :goto_0
.line 25
nop
:pswitch_data_0
.packed-switch 0x7f0b005a
:pswitch_0
.end packed-switch
.end method
可以看出来这是一个onclick方法,有一个checkInfo方法,看这行代码,if-eqz v0, :cond_0,意思是v0为0就跳转到cond_0。很显然cond_0就是登陆失败了,也就是checkInfo返回了true和false分别跳转到对应的方法中。寻着这个,我们看下checkInfo的代码:
.method private checkInfo()Z
.locals 3
.prologue
const/4 v0, 0x0
.line 37
const-string v1, "admin"
iget-object v2, p0, Lcom/jared/decompilationstudy/MainActivity;->binding:Lcom/jared/decompilationstudy/databinding/MainDataBinding;
iget-object v2, v2, Lcom/jared/decompilationstudy/databinding/MainDataBinding;->etAccount:Landroid/widget/EditText;
invoke-virtual {v2}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/Object;->toString()Ljava/lang/String;
move-result-object v2
invoke-virtual {v1, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v1
if-nez v1, :cond_1
.line 41
:cond_0
:goto_0
return v0
.line 39
:cond_1
const-string v1, "123456"
iget-object v2, p0, Lcom/jared/decompilationstudy/MainActivity;->binding:Lcom/jared/decompilationstudy/databinding/MainDataBinding;
iget-object v2, v2, Lcom/jared/decompilationstudy/databinding/MainDataBinding;->etPassword:Landroid/widget/EditText;
invoke-virtual {v2}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/Object;->toString()Ljava/lang/String;
move-result-object v2
invoke-virtual {v1, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v1
if-eqz v1, :cond_0
.line 41
const/4 v0, 0x1
goto :goto_0
.end method
首先赋值v0为0x0,const/4 v0, 0x0,接着看下代码:const-string v1, “admin”,很明显是常量赋值,接着往下看:if-nez v1, :cond_1,如果结果不为0就跳转到cond_1,继续看:const-string v1, “123456”。也是常量赋值123456,然后结果为0跳转到cond_0,否则执行,const/4 v0, 0x1,goto :goto_0,就是v0赋值为1,跳转到goto_0。
综上分析,可以得出主要的关键点是v0寄存器了,checkInfo返回的值为v0,那么如果我们把v0的初始值赋值为0x1,那么不就永远返回true了,不管什么账号登录都是ok的了。修改28行代码为:const/4 v0, 0x1。不容易啊,终于改好了,那么接着我们看看是不是如我们所愿呢?
修改完了代码,我们把修改好的代码打包吧,执行命令如下:
➜ apk apktool b decompilation -o decompilation2.apk
I: Using Apktool 2.1.1
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Building apk file...
I: Copying unknown files/dir...
打包完的代码是没有签名的,没办法在手机上安装的,那么接下来我们开始重签名吧。
➜ apk keytool -genkey -v -keystore Android.keystore -alias android.keystore -keyalg RSA -validity 20000
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
[Unknown]: 1
您的组织单位名称是什么?
[Unknown]: 1
您的组织名称是什么?
[Unknown]: 1
您所在的城市或区域名称是什么?
[Unknown]: 1
您所在的省/市/自治区名称是什么?
[Unknown]: 1
该单位的双字母国家/地区代码是什么?
[Unknown]: 1
CN=1, OU=1, O=1, L=1, ST=1, C=1是否正确?
[否]: y
正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 20,000 天):
CN=1, OU=1, O=1, L=1, ST=1, C=1
输入 <android.keystore> 的密钥口令
(如果和密钥库口令相同, 按回车):
[正在存储Android.keystore]
这里偷懒了,就随便填写了内容,接着用jarsigner签名:
➜ jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore Android.keystore -storepass 123456 decompilation2.apk Android.keystore
正在添加: META-INF/MANIFEST.MF
正在添加: META-INF/ANDROID_.SF
正在添加: META-INF/ANDROID_.RSA
…………
正在签名: com/android/databinding/library/baseAdapters/com.android.databinding.library.baseAdapters-br.bin
正在签名: com/android/databinding/library/baseAdapters/com.android.databinding.library.baseAdapters-layoutinfo.bin
正在签名: com/android/databinding/library/baseAdapters/com.android.databinding.library.baseAdapters-setter_store.bin
jar 已签名。
警告:
未提供 -tsa 或 -tsacert, 此 jar 没有时间戳。如果没有时间戳, 则在签名者证书的到期日期 (2071-05-29) 或以后的任何撤销日期之后, 用户可能无法验证此 jar。
大工搞成,接着安装到手机上通过adb install decompilation2.apk。
见证奇迹的时刻到了:
破解成功了,你也可以试试。