分享

实例详解:反编译Android APK,修改字节码后再回编译成APK

 飞鹰飞龙飞天 2014-11-24

本文详细介绍了如何反编译一个未被混淆过的Android APK,修改smali字节码后,再回编译成APK并更新签名,使之可正常安装。破译后的apk无论输入什么样的用户名和密码都可以成功进入到第二个Activity。

有时难免要反编译一个APK,修改其中的若干关键判断点,然后再回编译成一个全新的可用的apk,这完全是可实现的。若要完成上述工作,需要以下工具,杂家后面会把下载链接也附上。这些软件截止本文发布时,经过杂家确认都是最新的版本。

1.APK-Multi-Toolv1.0.11.zip 用它来反编译apk,得到smali类型的源码和资源文件。一般来讲,直接解压缩一个apk也可以看到其资源文件,但部分xml是不可阅读的,需用此工具反编译。网上流传较广的是一个经过高人汉化过的版本1.0.3,第一次使用这个工具可以用这个汉化的版本,熟悉其命令。但经过杂家测试,这个汉化的版本再回编译apk签名时已经出问题了,签不了,最终用的最新版才ok。

2.dex2jar-0.0.9.15.zip 用于将一个apk文件转换成jar类型的文件,转换之后再利用下面的jd-gui工具才能看到其java代码。上面的apk-tool固然强大,但是反编译出来是smali文件,即dalvik字节码,类似汇编语言的一种代码,直接阅读如天书一样,为此我们需要使用dex2jar + jd-gui来得到其java代码,进行阅读找到要修改的关键点。

3.jd-gui-0.3.6.windows.zip 可以打开一个jar类型的文件,看到java代码。并能将代码保存,保存后再弄到sourceinsight里就方便看了。最好使用本文所说的最新版本,使用老版本会发现一个BActivity,如果里面有内部类,则又多出一个BActivity$1.java,类似这种文件。新版本没这个问题。

4.Smali2Java.1.0.0.558.zip 用于将smali文件转成java文件,这个工具仅仅是备用。因为用上面三个工具已经能够胜任本文的要求了。

如果第一次接触反编译,不了解smali语法,可以阅读链接1  链接2

下面杂家先上一个Android APK,用来简单模拟用户登录的情况,注意只能是模拟哈。因为正式的apk很少有在本地进行判断的。此apk要求用户输入用户名和密码,如果用户名为yanzi,密码为123,则认为合法,自动跳转到第二个Activity。否则提示用户名或密码不正确。

LoginActivity.java

  1. <span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.decomdemo.activity;  
  2.   
  3. import org.yanzi.decompiledemo.R;  
  4.   
  5. import android.app.Activity;  
  6. import android.content.Intent;  
  7. import android.os.Bundle;  
  8. import android.view.Menu;  
  9. import android.view.View;  
  10. import android.widget.Button;  
  11. import android.widget.EditText;  
  12. import android.widget.Toast;  
  13.   
  14. public class LoginActivity extends Activity {  
  15.   
  16.     EditText loginName = null;  
  17.     EditText loginPswd = null;  
  18.     Button loginBtn = null;  
  19.   
  20.     @Override  
  21.     protected void onCreate(Bundle savedInstanceState) {  
  22.         super.onCreate(savedInstanceState);  
  23.         setContentView(R.layout.activity_login);  
  24.         initUI();  
  25.         loginBtn.setOnClickListener(new View.OnClickListener() {  
  26.   
  27.             @Override  
  28.             public void onClick(View v) {  
  29.                 // TODO Auto-generated method stub  
  30.                if(isAllowLogin()){  
  31.                    Intent intent = new Intent(LoginActivity.this, SecondActivity.class);  
  32.                    startActivity(intent);  
  33.                    LoginActivity.this.finish();  
  34.                }  
  35.             }  
  36.         });  
  37.     }  
  38.   
  39.     @Override  
  40.     public boolean onCreateOptionsMenu(Menu menu) {  
  41.         // Inflate the menu; this adds items to the action bar if it is present.  
  42.         getMenuInflater().inflate(R.menu.login, menu);  
  43.         return true;  
  44.     }  
  45.     private void initUI(){  
  46.         loginName = (EditText)findViewById(R.id.edit_login_name);  
  47.         loginPswd = (EditText)findViewById(R.id.edit_login_pswd);  
  48.         loginBtn = (Button)findViewById(R.id.btn_login);  
  49.     }  
  50.     private boolean isAllowLogin(){  
  51.         String name = loginName.getText().toString().trim();  
  52.         String pswd = loginPswd.getText().toString().trim();  
  53.         if(name != null  && pswd != null){  
  54.             if(name.equals("") || pswd.equals("")){  
  55.                 Toast.makeText(this, "密码或用户名不能为空", Toast.LENGTH_SHORT).show();  
  56.                 return false;  
  57.             }  
  58.         }  
  59.         if(name.equals("yanzi") && pswd.equals("123")){  
  60.             return true;  
  61.         }else{  
  62.             Toast.makeText(getApplicationContext(), "用户名或密码不符", Toast.LENGTH_SHORT).show();  
  63.         }  
  64.         return false;  
  65.     }  
  66.   
  67. }  
  68. </span>  



SecondActivity.java

  1. <span style="font-family:Comic Sans MS;font-size:18px;">package org.yanzi.decomdemo.activity;  
  2.   
  3. import org.yanzi.decompiledemo.R;  
  4. import org.yanzi.decompiledemo.R.layout;  
  5. import org.yanzi.decompiledemo.R.menu;  
  6.   
  7. import android.os.Bundle;  
  8. import android.app.Activity;  
  9. import android.view.Menu;  
  10.   
  11. public class SecondActivity extends Activity {  
  12.   
  13.     @Override  
  14.     protected void onCreate(Bundle savedInstanceState) {  
  15.         super.onCreate(savedInstanceState);  
  16.         setContentView(R.layout.activity_second);  
  17.     }  
  18.   
  19.     @Override  
  20.     public boolean onCreateOptionsMenu(Menu menu) {  
  21.         // Inflate the menu; this adds items to the action bar if it is present.  
  22.         getMenuInflater().inflate(R.menu.second, menu);  
  23.         return true;  
  24.     }  
  25.   
  26. }  
  27. </span>  


布局文件和其他代码都比较简单就不介绍了。运行界面如下:

其中判断用户名和密码是否合法的关键函数如下:

  1. <span style="font-family:Comic Sans MS;font-size:18px;">private boolean isAllowLogin(){  
  2.         String name = loginName.getText().toString().trim();  
  3.         String pswd = loginPswd.getText().toString().trim();  
  4.         if(name != null  && pswd != null){  
  5.             if(name.equals("") || pswd.equals("")){  
  6.                 Toast.makeText(this, "密码或用户名不能为空", Toast.LENGTH_SHORT).show();  
  7.                 return false;  
  8.             }  
  9.         }  
  10.         if(name.equals("yanzi") && pswd.equals("123")){  
  11.             return true;  
  12.         }else{  
  13.             Toast.makeText(getApplicationContext(), "用户名或密码不符", Toast.LENGTH_SHORT).show();  
  14.         }  
  15.         return false;  
  16.     }</span>  


现在假设我们根本不知道上面的代码,就是一个apk拿到手,如何破译呢?

第一步:dex2jar 将apk转成jar

需要将apk文件拷贝到dex2jar解压缩后的目录,cmd进去该目录,输入命令:

G:\反编译工具\dex2jar-0.0.9.15>d2j-dex2jar.bat G:\反编译工具\dex2jar-0.0.9.15\DecompileDemo.apk

注意apk的路径是拖apk进去的。按回车,得到jar,如图所示:


第二步:jd-gui打开此jar文件,如图所示:


可以看到反编译出来的java代码跟原来的代码相差无几,但还是有点小差别,把R.id.....换成了具体的数字,另外是很多地方多加了this指针,包括内部类调用父亲的私有函数上。再就是{}原来不换行的,现在换行了。后面会发现,代码所在的行号对破译修改smali文件是个很有用的信息。在jd-gui里是不能对代码进行修改的,可以ctrl+s将其全部保存,得到一个压缩文件。再解压缩,弄到sourceInsight里可以慢慢看了。等等,在看Activity的代码时,要结合APK TOOL里反编译出来的Manifinest.xml来看。因为通过xml至少可以看到一个工程的启动Activity是谁,程序从哪进去的,有几个Activity,又是怎么跳转的。

第三步:APK TOOL反编译出smali文件,研读smali文件,并进行修改

单纯依靠上面两个步骤虽然得到java,但是是无法进行修改的,而且就算你修改了其中的java文件,是无法再返回去生成apk的。有人说先弄成class,再弄成.dex,最后进行替换,这个基本不可能。另外一种思路是,仿照这个java文件老子再重写一份。这个如果项目简单可以这么搞,这样的话反编译的目的就是看代码,而非破译了,因为你重写了一份!

    所以,唯一可行的就是直接修改smali文件。首先运行APK-Multi-Tool文件夹下的Setup.bat,


这里需要执行的其实只有第三个步骤,生成相应的文件夹。第一个检查更新,第二个是安装框架资源,这个除非你是搞rom的,否则的话破译一般的非系统apk是用不到的。执行3步骤后,可以看到文件夹多了好几个:


红框里的五个文件夹都是新增的,需要将待反编译的apk放到place-apk-here-for-modding文件夹下,反编译后的源码在projects文件夹下。知道这就够用了。

首先把apk拷到place-apk-here-for-modding文件夹下,然后运行Script.bat,首先映入眼帘的是一堆类似乱码的东西,不用管,enter后看到主界面:


最上面的一栏类似状态栏,再往下可以看到这个工具的功能分三大部分:Simple Tasks Such As Image Editing(如果修改了图片资源或文字)就执行此模块的命令, Advanced Tasks Such As Code Editing(高级模式,如修改了源代码),一般我们破译apk需要的是这部分。最后一个模块是Themers Convertion Tools,这个杂家么有用到。

下面是 tools Stuff工具集合,我们按下24 Set current project设置当前工程,选中对应的apk即可。然后按9 Decompile apk,反编译apk,注意看标题栏里的状态是:

 Decompile : Sources and Resources Files即反编译源码和资源,我们需要的就是这种模式。反编译结束后再projects文件夹里找到输出结果:


smali就是待修改的dalvik 字节码文件:


可以看到多了一个****$1.smali的文件,这是因为button设置监听引入了内部类造成的,可以不用管。这种带$1的文件,一般表示内部的如按键监听这种属性关系,并没有太多事物的处理逻辑。打开LoginActivity.smali文件,找到下面这段话:

.method private isAllowLogin()Z
    .locals 5


    .prologue
    const/4 v2, 0x0


    .line 51
    iget-object v3, p0, Lorg/yanzi/decomdemo/activity/LoginActivity;->loginName:Landroid/widget/EditText;


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


    move-result-object v3


    invoke-interface {v3}, Landroid/text/Editable;->toString()Ljava/lang/String;


    move-result-object v3


    invoke-virtual {v3}, Ljava/lang/String;->trim()Ljava/lang/String;


    move-result-object v0


    .line 52
    .local v0, name:Ljava/lang/String;
    iget-object v3, p0, Lorg/yanzi/decomdemo/activity/LoginActivity;->loginPswd:Landroid/widget/EditText;


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


    move-result-object v3


    invoke-interface {v3}, Landroid/text/Editable;->toString()Ljava/lang/String;


    move-result-object v3


    invoke-virtual {v3}, Ljava/lang/String;->trim()Ljava/lang/String;


    move-result-object v1


    .line 53
    .local v1, pswd:Ljava/lang/String;
    if-eqz v0, :cond_1


    if-eqz v1, :cond_1


    .line 54
    const-string v3, ""


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


    move-result v3


    if-nez v3, :cond_0


    const-string v3, ""


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


    move-result v3


    if-eqz v3, :cond_1


    .line 55
    :cond_0
    const-string v3, "\u5bc6\u7801\u6216\u7528\u6237\u540d\u4e0d\u80fd\u4e3a\u7a7a"


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


    move-result-object v3


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


    .line 64
    :goto_0
    return v2


    .line 59
    :cond_1
    const-string v3, "yanzi"


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


    move-result v3


    if-eqz v3, :cond_2


    const-string v3, "123"


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


    move-result v3


    if-eqz v3, :cond_2


    .line 60
    const/4 v2, 0x1


    goto :goto_0


    .line 62
    :cond_2
    invoke-virtual {p0}, Lorg/yanzi/decomdemo/activity/LoginActivity;->getApplicationContext()Landroid/content/Context;


    move-result-object v3


    const-string v4, "\u7528\u6237\u540d\u6216\u5bc6\u7801\u4e0d\u7b26"


    invoke-static {v3, v4, v2}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;


    move-result-object v3


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


    goto :goto_0
.end method

注意看,方法是以.method开头的,Z表示boolean类型。下面再把jd-gui里破译出来的java代码附下:

private boolean isAllowLogin()
  {
    String str1 = this.loginName.getText().toString().trim();
    String str2 = this.loginPswd.getText().toString().trim();
    if ((str1 != null) && (str2 != null) && ((str1.equals("")) || (str2.equals(""))))
    {
      Toast.makeText(this, "密码或用户名不能为空", 0).show();
      return false;
    }
    if ((str1.equals("yanzi")) && (str2.equals("123"))) {
      return true;
    }
    Toast.makeText(getApplicationContext(), "用户名或密码不符", 0).show();
    return false;
  }

既然是破译,那么这两个代码就是最有力的线索了。另外,因为dalvik字节码是基于寄存器的,所有的变量处理都要经过寄存器。而寄存器又分为两大类:本地寄存器和参数寄存器。本地寄存器使用v0 v1这种方式命名,函数的开头.locals 5 也声明了要用5个本地寄存器,从v0到v4.参数寄存器是从p0开始。由于我们这个函数是没有参数传进来的,原则上是不使用参数寄存器的,但是它是非static的。凡是非static就又this这个指针,默认的使用p0来代表。p1才是函数输入的第一个参数。如果是static类型的函数,则p0就是第一个参数。

.prologue是开场白的意思,const/4 v2, 0x0 表示声明一个本地变量里面放的是0,因为我们的isAllowLogin()函数原来的java代码里有三个return,但在smali里只有一个return,且return的就是v2这个变量。找到 如下三句:

   .line 64
    :goto_0
    return v2

只有这一个return,且是加了标签goto_0,搜索goto_0可以看到有两个地方都执行了goto :goto_0,即也让它返回v2.其中,在判断用户名和密码复合要求的时候,它对v2这个变量进行了赋值:

const/4 v2, 0x1

这个意思就是把v2置为了true,并进行返回。至此,为了实现破解,只需把v2变量声明的时候直接默认值改成0x1不就ok了!如此修改后,回到APK TOOL,

点击15    Compile apk / Sign apk / Install apk  (Non-System Apps Only),这个可以直接将源码回编译成apk,并且添加签名再安装。但此处有个问题,这里的安装还是安装的原来的apk文件,选中此命令执行后,在刚才的place-apk-here-for-modding文件夹可以看到:


此处的signedDecompileDemo.apk就是回编译出来的apk文件,将这个apk安装才能看到修改smali后的效果。至此,大功告成。但,本文只是抛砖引玉,破译的关键还是要看smali语言的功底,和对整个代码的理解,找到正确的破译点。

1.APK-Multi-Toolv1.0.11.zip:http://download.csdn.net/detail/yanzi1225627/7728439

2.dex2jar-0.0.9.15.zip:http://download.csdn.net/detail/yanzi1225627/7728447

3.jd-gui-0.3.6.windows.zip: http://download.csdn.net/detail/yanzi1225627/7728453

4.Smali2Java.1.0.0.558.zip:http://download.csdn.net/detail/yanzi1225627/7728477


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多