2、APK结构解析:APK文件组成、AndroidManifest.xml详解、DEX文件格式
好,咱们今天来聊聊APK。很多做安全的朋友,拿到一个APK第一件事就是反编译。但我个人习惯,会先把它当成一个普通的ZIP包,看看里面到底装了些什么。你想想看,一个APK说白了就是一个压缩包,只不过Android系统认识它,能把它跑起来。
2.1 APK文件组成:一个压缩包里的秘密
APK的全称是Android Package Kit。它的本质就是一个ZIP格式的压缩文件。你可以用任何解压工具打开它。我刚开始接触Android逆向时,就犯过一个低级错误——直接用反编译工具去搞,结果发现很多问题其实看看原始文件结构就能明白。
一个标准的APK,解压后你会看到这些核心文件:
- AndroidManifest.xml:应用的“身份证”,所有权限、组件、版本信息都在这里。
- classes.dex:Dalvik可执行文件,你的Java/Kotlin代码编译后的产物。
- resources.arsc:编译后的资源索引表,字符串、布局、颜色等资源的映射。
- res/:存放各种资源文件,比如图片、布局XML、动画等。
- lib/:存放Native代码(C/C++编译的.so文件),按CPU架构分目录。
- META-INF/:签名信息目录,包含MANIFEST.MF、CERT.SF、CERT.RSA等文件。
重点提醒:META-INF目录是签名校验的核心。我曾经在分析一个加固应用时,发现攻击者只是替换了DEX文件,但没更新签名信息,结果应用一运行就闪退。嗯,这就是签名校验在起作用。
还有一个文件你可能不常见——assets/目录。它和res/不同,assets里的文件不会被编译,你可以放任何格式的文件进去。我见过有些应用把加密的配置文件藏在assets里,然后在Native层读取解密。
2.2 AndroidManifest.xml详解:应用的身份证
这个文件太重要了。它是Android系统的“门卫”,系统启动应用前,第一件事就是读这个文件。说白了,它告诉系统:我是谁、我要干什么、我需要什么权限。
咱们来看几个关键节点:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="false"
android:debuggable="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
这里有几个安全相关的属性,我重点说一下:
| 属性 | 说明 | 安全建议 |
|---|---|---|
| android:allowBackup | 是否允许备份应用数据 | 建议设为false,防止adb backup泄露数据 |
| android:debuggable | 是否可调试 | 发布版必须设为false,否则容易被动态调试 |
| android:exported | 组件是否对外暴露 | 不需要暴露的组件设为false,防止被外部调用 |
| android:protectionLevel | 权限保护级别 | 自定义权限建议用signature级别 |
注意:我曾经遇到过一款金融应用,它的android:debuggable被设为了true。攻击者直接用adb命令就能附加调试器,轻松hook了加密函数。嗯,这种低级错误在安全审计中其实很常见。
还有一个容易被忽略的点——intent-filter。如果你给某个Activity设置了
2.3 DEX文件格式:Dalvik的灵魂
DEX文件是Android应用的“心脏”。你的Java代码编译成.class文件后,会被dx工具(现在叫d8)打包成一个或多个DEX文件。为什么是DEX?因为Dalvik虚拟机不识别标准的Java字节码,它有自己的指令集。
DEX文件的结构,说白了就是一个精心设计的二进制格式。它的核心结构如下:
DEX文件结构:
├── header(文件头)
│ ├── magic(魔数,固定为"dex\n035\0")
│ ├── checksum(校验和)
│ ├── signature(SHA-1签名)
│ ├── file_size(文件大小)
│ ├── string_ids_off(字符串索引偏移)
│ ├── type_ids_off(类型索引偏移)
│ ├── proto_ids_off(方法原型索引偏移)
│ ├── field_ids_off(字段索引偏移)
│ ├── method_ids_off(方法索引偏移)
│ ├── class_defs_off(类定义偏移)
│ └── data_off(数据区偏移)
├── string_ids(字符串索引区)
├── type_ids(类型索引区)
├── proto_ids(方法原型索引区)
├── field_ids(字段索引区)
├── method_ids(方法索引区)
├── class_defs(类定义区)
└── data(数据区)
├── code_item(方法字节码)
├── string_data(字符串数据)
└── ...
我个人觉得,理解DEX文件最关键的是要明白它的“索引机制”。DEX里所有的数据都是通过索引来引用的。比如一个方法调用,它不会直接写方法名,而是写一个method_id的索引。这样做的好处是节省空间,坏处是解析起来比较麻烦。
小技巧:当你用010 Editor打开DEX文件时,可以加载DEX模板。这样就能直观地看到各个字段的值。我经常用这个方法来分析DEX文件是否被篡改过。
这里有一个安全相关的点——checksum和signature。DEX文件头部的checksum是对整个文件(除了checksum字段本身)的adler32校验,而signature是SHA-1哈希。Android系统在加载DEX时,会验证这两个值。如果被修改了,系统会拒绝加载。
但是,我遇到过一些加固方案,它们会修改DEX文件头部的这些校验值,然后在自定义的类加载器里绕过系统校验。嗯,这就是为什么有些加固应用能运行,但用标准工具反编译却报错的原因。
还有一个有意思的东西——MultiDEX。当你的应用方法数超过65535时,就需要拆分成多个DEX文件。主DEX文件(classes.dex)包含启动必需的类,其他DEX文件(classes2.dex、classes3.dex等)在运行时动态加载。从安全角度看,攻击者往往会关注主DEX,因为入口逻辑都在里面。
最后,我想说一句:理解DEX文件格式,是做Android安全的基础。不管是做逆向分析、代码保护还是漏洞挖掘,你都得跟DEX打交道。我建议你找个简单的APK,用010 Editor或者dex2jar工具,亲手拆解一个DEX文件看看。纸上得来终觉浅,绝知此事要躬行。