apk组成与Smali代码

逆向学习

Posted by JovenHe on March 31, 2019

在我看来要想做App的逆向分析,首先要对整个apk文件有一个整体的认知。apk文件就是android系统的应用安装包,它的本质是一个压缩包,我们将它解压后可以看到它的大概构成

apk文件构成

apk解包.png

AndroidManifest.xml是android的清单文件,记录版本号、包名、权限、四大组件等信息

classes.dex是应用内代码经过编译后得到的可用于Dalvik虚拟机执行的文件

lib中存放的是so文件

META-INF中存放的apk的签名相关文件,是apk 中所有文件逐个生成 SHA1 数字签名信息,再 base64 编码,存储在MANIFEST.MF文件中,而通过使用 SHA1withRSA 算法, 用私钥签名生成了 COM_TENC.SF文件,所以如果在反编译修改了某个文件,apk 安装校验时,取到的该文件的摘要与 MANIFEST.MF 中对应的摘要不同,则安装不成功

assets是应用内资源文件,相当于Android项目中的assets文件夹,包括一些字体文件、JS文件、音频、文本、压缩文件等

res是应用内资源文件,相当于Android项目中的res文件夹,包括动画XML文件、drawable、layout布局文件、颜色、strings等文件,在编译时会自动生成索引文件R,在源码中会以R.xxx.yyy来进行使用

resources.arsc是对res目录的一个索引,保存原项目中的drawable、layout、attr、anim、string等文件中的name与id值的对应。

以上是一个正常的apk,但还有一些apk被加固了,本来很大的一个apk文件却只有一个dex文件,而且反编译打开后,会有一些qihoo或者legu等加固平台的标识,这种情况下dex被加壳了,识别上只能凭经验或者第三方查壳工具,然后通过脱壳工具找到真正的dex文件,我自己是通过Fdex2这个插件来进行脱壳的,这个插件依赖于Xposed框架,脱壳原理值得学习一下,就是通过Hook应用的classLoader来找到系统加载真正dex文件的时机,把真正的dex文件写出到文件。

当然为什么有的大厂并不加壳,很大程度上这些加固产品要不断的适配各种型号的手机,CPU 类型,运行模式等等,所以很多 APP 为了考虑兼容性,他们也不会轻易去加固自己的产品。而且厂商在前后端交互上一些安全措施是在后端的,因为不管怎么加固,app都是不安全的。

例子1

在逆向中想要找某一个文本变化的方法,文本内容是”你发起了一笔收款”

直接在strings.xml找到该字符串的名字:

strings.png

name为”ar”,在resources.arsc中进行查找

resources_arsc.png

id为2131230779,从R文件中进行查找2131230779

R文件.png

再进行全局查找aa_msg_receiver_wait_receive,可以找到一个方法

代码使用.png

其实就是在一个方法中进行判断,返回不同信息

Smali语言

apktool这个工具在反编译apk后会得到大量的Smali格式的文件,这些文件就是我们这个项目中除了资源文件外的所有代码所生成的反汇编码,所以其中包括项目中implementation库、jar包、aar包等所有代码文件。Smali其实是一门很简单的汇编语言,是Dalvik虚拟机执行的指令反编译得到的代码,在日常反编译中我们得到的java代码都是工具根据smali得到的,并不是真正的源码,大部分可用,但也会出现反编译出错,或者代码可读性差等错误,所以Smali语言在反编译之路上还是很值得学习的。

学习方法上,我主要是通过对照java代码学习smali语法,以下是我学习过程中总结的一些知识点,参考自吾爱破解论坛教程以及《Android软件安全权威指南》第三章。

数据的表示

I->int Z->boolean B->byte S->short

V->void C->char J->long F->float

D=>double L完整类名->对象类型 [->数组类型

例如:

Ljava/lang/String Landroid/content/Context

int数组 一维是:[I,二维是:[[I

对象类型的数组 例如[Ljava/lang/String

寄存器

寄存器是用来暂时存放参与运算的数据和运算结果

本地寄存器v开头,v0、v1、v2、v3

参数寄存器p开头,如p0、p1、p2、p3

注意在非static函数中p0代指this,p1之后才是参数。

成员变量

.field public/private [static][final] varName:<类型>

例如:

.field private static final Rest_Request:I = 0x33e

指令集

数据定义

const(/4、/16、/hight16)

获取

iget、sget、iget-boolean、sget-boolean、iget-object、sget-object、aget、aget-object等

sget是指获取static fields, iget是指获取instance fields。

aget是用于获取array

例如:

sget-object v1, Lcom/zhongtai/workoffice/db/SharePreUtils;->phone:Ljava/lang/String;

获取SharePreUtils类中的String类型phone这个成员变量并存储在v1这个寄存器中。

iget-object v0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginActivity;->mEtAccount:Landroid/widget/EditText;

iget-object 指令比sget-object多一个参数p0,代表this,指v0=this.mEtAccount.

    iget-object v6, p0, Lcom/zhongtai/workoffice/ui/fragment/RecordSettingFragment;->dataArry:[I

    iget v7, p0, Lcom/zhongtai/workoffice/ui/fragment/RecordSettingFragment;->showFlag:I

    aget v6, v6, v7

先获取v6=this.dataArry;然后获取v7=this.showFlag,最后计算v6=v6[v7]

赋值

iput、sput、iput-boolean、sput-boolean、iput-object、sput-object等

put指令与get指令是一致的。

const/4 v0, 0x1
iput-boolean v0, p0, Lcom/aaa;->IsRegistered:Z

指this.IsRegistered=true;

获取与赋值指令中,带有-object代表是对象类型,不带则是基本类型,boolean类型例外

方法调用指令

一般结构是invoke-xxx {[p0,v0,v1]},Lcom/aaa;->methodName(Lcom/bbb;Lcom/ccc;)Lcom/zz

大括号中是调用该方法的实例+参数,以逗号分隔,大括号后面是函数所在的类,->后面紧跟着函数名,函数名后面括号中是函数的参数,最后是函数返回值。

invoke-static

调用实例的静态方法

例如:

iget-object p1, p0, Lcom/zhongtai/workoffice/ui/activity/LoginActivity;->mContext:Landroid/content/Context;

    const-string v0, "\u4e0d\u80fd\u5b8c\u6210\u767b\u5f55"

    invoke-static {p1, v0}, Lcom/zhongtai/workoffice/tools/ToastUtils;->showTextToast(Landroid/content/Context;Ljava/lang/String;)V

这段的意思是 调用ToastUtils 的showTextToast这个static方法,java源码是

  ToastUtils.showTextToast(mContext,"不能完成登录");
invoke-super

调用实例的父类方法

例如:

.method protected onActivityResult(IILandroid/content/Intent;)V
    .locals 0
    .param p3    # Landroid/content/Intent;
        .annotation build Landroid/support/annotation/Nullable;
        .end annotation
    .end param

.line 242
    invoke-super {p0, p1, p2, p3}, Lcom/zhongtai/workoffice/base/BaseActivity;->onActivityResult(IILandroid/content/Intent;)V
    
    return-void
.end method

java源码是

protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        
    }
invoke-direct

调用实例的直接方法(非静态private)

例如:

const v0, 0x7f0800d6
    .line 93
    invoke-direct {p0, v0}, Lcom/zhongtai/workoffice/ui/activity/MainActivity;->clickBottom(I)V

0x7f0800d6是R.id.rb_note的值。 java源码是

this.clickBottom(R.id.rb_note)
invoke-virtual

调用实例的虚方法(非静态的public和protect方法)

.line 92
    iget-object v0, p0, Lcom/zhongtai/workoffice/ui/activity/MainActivity;->mSwipeBackLayout:Lme/imid/swipebacklayout/lib/SwipeBackLayout;

    const/4 v1, 0x0
    invoke-virtual {v0, v1}, Lme/imid/swipebacklayout/lib/SwipeBackLayout;->setEnableGesture(Z)V

java源码是:

mSwipeBackLayout.setEnableGesture(false);
invoke-interface

调用实例的接口方法.

注意:平时使用的比较多的list和map集合的一些操作,add/addAll/put/get/keySet/iterator等其实都是invoke-interface 来实现的。

例如:

//v0=this.mListener;
iget-object v0, p0, Lcom/zhongtai/workoffice/ui/fragment/WorkbenchFragment;->mListener:Lcom/zhongtai/workoffice/ui/fragment/WorkbenchFragment$RefreshListener;

//v0.onRefreshListener();
    invoke-interface {v0}, Lcom/zhongtai/workoffice/ui/fragment/WorkbenchFragment$RefreshListener;->onRefreshListener()V

java源码是:

 mListener.onRefreshListener();
invoke-xxxx/range

当函数的参数个数>=5时,指令需要加上/range,使用格式:

invoke-xxxx/range {v0..vn},Lcom/aaa;->methodName(Lcom/bbb;Lcom/ccc;)Lcom/zz

//v0.cropImg(v1,v2,v3,v4,v5);
invoke-virtual/range {v0 .. v5}, Lcom/zhongtai/workoffice/tools/ImageSetUtils;->cropImg(Landroid/support/v4/app/Fragment;ZZLandroid/content/Intent;Ljava/lang/String;)V

java源码是:

imageSetUtils.cropImg(this, false, true, data, "image_hd");

函数接收

要知道函数有的有返回值,有的无返回值。对于有返回值的,就需要有寄存器接收,所以就有move-result与move-result-object。

move-result-object是接收对象类型,move-result是接收基本数据类型。

例如

invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
//v1=v1.toString();
    move-result-object v1
invoke-virtual {p1}, Ljava/lang/Boolean;->booleanValue()Z
//p1=p1.booleanValue();
    move-result p1

条件跳转

if-xx vA, vB, :cond_** 两者对比,满足条件则跳转。

if-xxz vA, :cond_** vA与zero对比,满足条件则跳转。

if-eq equal的缩写,条件是等于

if-ne not equal 的缩写,条件是不等于

if-lt less than的缩写,条件是小于

if-le less than or equal to 的缩写,条件是小于等于

if-gt greater than 的缩写,条件是大于

if-ge greater than or equal to 的缩写,条件是大于等于

例如:

.method public showLoadingDialog()V
    .locals 1

    .line 155
    invoke-virtual {p0}, Lcom/zhongtai/workoffice/base/BaseFragment;->getActivity()Landroid/support/v4/app/FragmentActivity;

    move-result-object v0 //v0=getActivity();

    invoke-virtual {v0}, Landroid/support/v4/app/FragmentActivity;->isFinishing()Z

    move-result v0//v0=getActivity().isFinishing();

    if-eqz v0, :cond_0//v0==false跳转

    return-void

    .line 158
    :cond_0
    iget-object v0, p0, Lcom/zhongtai/workoffice/base/BaseFragment;->mLoadingDialog:Lcom/zhongtai/workoffice/custom/LoadingDialog;

    if-nez v0, :cond_1 //mLoadingDialog!=null跳转

    .line 159
    invoke-direct {p0}, Lcom/zhongtai/workoffice/base/BaseFragment;->initDialog()V

    .line 161
    :cond_1
    iget-object v0, p0, Lcom/zhongtai/workoffice/base/BaseFragment;->mLoadingDialog:Lcom/zhongtai/workoffice/custom/LoadingDialog;

    if-eqz v0, :cond_2 //mLoadingDialog==null跳转

    iget-object v0, p0, Lcom/zhongtai/workoffice/base/BaseFragment;->mLoadingDialog:Lcom/zhongtai/workoffice/custom/LoadingDialog;

    invoke-virtual {v0}, Lcom/zhongtai/workoffice/custom/LoadingDialog;->isShowing()Z

    move-result v0 //v0=mLoadingDialog.isShowing()

    if-nez v0, :cond_2 //v0!=false

    .line 162
    iget-object v0, p0, Lcom/zhongtai/workoffice/base/BaseFragment;->mLoadingDialog:Lcom/zhongtai/workoffice/custom/LoadingDialog;

    invoke-virtual {v0}, Lcom/zhongtai/workoffice/custom/LoadingDialog;->show()V

    :cond_2
    return-void
.end method

循环

从上面的条件跳转可以看出,循环从某些方面可以看作是方法的循环调用,只要在符合跳出条件就可以跳出循环。

Smali文件构成

baksmali在反编译dex文件的时候,会为每个类单独生成了一个smali文件,内部类作为一个独立的类,它也拥有自己独立的smali文件,只是内部类的文件名形式为“[外部类]$[内部类].smali”

摘录来自: 丰生强. “Android软件安全与逆向分析 (图灵原创)”。

一个Smali文件通常由头部信息、变量、构造方法、

头部信息

.class public Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;//包名+类名
.super Lcom/zhongtai/workoffice/base/BaseActivity;//父类
.source "LoginQuickActivity.java"//来源自哪一个java文件

# interfaces
.implements Landroid/view/View$OnClickListener;//接口实现

对应到java中就是

package com.zhongtai.workoffice.ui.activity;

public class LoginQuickActivity extends BaseActivity implements View.OnClickListener

变量

# static fields //静态变量
.field public static From:Ljava/lang/String; = "From"
.field private static final Rest_Request:I = 0x33e

# instance fields //普通实例
.field private mEtCode:Lcom/zhongtai/workoffice/custom/PwdEditText;
.field private mEtPhone:Landroid/widget/EditText;
.field private mHandler:Landroid/os/Handler;
.field private mTvGetCode:Landroid/widget/TextView;
.field private mTvLogin:Landroid/widget/TextView;
.field private mType:Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity$Type;
.field private miTime:I
.field private strCode:Ljava/lang/String;
.field private strPhone:Ljava/lang/String;
.field private strPhoneLogin:Ljava/lang/String;

构造方法

.method static constructor <clinit>()V	//初始化静态变量构造方法
    .locals 0 //代表函数中本地寄存器的个数
    return-void
.end method

.method public constructor <init>()V	//默认构造方法
    .locals 1
    .line 37
    invoke-direct {p0}, Lcom/zhongtai/workoffice/base/BaseActivity;-><init>()V //调用函数-父类的构造方法
    const v0, 0xea60 //声明常量v0=60000
    .line 53
    iput v0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->miTime:I //把v0赋值给this.miTime
    .line 54
    new-instance v0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity$1; //创建实例v0
    invoke-direct {v0, p0}, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity$1;-><init>(Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;)V //调用v0的构造方法,并传入参数p0
    iput-object v0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->mHandler:Landroid/os/Handler; //把v0赋值给this.mHandler;
    return-void
.end method

声明匿名内部类调用方法

既然匿名内部类会生成另一个smali文件,那它如何调用主类中的private变量呢,就是通过声明static方法来让匿名内部类调用

.method static synthetic access$000(Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;)I
    .locals 0

    .line 37
    iget p0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->miTime:I

    return p0
.end method

.method static synthetic access$002(Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;I)I
    .locals 0

    .line 37
    iput p1, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->miTime:I

    return p1
.end method

.method static synthetic access$100(Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;)Landroid/os/Handler;
    .locals 0

    .line 37
    iget-object p0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->mHandler:Landroid/os/Handler;

    return-object p0
.end method

普通方法

除了上面提到的方法外,就是java文件中的public/private/static方法,以及重写的父类方法。

.method public initPresenter()Lcom/zhongtai/workoffice/base/BaseContract$BasePresenter;
    .locals 1

    const/4 v0, 0x0

    return-object v0
.end method

.method public initViews()V
    .locals 1

    const v0, 0x7f080057

    .line 88
    invoke-virtual {p0, v0}, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/widget/EditText;

    iput-object v0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->mEtPhone:Landroid/widget/EditText;

    const v0, 0x7f080054

    .line 89
    invoke-virtual {p0, v0}, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Lcom/zhongtai/workoffice/custom/PwdEditText;

    iput-object v0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->mEtCode:Lcom/zhongtai/workoffice/custom/PwdEditText;

    const v0, 0x7f080154

    .line 90
    invoke-virtual {p0, v0}, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/widget/TextView;

    iput-object v0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->mTvGetCode:Landroid/widget/TextView;

    .line 91
    iget-object v0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->mTvGetCode:Landroid/widget/TextView;

    invoke-virtual {v0, p0}, Landroid/widget/TextView;->setOnClickListener(Landroid/view/View$OnClickListener;)V

    const v0, 0x7f08015d

    .line 92
    invoke-virtual {p0, v0}, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/widget/TextView;

    iput-object v0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->mTvLogin:Landroid/widget/TextView;

    .line 93
    iget-object v0, p0, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->mTvLogin:Landroid/widget/TextView;

    invoke-virtual {v0, p0}, Landroid/widget/TextView;->setOnClickListener(Landroid/view/View$OnClickListener;)V

    return-void
.end method

.method protected onActivityResult(IILandroid/content/Intent;)V
    .locals 0

    .line 266
    invoke-super {p0, p1, p2, p3}, Lcom/zhongtai/workoffice/base/BaseActivity;->onActivityResult(IILandroid/content/Intent;)V

    const/4 p3, -0x1

    if-ne p2, p3, :cond_0

    const/16 p2, 0x33e

    if-ne p1, p2, :cond_0

    .line 269
    invoke-virtual {p0}, Lcom/zhongtai/workoffice/ui/activity/LoginQuickActivity;->finish()V

    :cond_0
    return-void
.end method

注意

一个简单的Smali文件构成到此已经结束,但一些不常用的指令,比如xor(异或)还是需要查询

总结

smali语法还是在于多看多用,我也总是一段时间不用就忘。也有不少人包括我,一开始认为Smali不重要,觉得Jadx都能得到java代码了,完全没必要学Smali,但后期不管是Jadx遇到问题,还是反混淆,重新打包都需要最基础的阅读Smali的能力。从某种程度上来说Android Killer这个软件还是很有必要多学多用的。