Android的代码安全那些事

来源:互联网 发布:教我开淘宝店 编辑:程序博客网 时间:2024/05/20 06:24

Android的代码安全那些事

作为一个正经八百的IT码农,我们有很多途径可以提高我们的“码农”能力,比如说,看书,比如说,浏览技术网站,比如说多写代码。其实根据我的经验呢,还有一种方式–读别人的代码。

大家都知道啊,我们中国人,有个能力挺厉害的,你们猜是啥?

读代码也是有很多途径,比如说下载下载Demo,读读github的代码,读读同事的代码。其实还有一种方式的,反编译商业软件。(不要学坏噢。)

所以说,这是一个不正经的技术分享。

这里写图片描述

为啥呢?总结来说,其实就是借鉴。

为啥要反编译,其实真的不是要获取啥机密资料,干些见光死的勾当。我们其实看看别人都干了些啥。

众所周知,我们人的进化,都是从模仿的过程进化而来的(废话)。还有做好我们自身应用的安全工作,防止比如API被破解啊,核心的一些内容被人知道,为了更好的为了自身的安全,所以去学习。

说到反编译呢,咱们就得先说说编译过程。

Java编译过程

Android的虚拟机是Dalvik,那么我们在非Android平台上默认使用的是Hotspot虚拟机,不仅有Hotsport这个虚拟机实现,其他的还包括JRocket(BEA),j9(IBM),Microsoft JVM等等等等。

各种虚拟机在编译方式,指令集的方式,编译优化,内存管理等上的差别是很大。但是都是根据《Java虚拟机规范》约定开发的,他们的基本的数据管理结构是一样的。

java代码在在正常的编译中,需要经过以下一些步骤,形成java字节码
这里写图片描述

而,Dalvik虚拟机和其他Java虚拟机除了以上的编译步骤是相同的,还是有很多差别的,比如:Dalvik 基于寄存器,而 JVM 基于栈,而由于Dalvik采用的是基于寄存器,所以整个优化和加载,就跟其他的JVM的实现有很多不同。Dalvik虚拟机在编译的时候会将Java字节码转为Dalvik虚拟机可运行的Dalvik字节码。

这里写图片描述

详细可参考 http://blog.csdn.net/dd864140130/article/details/52076515

Android的打包编译过程

这里写图片描述

1.Java编译器对工程本身的java代码进行编译,这些java代码有三个来源:app的源代码,由资源文件生成的R文件(aapt工具),以及有aidl文件生成的java接口文件(aidl工具)。产出为.class文件。

①.用AAPT编译R.java文件
②编译AIDL的java文件
③把java文件编译成class文件

2..class文件和依赖的三方库文件通过dex工具生成Dalvik虚拟机可执行的.dex文件,包含了所有的class信息,包括项目自身的class和依赖的class。产出为.dex文件。

3.apkbuilder工具将.dex文件和编译后的资源文件生成未经签名对齐的apk文件。这里编译后的资源文件包括两部分,一是由aapt编译产生的编译后的资源文件,二是依赖的三方库里的资源文件。产出为未经签名的.apk文件。

4.分别由Jarsigner和zipalign对apk文件进行签名和对齐,生成最终的apk文件。

总结为:编译–>DEX–>打包–>签名和对齐

反编译的过程就是前面提到编译的过程的逆向

说了那么多的编译过程,那么如何反编译呢。

首先需要逆向aapt的过程。这时候要用到apktools的工具。

单独的dex要解析成smali文件需要用到 baksmali工具,如果你要需要smali工具。

如果你还要将dex转为jar,需要用到dex2jar工具。

如果你还要查看jar的代码,还需要用到jd-gui。

需要单独解析编译后的AXML(Android Binary XML),需要用到AXMLPrinter去解码

啊。。工具还是有点多的,其实网上有人已经整理了以上的工具,长这样子的

这里写图片描述

这里写图片描述
回到我们文章的开头,我们如果想说去学习里面的一些原理,使用的框架,结构。把dex转为jar,再查看jar里面的代码,jd-gui的破解效率是比较低的。经常看到一些逻辑混乱的情况,其实我们的Android Studio 也是一个非常好用,反编译源代码的利器。

这里写图片描述

相对而言,Android studio 带的反编译工具,逻辑能够原模原样的还原出来,但是仍会存在一些文件,反编译不出来。

这时候,记得从dex入手,前面说过,dalvik虚拟机是基于寄存器(register)的,那么他的运行指令是跟寄存器挂钩的。Dex不仅可以反编译成字节码文件,还可以反编译成为一种跟寄存器相关的文件—-Smali。通俗的说,smali语言是Dalvik的反汇编语言。

在学习smali之前,对寄存器有点了解是能够更好的学习,分析逻辑。

以下是一个我写的Demo的原先代码。

package test.lemon.decompile;import android.content.Context;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;public class MainActivity extends AppCompatActivity {    Button button;    String string = "显示一段话";    public static int count = 0 ;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        button = (Button) findViewById(R.id.bt_check);        button.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                test(MainActivity.this,string);            }        });    }    public void test(Context context,String msg){        msg +=string;        button.setText(msg);    }}

界面是这样子的,点击按钮之后,方法中的一句话跟MainActivity当中的String拼接。这里写图片描述

这是一个反编译之后的代码

.class public Ltest/lemon/decompile/MainActivity; #说明类的位置.super Landroid/support/v7/app/AppCompatActivity;#父类.source "MainActivity.java"#源文件名称# static fields.field public static count:I #定义一个 公共 静态 int 名称为count的字段# instance fields.field button:Landroid/widget/Button;#定义一个android/widget/Button类型的 名称为button.field string:Ljava/lang/String;# direct methods.method static constructor <clinit>()V #接口初始化方法(名为<clinit>)。在存在静态变量,类变量(本来中)的时候,会执行这个构造函数    .locals 1    .prologue    .line 16    const/4 v0, 0x0 #在寄存器声明一个 “引用” v0 将0x0赋予 给v0    sput v0, Ltest/lemon/decompile/MainActivity;->count:I #用sput操作,将寄存器 v0赋予给count    return-void.end method.method public constructor <init>()V #实例初始化方法(名为<init>)    .locals 1    .prologue    .line 11    invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V #调用父类的构造函数    .line 15    const-string v0, "\u663e\u793a\u4e00\u6bb5\u8bdd"  # unicode码 翻译过来就是 显示一句话    iput-object v0, p0, Ltest/lemon/decompile/MainActivity;->string:Ljava/lang/String; # 将常量值 赋予 给p0的寄存器地址,代指类中的string字段    return-void.end method# virtual methods.method protected onCreate(Landroid/os/Bundle;)V #oncreate函数    .locals 2    .param p1, "savedInstanceState"    # Landroid/os/Bundle; 参数 p1    .prologue    .line 19    invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V # 调用父类    .line 21    const v0, 0x7f04001b   # = 十进制 2130968603 java代码中 ,R。layout.activity_main:    invoke-virtual {p0, v0}, Ltest/lemon/decompile/MainActivity;->setContentView(I)V    .line 25    const v0, 0x7f0b005e    # findViewById 用到 是 R.id.bt_check    # invoke-virtual 调用一个虚方法     invoke-virtual {p0, v0}, Ltest/lemon/decompile/MainActivity;->findViewById(I)Landroid/view/View;    move-result-object v0    #强制转换结果    check-cast v0, Landroid/widget/Button;    iput-object v0, p0, Ltest/lemon/decompile/MainActivity;->button:Landroid/widget/Button;    # .line 指令代码原来的代码行的位置,瞎改都没事,改变了之后,在报错,调试的时候,代码行数位置不对。    .line 26    iget-object v0, p0, Ltest/lemon/decompile/MainActivity;->button:Landroid/widget/Button;    new-instance v1, Ltest/lemon/decompile/MainActivity$1; # MainActivity 是指OnClickListener,实例化,赋给寄存器 v1    invoke-direct {v1, p0}, Ltest/lemon/decompile/MainActivity$1;-><init>(Ltest/lemon/decompile/MainActivity;)V    invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V    .line 32    return-void.end method.method public test(Landroid/content/Context;Ljava/lang/String;)V    .locals 2    .param p1, "context"    # Landroid/content/Context;    .param p2, "msg"    # Ljava/lang/String;    .prologue    .line 35    #注意,注意,原来的 string +=msg;在这里进行了优化,变成了StringBuilder的实现方式,提高了效率    new-instance v0, Ljava/lang/StringBuilder;    invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V    invoke-virtual {v0, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;    move-result-object v0    iget-object v1, p0, Ltest/lemon/decompile/MainActivity;->string:Ljava/lang/String;    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;    move-result-object v0    invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;    move-result-object p2    .line 36    iget-object v0, p0, Ltest/lemon/decompile/MainActivity;->button:Landroid/widget/Button;    invoke-virtual {v0, p2}, Landroid/widget/Button;->setText(Ljava/lang/CharSequence;)V #将值 赋给了setText()    .line 38    return-void.end method

三十行的代码反编译成smali之后一百二十行,去掉换行,源代码跟smali代码两倍多的代码的差距。

smali还是一种比较麻烦而且复杂的语法,但是细细的分析下去,我们能看到一些编译过程之中的优化和语法的含义,还有虚拟机初始化的过程。

比如:

所做的优化

源代码public void test(Context context,String msg){    msg +=string;    button.setText(msg);}

smali

.method public test(Landroid/content/Context;Ljava/lang/String;)V    .locals 2    .param p1, "context"    # Landroid/content/Context;    .param p2, "msg"    # Ljava/lang/String;    .prologue    .line 35    #注意,注意,原来的 string +=msg;在这里进行了优化,变成了StringBuilder的实现方式,提高了效率    new-instance v0, Ljava/lang/StringBuilder;    invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V    invoke-virtual {v0, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;    move-result-object v0    iget-object v1, p0, Ltest/lemon/decompile/MainActivity;->string:Ljava/lang/String;    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;    move-result-object v0    invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;    move-result-object p2    .line 36    iget-object v0, p0, Ltest/lemon/decompile/MainActivity;->button:Landroid/widget/Button;    invoke-virtual {v0, p2}, Landroid/widget/Button;->setText(Ljava/lang/CharSequence;)V #将值 赋给了setText()    .line 38    return-void.end method

该方法当中原来简单的一句话 msg +=string; 已经被优化成为StringBuilder的实现方式,从原来的一句话,变成了句操作。

再比如,我们细细的阅读Smali代码,尝试去理解的时候 ,会发现两个构造函数

# direct methods.method static constructor <clinit>()V.method public constructor <init>()V

他们之间所代表的意义是这样子的。

构造函数是静态的 ,里面包含的一些代码,也是对静态的count字段进行初始化,所代表的意义是“ 装载一个类初始化的时候调用”。就是虚拟机在load一个类的时候,就调用的。。。哦~,原来静态变量,是有单独的构造函数初始化的。

而 也是一个构造函数,这个就是我们常见的构造函数。里面也是对我们类块中的初始化,放到了构造函数当中进行初始化。

这里写图片描述

看了Smali的语法之后,其实干点“调试”的事。

我们原来的test方法中,拼接前面一串字符串,然后丢给button进行显示。现在如果说不丢给button显示,而是要用toast进行显示,我们在test函数下,修改相应的smali文件,重新打包。即可实现。

.line 28const/4 v0, 0x0# 调用Toast的静态方法 将参数 p1,p2,v0喂给makeText invoke-static {p1, p2, v0}, 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

这里写图片描述

即重新生成了新的程序,包含了我们hook之后的代码。

可见,一个软件要被篡改是多么的容易!!!

不仅如此,常见的so库,也可以用IDEA PRO去侦探逻辑,修改逻辑。

所以,可见一个应用的安全是很重要的。

文章如果错误或者描述错误,请指出。

文章部分参考文献

《深入理解Java虚拟机 JVM高级特性与最佳实践》2版 周志明

原创粉丝点击