本文我们将来探讨关于Android的反编译。通常来说,我们在开发过程中的apk出于DEBUG状态,我们并没有给予APK一个特定的签名,而是编译系统默认给apk一个签名。在发布到应用商城时,我们会用自己的签名文件来签名apk,以防止被其他人恶意篡改apk。当然,我们也会利用Android的混淆技术或者一些加固技术来防止apk被反编译造成源码泄漏。
所以,本文只能针对于没有被签名、混淆、加固过的apk,对于绝大多数市面上的apk来说,如果你想要通过反编译得到里面的重要源码,那是行不通的。如果apk用了加固技术,那根本要反编译都很困难。
我先列举一下我们将会用到的几个工具:
apktool.jar:查看apk包下的AndroidManifest.xml和res文件夹内容。
dex2jar.jar:把apk中的classes.dex转为一个jar包
jdgui:通过上面获得的jar包,利用这个工具打开
baksmali.jar:把apk中的classes.dex转为为smali源码
smali:把smali文件编译打包成classes.dex的工具
signapk.jar 把我们重新生成的apk重新签名
以上的所有工具打包下载链接:http://download.csdn.net/download/lc_miao/9966230
废话少说,我们来自己写个Demo,编译出一个apk,这个apk很简单,我们在AActivity输入密码:123456之后才能启动到BActivity,否则提示密码错误。
源码如下:
public class AActivity extends ActionBarActivity {
Button btn;
EditText et;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = (Button) findViewById(R.id.bt);
et = (EditText) findViewById(R.id.et);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
String pwd = et.getText().toString();
if("123456".equals(pwd)){
startActivity(new Intent(AActivity.this,BActivity.class));
Toast.makeText(AActivity.this, "登录成功", Toast.LENGTH_LONG).show();
}else{
Toast.makeText(AActivity.this, "密码错误", Toast.LENGTH_LONG).show();
}
}
});
}
我们的运行截图是这样:
好的,接下来我们来尝试解开apk里面的内容。把apk文件的后缀名改成.zip的压缩格式,打开:
从apk的目录来看:
res:我们的资源目录
META-INF:一些信息配置,这里我们可以不关心。
resources.arsc:编译资源时生成的文件,资源能根据配置索引到相应的资源就是依赖了它。
classes.dex:源码编译打包后的文件。
AndroidManifest.xml:大家都知道了
首先,我们来看下如何查看apk的源码,我们提取出classes.dex,把classes.dex放到dex2jar的文件夹里面,然后打开cmd,cd进入dex2jar的文件目录,输入命令:dex2jar.bat classes.dex
可以发现文件夹里面生成了一个classes_dex2jar.jar,我们把这个jar包提取出来,用jd-gui这个工具来打开,可以直接将jar包拖曳到jd-gui上打开,如下:
到此,我们就完成了反编译的源码查看。
而apk里面的res目录一些xml文件和AndroidManifest.xml,由于已经被编译成二进制文件,我们无法直接打开查看。可以由apktool.jar这个工具来反编译还原成我们能打开查看的文件。
同样在cmd里面 cd进入apktool.jar所在的文件夹,把我们的apk放进来,后缀名可以是被我们改成的zip后缀,或者是原先的.apk后缀。
敲入命令:apktool d Demo.zip
在文件夹中生成了一个文件夹,里面所有的xml文件我们就可以打开查看了,
比如查看AndroidManifest.xml:
到这里我们已经学会了反编译查看apk源码。接下来我们再来看看如何修改apk进行二次打包。
在上面我们写的apk中,需要输入123456才能登录进第二个界面,。并且会弹出Toast提示。
我们来修改成输入123即可进入第二个界面。
首先,我们需要把classex.dex转为smali文件,利用baksmali.jar这个工具,如下:
我们把classex.dex复制到baksmali.jar所在的文件夹,然后cd进入这个文件夹之后,敲入命令: java -jar baksmali-2.0.3.jar -x classes.dex
可以发现目录生成了个out文件夹,里面存放的就是我们的源码,不过是smali格式的,如果想要深层次的去修改源码则需要先学习smali的语法构造。这里我们简单的修改几个数值,进入out文件中,依次点开文件夹可以发现好几个smali文件,我们发现AActivity的有AActivity.smali文件和AActivity
1.smali,之所以会出现多一个AActivity
1.smali是和匿名内部类有关,这跟我们在开发中打开出匿名内部类的类名是一样的。由于这里我们只是用到了点击事件,所以这个AActivity$1.smali就是点击事件的匿名内部类的实现了,我们打开这个文件。
打开后发现都是我们不熟悉的语法,
首先:
.class Lcom/example/demo/AActivity$1; 我们定义的类
.super Ljava/lang/Object; 继承的超类,默认是Object
.source “AActivity.java” 对应的源文件
.# interfaces
.implements Landroid/view/View$OnClickListener;
这个是实现的接口
下面的# instance fields、# direct methods、# virtual methods则是这个类定义的字段、方法了。
我们重点来看onClick方法:
# virtual methods
.method public onClick(Landroid/view/View;)V
.registers 8
.param p1, "v" # Landroid/view/View;
.prologue
const/4 v5, 0x1
.line 27
iget-object v1, p0, Lcom/example/demo/AActivity$1;->this$0:Lcom/example/demo/AActivity;
iget-object v1, v1, Lcom/example/demo/AActivity;->et:Landroid/widget/EditText;
invoke-virtual {v1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object v1
invoke-interface {v1}, Landroid/text/Editable;->toString()Ljava/lang/String;
move-result-object v0
.line 28
.local v0, "pwd":Ljava/lang/String;
const-string v1, "123456"
invoke-virtual {v1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v1
if-eqz v1, :cond_2f
.line 29
iget-object v1, p0, Lcom/example/demo/AActivity$1;->this$0:Lcom/example/demo/AActivity;
new-instance v2, Landroid/content/Intent;
iget-object v3, p0, Lcom/example/demo/AActivity$1;->this$0:Lcom/example/demo/AActivity;
const-class v4, Lcom/example/demo/BActivity;
invoke-direct {v2, v3, v4}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V
invoke-virtual {v1, v2}, Lcom/example/demo/AActivity;->startActivity(Landroid/content/Intent;)V
.line 30
iget-object v1, p0, Lcom/example/demo/AActivity$1;->this$0:Lcom/example/demo/AActivity;
const-string v2, "\u767b\u5f55\u6210\u529f"
invoke-static {v1, v2, v5}, 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 34
:goto_2e
return-void
.line 32
:cond_2f
iget-object v1, p0, Lcom/example/demo/AActivity$1;->this$0:Lcom/example/demo/AActivity;
const-string v2, "\u5bc6\u7801\u9519\u8bef"
invoke-static {v1, v2, v5}, 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_2e
.end method
再配合我们在jd-gui中打开查看到的源码。
.line 27
iget-object v1, p0, Lcom/example/demo/AActivity$1;->this$0:Lcom/example/demo/AActivity;
iget-object v1, v1, Lcom/example/demo/AActivity;->et:Landroid/widget/EditText;
invoke-virtual {v1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object v1
invoke-interface {v1}, Landroid/text/Editable;->toString()Ljava/lang/String;
move-result-object v0
首先onClick的这部分代码,可以对应我们查看到的源码这行:
“123456”.equals(AActivity.this.et.getText().toString())
可以看出上面几句smali源码是这句代码的一个执行顺序,首先是有这个AActivity对象,然后得到EditText对象,然后执行getText后执行toString
其后:
.line 28
.local v0, "pwd":Ljava/lang/String;
const-string v1, "123456"
invoke-virtual {v1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
发现载入了一个pwd变量,赋值为v0,v0实际上就是上面 move-result-object v0得到的。然后再有一个字符串常量为123456,到此我们就可以把123456修改成123了。
接着执行了equals后,注意到:
move-result v1
if-eqz v1, :cond_2f
把结果move到v1,又判断v1,如果v1是0的话跳到cond_2f,
不是0则继续下面,下面的代码也可以看出加载顺序就是intent启动的加载顺序了,
直到最后弹出了Toast提示,我们可以发现到
const-string v2, "\u767b\u5f55\u6210\u529f"
正是Toast弹出提示的内容,也可以进行修改。
最后面:
:cond_2f
iget-object v1, p0, Lcom/example/demo/AActivity$1;->this$0:Lcom/example/demo/AActivity;
const-string v2, "\u5bc6\u7801\u9519\u8bef"
invoke-static {v1, v2, v5}, 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
则是上面的if eqz v1后为0跳到这里来,实际上就是密码匹配123456不对后跳到这里来提示了密码错误。
上面的源码我们可以修改很多东西,比如修改if条件,加入其他方法执行,但注意不能加入新定义的方法。
这里我们简单就修改了密码为123后保存文件
然后重新转为classex.dex,利用smali.jar工具打包,同样进入文件夹,敲入命令:
java -jar smali-2.0.3.jar -o classes.dex out
后生成了一个新的classex.dex,我们把它替换到apk中去,
然后重新签名,利用signapk.jar工具签名,同样cd到signapk.jar目录下,敲入命令:
java -jar signapk.jar platform.x509.pem platform.pk8 Demo.apk DemoSigned.apk
得到了一个DemoSigned.apk,我们把DemoSigned.apk转载到模拟器上看,输入命令:
adb uninstall com.example.demo
先卸载掉原先的apk,再输入命令安装:
adb install DemoSigned.apk
我们来看看运行:
可以发现我们成功修改了apk,现在输入123456是密码错误了,因为密码验证被我们改成了123.
到此就结束了,后面有机会我再写一些关于smali语法和逆向分析的博文。