第五章 静态分析 Android 程序(一)(阅读 smali 代码)

文章目录

  • 静态分析 Android 程序
    • 静态分析简介
      • 静态分析 Android 程序的方法
    • 阅读 smali 代码
      • smali 文件结构
        • 类声明
        • 注释
        • 字段
        • 注解
        • 方法
      • 循环语句
      • switch 分支语句
      • try/catch 语句

静态分析 Android 程序

  • 静态分析是探索 Android 程序内幕的一种最常见方法,与动态调试一起,能帮助分析人员解决分析时遇到的各类问题
  • 要求:具备较强的代码理解能力

静态分析简介

  • 静态分析(Static Analysis):在不运行代码的情况下,用词法分析、语法分析等技术手段对程序文件扫描,生成反汇编代码,通过阅读反汇编代码掌握程序功能的一种技术
  • 但实际分析时,完全不运行程序不太可能,往往要先运行目标程序寻找程序的突破口
  • 所谓“静态”,即分析过程中主要的工作是阅读反汇编代码
  • 生成反汇编代码的工具称反汇编工具或反编译工具,工具越强大,反汇编效果越好,能事半功倍

静态分析 Android 程序的方法

  • 阅读反汇编生成的 Dalvik 字节码。可用 IDA Pro 分析 DEX 文件,也可用文本编辑器阅读 baksmali 反编译生成的 smali 文件
  • 阅读反汇编生成的 Java 源码,可用 dex2jar 生成 jar 文件,然后用 jd-gui 阅读 jar 文件的代码

阅读 smali 代码

  • ApkTool 和 baksmali 反编译 APK 或 DEX 的结果就是 smali 文件
  • 想要操作 smali 来改变 DEX 的一些行为,就必须对 smali 的文件与指令格式有所了解

smali 文件结构

  • 虽然 DEX 使用的是 Dalvik 指令集,但它最初是用 Java 开发的,Java 中的类、方法、字段、注解等概念与 DEX 中的是相同的。将原生 Java 程序编译成 class 时,会将每个 Java 类存放到单独的 class 中。若一个类包含了多个内部类,那每个内部类也会单独保存。DEX 的 smali 文件与之类似,每个 DEX 中的内部类在反编译时会用单独的 smali 文件存放
  • 以 Crackme0201.apk 为例,用 ApkTool 反编译后,查看其目录结构,会发现存在 MainActivity$1.smaliR$anim.smali 这类文件名中带美元符号的 smali 文件,这些文件就是内部类 smali 文件
    apktool d app-release.apk
    第五章 静态分析 Android 程序(一)(阅读 smali 代码)_第1张图片
  • MainActivity$1.smali 为例,执行如下命令查看其内容
    第五章 静态分析 Android 程序(一)(阅读 smali 代码)_第2张图片
  • 从输出信息可看出:MainActivity$1.smali 中存放的是原文件名;.resource 指令中指定的是 MainActivity.java;.implements 指令告诉我们,这个类实现了 View 类的 OnClickListener 接口,“Landroid/view/View$OnClickListener;”这一长串指令的内容是 Java 的类或方法的完整签名
  • 以 MainActivity.smali 为例,一个 smali 文件的结构如下:
    第五章 静态分析 Android 程序(一)(阅读 smali 代码)_第3张图片

类声明

  • 在 smali 文件的开头,.class.super 指令会指明 smali 文件所保存的类的完整签名,和类的父类的完整签名
  • DEX 文件中的每一个类的签名都是不同的

注释

  • smali 文件中会自动生成一些注释,如:“# instance fields”和“# virtual methods”表示 smali 文件中接下来存放的是哪种类型的信息;“# instance fields”表示实例字段,在它之后,每个类中的字段都会以 .field 指令声明,后面跟着字段的访问权限和完整的签名;“# virtual methods”表示接下来存放的是类的虚方法
  • smali 文件只支持单行注释,注释可放在一行开头,也可放在一行的任何地方,使用 # 标识注释的开始,井号后的部分都是注释

字段

  • 若类中有字段,会在“# instance fields”下面用 .field 指令声明

注解

  • 若类中包含 Java 注解,在 smali 文件中会以“# annotations”注释加以说明,接着会以 .annotation 指令开始,以 .end annotation 指令结束(声明一个注解)。.annotation 指令后跟着注解的类型和完整的签名,若类中有多个注解,则会有多个指令对
  • 上面的 MainActivity.smali 中没有注解

方法

  • DEX 中的类包含类自己实现的实例方法“# direct methods”和继承自父类的虚方法“# virtual methods”。每个方法以 .method 指令开始,以 .end method 指令结束,.method 指令后跟着方法的访问权限和完整的签名
  • 在一个方法中,除了真实执行的机器指令,还有其他指令,列举如下:
  • .locals:声明当前方法中使用的寄存器数目。对 DEX 中的每个方法,都可通过静态分析知道其使用了多少个寄存器。这样做的好处是,虚拟机执行 DEX 中类的方法时可提前为方法准备栈空间
  • .param:指定方法中的参数名,以便程序的调试。经过 Proguard(混淆工具)处理的 DEX 可能不含该信息
  • .prologue:表示下面的部分是 DEX 指令
  • .line:保存 DEX 中的方法在 Java 源文件中的行号信息,以便调试。经过 Proguard 处理的 DEX 可能不含该信息

循环语句

  • 循环语句是程序开发中最常用的语法结构。Android 开发中常见的循环结构有:迭代器循环、for 循环、while 循环、do while 循环
  • 迭代器循环形式如下:
// 形式一
Iterator<对象> <对象名> = <方法返回一个对象列表>;
for (<对象> <对象名>: <对象列表>) {
    [处理单个对象的代码体];
}

// 形式二
Iterator<对象> <迭代器> = <方法返回一个迭代器>;
while (<迭代器>.hasNext()) {
    <对象> <对象名> = <迭代器>.next();
    [处理单个对象的代码体];
}
  • 第一种形式的迭代是在 for 关键字中将对象名和对象列表用冒号分隔,然后在循环体中直接访问单个对象。这种形式的代码简练、可读性好,编程中使用颇多

  • 第二种形式是手动获取一个迭代器,然后在一个循环中调用迭代器中的 hasNext() 检测其是否为空,最后在循环体中调用其 next() 遍历迭代器

  • 现在,反编译实例程序 Circulate(实例代码:GitHub)。打开反编译工程 smali/com/droider/circulate 目录下的 MainActivity.smali 文件,找到 iterator() 方法,代码如下:

.method private iterator()V
    .locals 4

    const-string v0, "activity"

    .line 42
    invoke-virtual {p0, v0}, Lcom/droider/circulate/MainActivity;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;    # 获取 ActivityManager

    move-result-object v0

    check-cast v0, Landroid/app/ActivityManager;

    .line 44
    invoke-virtual {v0}, Landroid/app/ActivityManager;->getRunningAppProcesses()Ljava/util/List;

    move-result-object v0    # 正在运行的进程列表

    .line 45
    new-instance v1, Ljava/lang/StringBuilder;    # 新建一个 StringBuilder 对象
    # 调用 StringBuilder 的构造方法
    invoke-direct {v1}, Ljava/lang/StringBuilder;->()V

    .line 46
    # 获取进程列表的迭代器
    invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator;

    move-result-object v0

    :goto_0        # 迭代循环开始
    invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z    # 开始迭代

    move-result v2
    # 若迭代器为空就跳走
    if-eqz v2, :cond_0

    invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object;
    # 循环获取每一项
    move-result-object v2

    check-cast v2, Landroid/app/ActivityManager$RunningAppProcessInfo;

    .line 47
    # 新建一个临时的 StringBuilder 对象
    new-instance v3, Ljava/lang/StringBuilder;

    invoke-direct {v3}, Ljava/lang/StringBuilder;->()V
    # 获取进程名
    iget-object v2, v2, Landroid/app/ActivityManager$RunningAppProcessInfo;->processName:Ljava/lang/String;

    invoke-virtual {v3, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    # 换行符
    const-string v2, "\n"
    # 组合进程名和换行符
    invoke-virtual {v3, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

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

    move-result-object v2
    # 将组合后的字符串添加到 StringBuilder 末尾
    invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    # 跳到循环开始处
    goto :goto_0

    .line 50
    :cond_0
    # 将 StringBuilder 转为字符串
    invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v0

    const/4 v1, 0x0

    .line 49
    invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    .line 51
    # 用 Toast 弹出 StringBuilder 的内容
    invoke-virtual {v0}, Landroid/widget/Toast;->show()V
    # 方法返回
    return-void
.end method
  • 上面这段代码的功能是获取正在运行的进程列表,然后用 Toast 弹出所有进程名。获取正在运行的进程列表用的是 ActivityManager 类的 getRunningAppProcesses(),该方法返回一个 List 对象。

  • 上面的代码中,调用了 List 的 iterator() 获取进程列表的迭代器,然后从 goto_0 处开始进入迭代循环。在循环中,首先调用迭代器的 hasNext() 检测迭代器是否为空。若迭代器为空,就跳转到 cond_0 处,调用 Toas 弹出所有的进程信息;若迭代器不为空,则说明迭代器中的内容还没取完,要调用迭代器的 next() 获取单个 RunningAppProcessInfo 对象,然后新建一个临时的 StringBuilder,将进程名与换行符组合后添加到循环开始前创建的 StringBuilder 中,最后用 goto 语句跳转到循环开始处

  • 可看出,这段代码与前面列出的 while 循环声明很相似,其实,第一种迭代器循环展开后就是第二种循环的实现,尽管它们的 Java 代码不同,但生成的反汇编代码很相似

  • 迭代器循环的特点:

  • 迭代器循环会调用迭代器的 hasNext() 检测循环条件是否满足

  • 迭代器循环会调用迭代器的 next() 获取单个对象

  • 迭代器循环会使用 goto 指令控制代码的流程

  • for 形式的迭代器循环展开后即 while 形式的迭代器循环

  • 接下来是传统的 for 循环。来看 MainActivity.smali 文件中的 forCirculate(),代码:

.method private forCirculate()V
    .locals 7

    .line 56
    invoke-virtual {p0}, Lcom/droider/circulate/MainActivity;->getApplicationContext()Landroid/content/Context;

    move-result-object v0
	# 获取 PackageManager
    invoke-virtual {v0}, Landroid/content/Context;->getPackageManager()Landroid/content/pm/PackageManager;

    move-result-object v0

    const/16 v1, 0x2000

    .line 58
	# 获取已安装程序列表
    invoke-virtual {v0, v1}, Landroid/content/pm/PackageManager;->getInstalledApplications(I)Ljava/util/List;

    move-result-object v0

    .line 60
	# 获取列表中 ApplicationInfo 对象个数
    invoke-interface {v0}, Ljava/util/List;->size()I

    move-result v1

    .line 61
	# 新建 StringBuilder 对象
    new-instance v2, Ljava/lang/StringBuilder;
	# 调用 StringBuilder 的构造方法
    invoke-direct {v2}, Ljava/lang/StringBuilder;->()V

    const/4 v3, 0x0
	# v4 = 0
    const/4 v4, 0x0
	# 循环开始
    :goto_0
	# v4 = 0,v1 = ApplicationInfo 对象个数
	# 若 v4 大于等于 v1 就跳转,即 v1 < 0 时跳转到 cond_0 处
    if-ge v4, v1, :cond_0

    .line 63
	# 获取单个 ApplicationInfo 对象
    invoke-interface {v0, v4}, Ljava/util/List;->get(I)Ljava/lang/Object;

    move-result-object v5

    check-cast v5, Landroid/content/pm/ApplicationInfo;

    .line 64
	# 新建临时的 StringBuilder 对象
    new-instance v6, Ljava/lang/StringBuilder;

    invoke-direct {v6}, Ljava/lang/StringBuilder;->()V
	# 获取包名
    iget-object v5, v5, Landroid/content/pm/ApplicationInfo;->packageName:Ljava/lang/String;
	# 将包名添加到临时的 StringBuilder 中
    invoke-virtual {v6, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
	# 换行符
    const-string v5, "\n"
	# 组合包名和换行符
    invoke-virtual {v6, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
	# 转换为字符串
    invoke-virtual {v6}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v5
	# 添加到循环外的 StringBuilder 中
    invoke-virtual {v2, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
	# 下一个索引
    add-int/lit8 v4, v4, 0x1
	# 跳转到循环开始处
    goto :goto_0

    .line 67
    :cond_0
    invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v0

    .line 66
	# 构造 Toast
    invoke-static {p0, v0, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    .line 68
	# 显示已安装程序列表
    invoke-virtual {v0}, Landroid/widget/Toast;->show()V
	# 方法返回
    return-void
.end method
  • 上面这段代码的功能是获取已安装的程序,用 Toast 弹出所有的软件包名。获取已安装程序用的是 PackageManager 类的 getInstalledApplications(),首先创建一个 StringBuilder 对象存放所有的字符串信息,接着初始化 v4 寄存器为 0(作为获取列表项的索引)。for 循环的起始处是 goto_0 标号。循环条件的代码为 if-ge v4, v1, :cond_0,其中 v4 为索引值,v1 为列表中 ApplicationInfo 的个数,cond_0 标号处为循环结束后的代码,若索引没有完成(没找到最后一项),代码会顺序执行(循环体中代码);若索引完成(找到最后一项),会跳转到 cond_0 处执行 Toast 以显示所有字符串信息。if-ge 下的第一行代码调用 List 的 get() 获取列表中单个 ApplicationInfo 对象,然后将包名和换行符组合后添加到之前声明的 StringBuilder 中,最后将 v4 索引值加 1 后调用 goto :goto_0 语句跳转到循环开始处

  • for 循环代码的特点:

  • 进入循环前,要初始化循环计数器变量,且其值要在循环体中更改

  • 循环条件判断可以是由条件跳转指令组合成的合法语句

  • 在循环中用 goto 指令控制代码的流程

  • while、do while 循环的结构差异不大,二者的代码和前面的迭代器循环的代码非常相似,只是循环条件判断的位置不同

switch 分支语句

  • 这也是比较常见的语句结构,常出现在判断分支比较多的代码中
  • 实例:SwitchCase
  • 实例代码:GitHub
  • 用 ApkTool 反编译实例 SwitchCase,然后打开反编译后的工程目录中的 smali/com/droider/switchcase/MainActivity.smali 文件,找到 packedSwitch() 的代码:
.method private packedSwitch(I)Ljava/lang/String;
    .locals 1
	# 若 p1 = 0 就跳转到 cond_3 处
	# p1 为传进来的参数
    if-eqz p1, :cond_3
	# v0 = 1
    const/4 v0, 0x1
	# 若 p1 = 1 就跳转到 cond_2 处
    if-eq p1, v0, :cond_2
	# v0 = 2
    const/4 v0, 0x2
	# 若 p1 = 2 就跳转到 cond_1 处
    if-eq p1, v0, :cond_1
	# v0 = 3
    const/4 v0, 0x3
	# 若 p1 = 3 就跳转到 cond_0 处
    if-eq p1, v0, :cond_0
	# 若能执行到这儿,说明到了 default
	# 定义一个字符串
    const-string p1, "She is a person"
	# 跳转到 goto_0 处
    goto :goto_0
	# 当 p1 = 3 时定义的字符串
    :cond_0
    const-string p1, "She is a obasan"
	# 跳转到 goto_0 处
    goto :goto_0
	# 当 p1 = 2 时定义的字符串
    :cond_1
    const-string p1, "She is a women"
	# 跳转到 goto_0 处
    goto :goto_0
	# 当 p1 = 1 时定义的字符串
    :cond_2
    const-string p1, "She is a girl"
	# 跳转到 goto_0 处
    goto :goto_0
	# 当 p1 = 0 时定义的字符串
    :cond_3
    const-string p1, "She is a baby"
	# 所有 case 的出口
    :goto_0
	# 返回字符串 p1
    return-object p1
.end method
  • 不知是用了 3.5.3 版本的 AS 编写 SwitchCase 实例,还是什么原因,这里生成的 smali 代码和书上的完全不一样,书上涉及了 packed-switch 指令,还有 case 区域,难很多;而上面生成的 smali 代码很容易看懂,就知识单纯的跳转而已,基本在注释里已分析完毕
  • 实例中的第二个方法 sparseSwitch() 虽然 case 的范围变大,也不连续,但生成的 smali 跟第一个方法的原理一样,因此没有分析的必要
  • 个人认为上面的 switch-case 代码比书上的相比,可读性更好,易懂,执行效率更高

try/catch 语句

  • 实际编写代码时,各种无法预料的结果都可能出现,为尽可能多地捕捉异常信息,有必要在代码中使用 try/catch 语句将可能产生问题的代码包起来
  • 实例:TryCatch
  • 实例代码:GitHub
  • 用 ApkTool 反编译实例 TryCatch,然后打开反编译后的工程目录中的 smali/com/droider/trycatch/MainActivity.smali 文件,找到 tryCatch() 的代码:
.method private tryCatch(ILjava/lang/String;)V
    .locals 5

    const/4 v0, 0x0

    .line 21
	# 第一个 try 语句块开始
    :try_start_0
	# 将第二个参数转换为 int 类型
    invoke-static {p2}, Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I

    move-result p2
	# 第一个 try 语句块结束
    :try_end_0
	# catch_1
    .catch Ljava/lang/NumberFormatException; {:try_start_0 .. :try_end_0} :catch_1
	# 若出现异常,这里不会执行,会跳转到 catch_1 标号处
    .line 23
	# 第二个 try 语句块开始
    :try_start_1
	# v1 = 第一个参数除以第二个参数(转换为 int 后)
    div-int v1, p1, p2
	# v2 = 上面的商乘以第二个参数(即人数)
    mul-int v2, v1, p2
	# v2 = 第一个参数(即鸡腿数)减去上面的积,现在 v2 为余数
    sub-int v2, p1, v2
	# Unicode 字符串“共有 %d 只鸡腿,%d 个人平分,每人可分 %d 只,还剩 %d 只”
    const-string v3, "\u5171\u6709 %d \u53ea\u9e21\u817f\uff0c%d \u4e2a\u4eba\u5e73\u5206\uff0c\u6bcf\u4eba\u53ef\u5206 %d \u53ea\uff0c\u8fd8\u5269 %d \u53ea"

    const/4 v4, 0x4

    new-array v4, v4, [Ljava/lang/Object;

    .line 26
    invoke-static {p1}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;

    move-result-object p1

    aput-object p1, v4, v0

    const/4 p1, 0x1

    invoke-static {p2}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;

    move-result-object p2

    aput-object p2, v4, p1

    const/4 p1, 0x2

    invoke-static {v1}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;

    move-result-object p2

    aput-object p2, v4, p1

    const/4 p1, 0x3

    invoke-static {v2}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;

    move-result-object p2

    aput-object p2, v4, p1

    .line 25
	# 格式化字符串
    invoke-static {v3, v4}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;

    move-result-object p1

    .line 27
    invoke-static {p0, p1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object p1

    .line 29
	# 用 Toast 显示格式化字符串
    invoke-virtual {p1}, Landroid/widget/Toast;->show()V
	# 第二个 try 语句块结束
    :try_end_1
	# catch_0
    .catch Ljava/lang/ArithmeticException; {:try_start_1 .. :try_end_1} :catch_0
	# catch_1
    .catch Ljava/lang/NumberFormatException; {:try_start_1 .. :try_end_1} :catch_1
	# 跳转到 goto_0 标号处,即方法返回处
    goto :goto_0

    :catch_0
	# 第三个 try 语句块开始
    :try_start_2
	# 字符串“人数不能为 0”
    const-string p1, "\u4eba\u6570\u4e0d\u80fd\u4e3a 0"

    .line 32
    invoke-static {p0, p1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object p1

    .line 34
	# 用 Toast 显示产生异常的原因
    invoke-virtual {p1}, Landroid/widget/Toast;->show()V
	# 第三个 try 语句块结束
    :try_end_2
    .catch Ljava/lang/NumberFormatException; {:try_start_2 .. :try_end_2} :catch_1
	# 跳转到 goto_0 处,即方法返回处
    goto :goto_0

    :catch_1
	# 字符串“无效的数值字符串”
    const-string p1, "\u65e0\u6548\u7684\u6570\u503c\u5b57\u7b26\u4e32"

    .line 38
    invoke-static {p0, p1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object p1

    .line 40
	# 用 Toast 显示异常产生的原因
    invoke-virtual {p1}, Landroid/widget/Toast;->show()V

    :goto_0
	# 方法返回
    return-void
.end method
  • 上面这段代码的功能很简单:输入鸡腿的数量和人数,然后用 Toast 弹出鸡腿的分配方案。在传入人数时,为了演示 try/catch 语句的效果,用了 String 类型。在两种情况下会产生异常:一、将 String 类型转换成 int 类型时可能会产生 NumberFormatException 异常;二、计算分配方案时除数为 0 会产生 ArithmeticException 异常
  • 代码中的 try 语句块使用以 try_start 开头的标号注明,以 try_end 开头的标号结束。第一个 try 语句的开头标号为 try_start_0,结束标号为 try_end_0。在使用多个 try 语句块时,标号名称后的数值是递增的(上面的代码中用到了 try_end_2)
  • 在 try_end_0 标号下面用 .catch 指令指定所处理的异常类型和 catch 的标号,格式:
    .catch <异常类型> { .. }
  • 查看 catch_1 标号处的代码,会发现若在将 String 类型转换为 int 类型时产生异常,就会弹出提示“无效的数值字符串”。对于代码中的汉字,ApkTool(或 baksmali)在反编译时会对其用 Unicode 编码,阅读代码时使用编码转换工具(或在线网站)转换一下会更好分析
  • 再次仔细阅读上面的代码,会发现在 try_end_1 标号下用 .catch 指令定义了 catch_0 和 catch_1 两个 catch。在 catch_0 标号的下面有一个标号为 try_start_2 的 try 语句块,其实这个语句块是虚构的。假设有如下代码:
private void a() {
	try {
		...
		try {
			...
		}
		catch (XXX) {
			...
		}
	}
	catch (YYY) {
		...
	}
}
  • 假设在上面的代码中,执行内部的 try 语句时发生了异常。若异常类型是 XXX,则内部 catch 会捕捉该异常并执行相应的处理代码;若异常类型不是 XXX,就会到外层的 catch 中去查找异常处理代码,这也是实例的 try_end_1 标号下有两个 catch 的原因。若在执行 XXX 异常的处理代码时又产生异常,怎么办?此时这个异常就会扩散到外层的 catch 中去。由于 XXX 异常的外层只有一个 YYY 的异常处理,这时会判断产生的异常是否为 YYY 类型,若是,就会处理;若不是,则将异常抛给应用程序。回到本实例,若在执行内部的 ArithmeticException 异常处理时产生其他异常,就会调用外层的 catch 进行异常捕捉。这样就能很容易理解为什么 try_end_2 标号下会有一个 catch_1 了
  • Dalvik 指令集中没有与 try/catch 相关的指令,在处理 try/catch 语句时是通过相关的数据结构保存异常信息的。第四章学过 DEX 文件格式的 DexCode 数据结构,其声明如下:
struct DexCode {
	u2 registersSize;		// 使用的寄存器个数
	u2 insSize;		// 参数个数
	u2 outsSize;		// 调用其他方法时使用的寄存器个数
	u2 triesSize;		// try/catch 语句的个数
	u4 debugInfoOff;		// 指向调试信息的偏移量
	u4 insnsSize;		// 指令集的个数,以 2 字节为单位
	u2 insns[1];		// 指令集
	// 2 字节空间用于结构对齐
	//try_item[triesSize];		// DexTry 结构
	// try/catch 语句中 handler 的个数
	// catch_handler_item[handlersSize];		// DexCatchHandler 结构
};
  • 在该结构下面的 try_item 中保存了 try 语句的信息,其结构 DexTry 声明如下:
struct DexTry {
	u4 startAddr;		// 起始地址
	u2 insnCount;		// 指令数量
	u2 handleOff;		// handler 的偏移量
};
  • 每个 DexTry 中保存了 try 语句的起始地址和指令数量,这就能计算出 try 语句块包含的地址范围。在 try_item 字段下面就是 handler 的个数
  • 现在来看看 DEX 中存储的 try/catch 信息。由于 TryCatch 实例的类个数较多,手动查找速度太慢,这里用 Android SDK 中的 dexdump 工具(第三章用过)。使用解压软件(如 7-Zip)取出实例中的 classes.dex,然后执行如下命令查看 tryCatch() 的内容:
    第五章 静态分析 Android 程序(一)(阅读 smali 代码)_第4张图片
  • 从上面的输出信息可看出,tryCatch() 是私有方法,用了 8 个寄存器,共 75 条指令,其中有三个 try 语句块,共有两个异常处理 handler。0x0001 ~ 0x0005 为第一个 try 语句块的代码范围,tryCatch() 的代码位于 0x503ac 处。计算第一个 try 语句块的代码范围:
    (0x503ac + 1 * 2) ~ (0x503ac + 5 * 2) = 0x503ae ~ 0x503b6
  • 同样,可通计算得到第二和第三个 try 语句块的代码范围,分别是 0x503b6 ~ 0x50418 和 0x5041a ~ 0x5042c
  • 最后,将这段 smali 代码整理为 Java 代码:
private void tryCatch(int drumsticks, String people) {
        try {
            int i = Integer.parseInt(people);
            try {
                int m = drumsticks / i;
                int n = drumsticks - m * i;
                String str = String.format("共有 %d 只鸡腿,%d 个人平分," +
                        "每人可分 %d 只,还剩 %d 只", drumsticks, i, m, n);
                Toast.makeText(this,
                        str,
                        Toast.LENGTH_SHORT).show();
            }
            catch (ArithmeticException e) {
                Toast.makeText(this,
                        "人数不能为 0",
                        Toast.LENGTH_SHORT).show();
            }
        }
        catch (NumberFormatException e) {
            Toast.makeText(this,
                    "无效的数值字符串",
                    Toast.LENGTH_SHORT).show();
        }
    }

你可能感兴趣的:(《Android,软件安全权威指南》学习笔记)