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/ | 资源文件(布局、图片等) | 未编译的资源 |
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方法示例
咱们来看一个实际的例子。假设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的调用方式虽然啰嗦,但模式很固定。
知识体系总览
下面这张图把本章的核心内容串起来了。你可以把它当成一个思维导图,方便回顾:
这张图把APK结构、dex格式和smali语法串在了一起。你想想看,这三者其实是层层递进的关系:APK是外壳,dex是核心,smali是读懂核心的语言。搞明白了这三层,你就能看懂大部分Android应用了。
总结一下:本章我们拆解了APK的目录结构,深入看了dex文件的内部布局,最后学会了smali的基本指令。这些知识就像盖房子的地基——虽然不显眼,但决定了你能盖多高的楼。我个人建议你找个简单的APK,用apktool反编译后,对照着本文内容实际看一遍。光看不练,永远学不会逆向。
公众号:蓝海资料掘金营,微信deep3321