65、Android逆向(一):APK结构、dex文件格式、smali语法基础

做Android逆向也有些年头了。说实话,每次拿到一个APK,我第一反应不是直接反编译,而是先把它当成一个压缩包,看看里面到底塞了些什么东西。你想想看,一个APK本质上就是个zip包,只不过Android系统认识它而已。

今天咱们就从最基础的开始,把APK的骨架摸清楚。嗯,这部分内容虽然基础,但你要是搞不明白,后面分析代码的时候肯定会卡壳。

APK文件结构:拆开看看里面有什么

APK的全称是Android Package Kit。说白了,它就是Android应用的安装包。我习惯用7-Zip或者直接改后缀名为.zip来解压查看。解压后你会看到这么几个关键文件:

文件/目录 作用 我的备注
classes.dex Dalvik字节码文件,应用的核心代码 可能有多个,比如classes2.dex
AndroidManifest.xml 清单文件,声明权限、组件等 二进制格式,需要工具解析
resources.arsc 编译后的资源索引表 字符串、布局ID都在这
lib/ Native库(.so文件) armeabi-v7a、arm64-v8a等
META-INF/ 签名信息 篡改APK后必须重新签名
res/ 资源文件(布局、图片等) 未编译的资源
我的习惯:拿到APK先看AndroidManifest.xml。这里能快速了解应用有哪些Activity、Service,以及申请了哪些敏感权限。我曾经遇到过一个恶意应用,它申请了"读取短信"权限但界面只是个计算器——这明显有问题。

dex文件格式:Dalvik字节码的容器

dex文件是Android逆向的核心战场。Java代码编译成.class文件后,会被dx工具(现在叫d8)转换成.dex文件。为什么要这么做?因为dex更紧凑,适合移动设备。

dex文件的结构大致如下:

dex文件头(header)
├── string_ids(字符串常量池)
├── type_ids(类型索引)
├── proto_ids(方法原型)
├── field_ids(字段索引)
├── method_ids(方法索引)
├── class_defs(类定义)
├── data(实际数据区)
└── map_list(映射列表)

关键点:dex文件头中有个叫checksum的字段,用来校验文件完整性。我当年刚入门时,想手动修改dex里的字符串,结果忘了更新checksum,导致APK运行时直接崩溃。嗯,这个坑我替你们踩过了。

用010 Editor配合dex模板,可以直观地看到这些结构。我个人更推荐用命令行工具:

# 查看dex文件头信息
dexdump -f classes.dex

# 查看dex中的类和方法
dexdump -d classes.dex | head -100

为什么会关注这些底层结构?因为当你需要手动修复损坏的dex,或者做代码注入时,这些知识就是你的手术刀。

Smali语法基础:读懂Dalvik汇编

Smali是Dalvik字节码的一种可读表示形式。baksmali工具可以把dex反编译成.smali文件。我第一次看到smali代码时,说实话有点懵——这跟Java长得完全不一样啊。

但别怕,核心逻辑是一样的。咱们先看几个最常用的指令:

Smali指令 含义 Java对应
const/4 v0, 0x1 将常量1存入v0寄存器 int v0 = 1;
iget-object v0, p0, Lcom/example/Test;->name:Ljava/lang/String; 获取实例字段 this.name
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z 调用虚方法 v0.equals(v1)
if-eqz v0, :cond_0 如果v0等于0则跳转 if (v0 == 0) goto label
return-object v0 返回对象 return v0;

寄存器是smali里的核心概念。Dalvik虚拟机使用寄存器架构,而不是Java虚拟机那样的栈架构。这意味着数据操作都在寄存器里完成。p0代表方法的第一个参数(对于非静态方法,p0是this),v0到v15是局部变量寄存器。

注意:smali中的寄存器数量是有限制的。如果一个方法用了超过16个寄存器,就会使用v16及以上的命名方式。我曾经在分析一个混淆严重的应用时,发现它故意把寄存器数量设得很大,让反编译工具解析出错。这种对抗手段现在越来越常见了。

一个完整的smali方法示例

咱们来看一个实际的例子。假设Java代码是这样的:

public String getGreeting(String name) {
    if (name == null) {
        return "Hello, Guest!";
    }
    return "Hello, " + name + "!";
}

反编译成smali后:

.method public getGreeting(Ljava/lang/String;)Ljava/lang/String;
    .registers 4

    .param p1, "name"    # Ljava/lang/String;

    .prologue
    .line 5
    if-eqz p1, :cond_0

    .line 7
    new-instance v0, Ljava/lang/StringBuilder;
    invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
    const-string v1, "Hello, "
    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v0
    invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v0
    const-string v1, "!"
    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 v0

    .line 8
    :goto_0
    return-object v0

    .line 6
    :cond_0
    const-string v0, "Hello, Guest!"
    goto :goto_0
.end method

你看,逻辑其实没变。if-eqz判断参数是否为空,为空就走cond_0分支返回"Hello, Guest!",否则就拼接字符串。StringBuilder的调用方式虽然啰嗦,但模式很固定。

我的经验:刚开始学smali时,建议对照着Java源码看。把smali指令和Java语句一一对应起来,慢慢就熟悉了。我当年就是这么干的——左边开个Java文件,右边开个smali文件,一行行对照。大概看了两三天,基本就能直接读smali了。

知识体系总览

下面这张图把本章的核心内容串起来了。你可以把它当成一个思维导图,方便回顾:

Android逆向基础 APK文件结构 classes.dex - 核心代码 AndroidManifest.xml - 清单 resources.arsc - 资源索引 lib/ - Native库 dex文件格式 header - 文件头 string_ids - 字符串池 method_ids - 方法索引 class_defs - 类定义 data - 实际数据区 Smali语法基础 寄存器操作(v0, p0等) 指令集(const, invoke等) 条件跳转(if-eqz等) 字段访问(iget, iput) 方法调用(invoke系列) 核心工具链 apktool → baksmali → 010 Editor → dexdump

这张图把APK结构、dex格式和smali语法串在了一起。你想想看,这三者其实是层层递进的关系:APK是外壳,dex是核心,smali是读懂核心的语言。搞明白了这三层,你就能看懂大部分Android应用了。

总结一下:本章我们拆解了APK的目录结构,深入看了dex文件的内部布局,最后学会了smali的基本指令。这些知识就像盖房子的地基——虽然不显眼,但决定了你能盖多高的楼。我个人建议你找个简单的APK,用apktool反编译后,对照着本文内容实际看一遍。光看不练,永远学不会逆向。


公众号:蓝海资料掘金营,微信deep3321