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设置了,系统默认会把exported设为true。这意味着任何应用都可以通过隐式Intent启动你的Activity。我建议,除非必要,否则显式设置exported="false"。

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文件看看。纸上得来终觉浅,绝知此事要躬行。