在Android应用开发中,软件安全和逆向分析非常重要。试想如果一个优秀的APP应用没有建立完善的安全机制,从而很容易被黑客破解修改,一方面泄露了应用程序的核心技术,另一方面势必会对用户带来损害,从而造成大量的用户流失。如何反编译破解apk以及保护自己的软件免受反编译破解,是这个系列文章的主题。
这篇文章主要从apk反编译破解和java汇编语言读写两个方面进行了Android中逆向分析的简述。俗话说:知己知彼,百战不殆。只有了解了apk的反编译破解过程,才能反方面进行加密处理从而避免自己的应用程序被反编译破解。本文通过使用apktool、dex2jar、jd-gui等工具实现了apk的反编译破解,然后修改破解后的java汇编语言、利用jarsigner进行二次签名从而达到改变apk功能的作用。
一、测试环境和工具
点击即可下载相应工具
- windows7 x86
- jdk1.7.0
- Android SDK和eclipse:adt-bundle-windows-x86-20140321
- jd-gui
- signapk
- dex2jar
- apktool
二、编写Android实例
首先完成一个Android简单实例的编写,主要功能是输入一个序列号,如果输入序列号为1234,输出“恭喜你,注册成功!”,否则输出”对不起,序列号错误!”。
整个应用程序的工程目录如下,
activity_main.xml文件很简单,就两个控件EditText和Button。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.example.cracktest.MainActivity" > <EditText android:id="@+id/et_password" android:layout_width="fill_parent" android:layout_height="wrap_content" android:hint="请输入软件的序列号" /> <Button android:id="@+id/click" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="确定" /></LinearLayout>
MainActivity.java的实现也很简单,显示用Toast实现,
package com.example.cracktest;import android.app.Activity;import android.os.Bundle;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.EditText;import android.widget.Toast;public class MainActivity extends Activity { private EditText et; private Button eb; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); et = (EditText) findViewById(R.id.et_password); eb = (Button) findViewById(R.id.click); eb.setOnClickListener(new OnClickListener() { public void onClick(View v) { String pwd = et.getText().toString().trim(); if ("1234".equals(pwd)) { Toast.makeText(MainActivity.this, "恭喜你,注册成功!", 0).show(); } else { Toast.makeText(MainActivity.this, "对不起,序列号错误!", 0).show(); } } }); }}
模拟器上运行这个简单实例如下,
三、apk反编译破解
假设我们现在要对上述工程生成的apk进行反编译破解,那么该怎么做呢?下面我们进行详细的流程描述。
导出APK到桌面
DDMS->Devices->点击stop停止程序->File Explore->data->APP->.apk->点击导出键.zip解压apk
得到apk文件以后,若是直接修改后缀.apk为.zip文件然后解压查看,会发现解压完的文件在编辑器打开为乱码,只能寻求更好的方法来查看apk中的文件。apktool打开apk
apktool为apk逆向工程的软件,首先拷贝apk到apktool的目录下,然后cmd进入apktool的目录下,执行如下命令即可得到apk的全部的资源素材。
得到apk资源素材后,点击查看,C:\Users\Administrator\Desktop\apktool\com.example.cracktest-1\smali\com\example\cracktest
用编辑器打开MainActivity.smali文件如下,
.class public Lcom/example/cracktest/MainActivity;.super Landroid/app/Activity;.source "MainActivity.java"# instance fields.field private eb:Landroid/widget/Button;.field private et:Landroid/widget/EditText;# direct methods.method public constructor <init>()V .locals 0 .prologue .line 11 invoke-direct {p0}, Landroid/app/Activity;-><init>()V return-void.end method.method static synthetic access$0(Lcom/example/cracktest/MainActivity;)Landroid/widget/EditText; .locals 1 .parameter .prologue .line 13 iget-object v0, p0, Lcom/example/cracktest/MainActivity;->et:Landroid/widget/EditText; return-object v0.end method# virtual methods.method protected onCreate(Landroid/os/Bundle;)V .locals 2 .parameter "savedInstanceState" .prologue .line 17 invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V .line 18 const/high16 v0, 0x7f03 invoke-virtual {p0, v0}, Lcom/example/cracktest/MainActivity;->setContentView(I)V .line 20 const/high16 v0, 0x7f08 invoke-virtual {p0, v0}, Lcom/example/cracktest/MainActivity;->findViewById(I)Landroid/view/View; move-result-object v0 check-cast v0, Landroid/widget/EditText; iput-object v0, p0, Lcom/example/cracktest/MainActivity;->et:Landroid/widget/EditText; .line 21 const v0, 0x7f080001 invoke-virtual {p0, v0}, Lcom/example/cracktest/MainActivity;->findViewById(I)Landroid/view/View; move-result-object v0 check-cast v0, Landroid/widget/Button; iput-object v0, p0, Lcom/example/cracktest/MainActivity;->eb:Landroid/widget/Button; .line 23 iget-object v0, p0, Lcom/example/cracktest/MainActivity;->eb:Landroid/widget/Button; new-instance v1, Lcom/example/cracktest/MainActivity$1; invoke-direct {v1, p0}, Lcom/example/cracktest/MainActivity$1;-><init>(Lcom/example/cracktest/MainActivity;)V invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V .line 34 return-void.end method
这就是反编译后生成的java汇编语言,发现读起来很困难,没事,这个在第三部分有讲解,那么我们怎么得到反编译后的java源代码呢?
4. dex2jar生成jar
在apk文件中,修改.apk后缀名为.zip,然后解压得到classes.dex文件,dex2jar工具的作用就是把dex文件转化为jar文件。把得到的classes.dex文件复制到dex2jar目录下,cmd执行命令如下,
5. jd-gui把jar转成java源码
打开jd-gui,直接将生成的classes_dex2jar.jar文件拖入到jd-gui中如下,
点击生成的java包即可查看apk里的java源码,仿真度很高
四、java汇编语言读写
在上面使用apktool工具生成的.smali文件,打开发现是java汇编语言,很难读懂,下面就上述生成的汇编语言进行简单的说明。apktool生成了两个主要的.smali需要查看,分别为MainActivity.smali和MainActivity$1.smali
.class public Lcom/example/cracktest/MainActivity;//类生命,L表示对象类型.super Landroid/app/Activity;//当前类的父类.source "MainActivity.java"//源文件名字//#就像c语言中的双斜杠,注释的作用# instance fields//类成员变量.field private eb:Landroid/widget/Button;.field private et:Landroid/widget/EditText;# direct methods.method public constructor <init>()V //Activity构造方法,返回值为Void .locals 0//本地变量声明0个 .prologue .line 11//这两句为自动生成行号等信息 invoke-direct {p0}, Landroid/app/Activity;-><init>()V//直接调用,p0为this的意思 return-void.end method.method static synthetic access$0(Lcom/example/cracktest/MainActivity;)Landroid/widget/EditText; .locals 1 .parameter .prologue .line 13 iget-object v0, p0, Lcom/example/cracktest/MainActivity;->et:Landroid/widget/EditText; return-object v0.end method# virtual methods.method protected onCreate(Landroid/os/Bundle;)V //返回值为Void,null .locals 2 .parameter "savedInstanceState"//方法参数 .prologue .line 17 invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V .line 18 const/high16 v0, 0x7f03 invoke-virtual {p0, v0}, Lcom/example/cracktest/MainActivity;->setContentView(I)V .line 20 const/high16 v0, 0x7f08 invoke-virtual {p0, v0}, Lcom/example/cracktest/MainActivity;->findViewById(I)Landroid/view/View; move-result-object v0 check-cast v0, Landroid/widget/EditText; iput-object v0, p0, Lcom/example/cracktest/MainActivity;->et:Landroid/widget/EditText; .line 21 const v0, 0x7f080001 invoke-virtual {p0, v0}, Lcom/example/cracktest/MainActivity;->findViewById(I)Landroid/view/View; move-result-object v0 check-cast v0, Landroid/widget/Button; iput-object v0, p0, Lcom/example/cracktest/MainActivity;->eb:Landroid/widget/Button; .line 23 iget-object v0, p0, Lcom/example/cracktest/MainActivity;->eb:Landroid/widget/Button; new-instance v1, Lcom/example/cracktest/MainActivity$1;//新实例对象类型为com/example/cracktest/MainActivity$1 invoke-direct {v1, p0}, Lcom/example/cracktest/MainActivity$1;-><init>(Lcom/example/cracktest/MainActivity;)V invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V .line 34 return-void.end method
.class Lcom/example/cracktest/MainActivity$1;//类生命,L表示对象类型.super Ljava/lang/Object;//当前类的父类.source "MainActivity.java"//源文件名字//#就像c语言中的双斜杠,注释的作用# interfaces //实现接口.implements Landroid/view/View$OnClickListener;# annotations.annotation system Ldalvik/annotation/EnclosingMethod; value = Lcom/example/cracktest/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/cracktest/MainActivity;# direct methods //Activity构造方法.method constructor <init>(Lcom/example/cracktest/MainActivity;)V //Activity构造方法,返回值为Void .locals 0 //本地变量声明0个 .parameter .prologue .line 1 //这两句为自动生成行号等信息 iput-object p1, p0, Lcom/example/cracktest/MainActivity$1;->this$0:Lcom/example/cracktest/MainActivity; .line 23 invoke-direct {p0}, Ljava/lang/Object;-><init>()V //直接调用,p0为this、Activity的意思 return-void.end method# virtual methods.method public onClick(Landroid/view/View;)V //onClick事件处理函数,V代表返回值为空 .locals 4 //需要声明4个本地变量 .parameter "v"//参数为view v .prologue const/4 v3, 0x0 .line 25 //调用p0里面的方法 iget-object v1, p0, Lcom/example/cracktest/MainActivity$1;->this$0:Lcom/example/cracktest/MainActivity; #getter for: Lcom/example/cracktest/MainActivity;->et:Landroid/widget/EditText; invoke-static {v1}, Lcom/example/cracktest/MainActivity;->access$0(Lcom/example/cracktest/MainActivity;)Landroid/widget/EditText; move-result-object v1 invoke-virtual {v1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;//返回值为Editable move-result-object v1 invoke-interface {v1}, Landroid/text/Editable;->toString()Ljava/lang/String; move-result-object v1 invoke-virtual {v1}, Ljava/lang/String;->trim()Ljava/lang/String; move-result-object v0 //由EditText->Editable getText()->toString()->trim(),返回值为v0 .line 27 .local v0, pwd:Ljava/lang/String; const-string v1, "1234" //定义常量,名字v1,值为1234 //比较v1和v0的值 invoke-virtual {v1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z //相等返回非0 move-result v1 if-eqz v1, :cond_0 //如果equal 0,不相等,执行cond_0逻辑 .line 28 iget-object v1, p0, Lcom/example/cracktest/MainActivity$1;->this$0:Lcom/example/cracktest/MainActivity; const-string v2, "\u606d\u559c\u4f60\uff0c\u6ce8\u518c\u6210\u529f\uff01" //如果相等,执行这个逻辑 invoke-static {v1, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; move-result-object v1 invoke-virtual {v1}, Landroid/widget/Toast;->show()V .line 32 :goto_0 return-void .line 30 :cond_0 iget-object v1, p0, Lcom/example/cracktest/MainActivity$1;->this$0:Lcom/example/cracktest/MainActivity; const-string v2, "\u5bf9\u4e0d\u8d77\uff0c\u5e8f\u5217\u53f7\u9519\u8bef\uff01" //可以测试这个字符串,控制台打印 invoke-static {v1, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; move-result-object v1 invoke-virtual {v1}, Landroid/widget/Toast;->show()V goto :goto_0.end method
在上述分析中,如果v1和v0相等,则执行逻辑
const-string v2, “\u606d\u559c\u4f60\uff0c\u6ce8\u518c\u6210\u529f\uff01” //如果相等,执行这个逻辑
这句话是什么意思呢?我们可以自己写个java Test测试一下这个字符串,
五、修改反编译后的汇编语言文件
假设我们现在得到了一个apk反编译后的java汇编语言,如果修改它使的改变它的功能。就上面这个列子而言,如果输入字符串为1234,则显示注册成功,否则显示序列号错误。在汇编语言中它实现的逻辑如下,
if-eqz v1, :cond_0 //如果equal 0,不相等,执行cond_0逻辑
如果v1和v0的比较返回值为0,即eqz为equal 0的话,执行显示cond_0,显示序列号错误。。。
修改if从句为
if-nez v1, :cond_0 //如果not equal 0,相等,执行cond_0逻辑
意思是说,如果v1和v0的比较不为0,即v1,v0相等,让它显示序列号错误,相当于和原来的逻辑相反了。
在C:\Users\Administrator\Desktop\apktool\com.example.cracktest-1\smali\com\example\cracktest\MainActivity$1.smali文件中按照上述修改if从句。然后cmd进入apktool目录下,执行命令如下,
生成new.apk新的apk文件后,我们将之部署到模拟器上测试,找到adb安装目录后,cmd执行如下命令,
结果安装解析失败,原因没有签名证书,为什么呢?
点击查看原来的和新的apk文件,我们发现new.apk文件中没有META-INF文件,
META-INF文件夹为应用程序的签名。
为了解决安装apk失败的问题,我们要给new.apk进行签名。
将new.apk复制到signapk目录下,cmd中执行如下命令生成out.apk文件,
在out.apk中已经生成了META-INF文件夹,最后安装out.apk到模拟器上,
还是失败,因为原来模拟器上安装过这个软件,需要重新卸载掉,然后安装就可以了!
模拟器上卸载程序有两种方法:
- 直接在模拟器中点击menu–>settings –> Applications –> Manage Applications 中点击你需要卸载的apk(就是你的应用软件)–>Uninstall;
- 进入内嵌的linux,去data/app目录下删除 apk文件,这种方法可以批量删除
adb shell
cd data
cd app
rm ApplicationName.apk
卸载之后,重新进行安装如下,
最后在模拟器上测试结果展示
显示结果正好和原来结果相反,证明我们之前修改的.smali汇编语言文件,起了作用。
实际反汇编破解apk过程中,还是用dex2jar工具比较好,如果一个工程应用程序很复杂,那么直接查看汇编语言文件.smali会是项庞大的工程。no zuo no die !
六、参考引用
android apk反编译详解
Android APK反编译就这么简单 详解(附图)
Android APK反编译详解(附图)
谈谈android反编译和防止反编译的方法
未完待续。。。