应用安全 --- APK 虚拟内存布局
1. Android进程虚拟内存空间概览 Android基于Linux内核,每个应用进程都有独立的虚拟地址空间: 64位Android进程虚拟内存布局(简化版): 0x0000000000000000 - 0x0000555555554000 [用户空间下限] ├── 0x0000000000000000 - 0x0000000000001000 [NULL页,不可访问] └── 0x0000000000001000 - 0x0000555555554000 [用户代码段起始区域] 0x0000555555554000 - 0x00007fffffffffff [主要用户空间] ├── 代码段 (.text) ├── 数据段 (.data, .bss) ├── 堆 (Heap) ├── 内存映射区 (mmap) ├── 共享库区域 └── 栈 (Stack) 0x8000000000000000 - 0xffffffffffffffff [内核空间] 2. APK加载过程中的内存布局 2.1 应用启动时的内存分配 典型Android应用内存布局: [低地址] 0x0000000000400000 - app_process64 (Zygote fork出来的进程) 0x0000000012c00000 - linker64 (动态链接器) 0x0000006f00000000 - [匿名内存区域] 0x0000007000000000 - [共享库加载区域开始] ├── 0x0000007000000000 - libc.so ├── 0x0000007001000000 - libm.so ├── 0x0000007002000000 - libdl.so ├── 0x0000007003000000 - libart.so (ART虚拟机) ├── 0x0000007010000000 - libandroid_runtime.so └── 0x0000007020000000 - [应用特定的native库] 0x0000007100000000 - [ART堆区域] ├── Image Space (boot.art) ├── Zygote Space ├── Allocation Space (应用对象) └── Large Object Space 0x0000007fff000000 - [栈区域] [高地址] 2.2 DEX文件在内存中的布局 DEX文件内存映射: /data/app/com.example.app/base.apk ├── classes.dex → 映射到内存 │ ├── 0x7030000000 - DEX Header │ ├── 0x7030001000 - String Pool │ ├── 0x7030010000 - Type Definitions │ ├── 0x7030020000 - Method Definitions │ └── 0x7030100000 - Code Items ├── classes2.dex → 单独映射 └── lib/arm64-v8a/ ├── libnative1.so → 0x7040000000 └── libnative2.so → 0x7041000000 3. SO库的详细内存布局 3.1 ELF文件段映射 单个SO文件在内存中的布局: libnative.so (基址: 0x7040000000) ├── ELF Header [0x7040000000 - 0x7040000040] ├── Program Headers [0x7040000040 - 0x7040000200] ├── .text (代码段) [0x7040001000 - 0x7040050000] (可执行) ├── .rodata (只读数据) [0x7040050000 - 0x7040060000] (只读) ├── .data (已初始化数据) [0x7040061000 - 0x7040062000] (读写) ├── .bss (未初始化数据) [0x7040062000 - 0x7040063000] (读写) ├── .got (全局偏移表) [0x7040063000 - 0x7040064000] (读写) ├── .plt (过程链接表) [0x7040064000 - 0x7040065000] (可执行) └── .dynamic (动态信息) [0x7040065000 - 0x7040066000] (只读) 3.2 内存权限设置 # 通过 /proc/[pid]/maps 可以看到实际布局 cat /proc/12345/maps 7040000000-7040001000 r--p 00000000 fd:00 12345 /data/app/.../lib/arm64/libnative.so 7040001000-7040050000 r-xp 00001000 fd:00 12345 /data/app/.../lib/arm64/libnative.so 7040050000-7040060000 r--p 00050000 fd:00 12345 /data/app/.../lib/arm64/libnative.so 7040061000-7040063000 rw-p 00060000 fd:00 12345 /data/app/.../lib/arm64/libnative.so 7040063000-7040064000 rw-p 00000000 00:00 0 [anon:.bss] 4. ART虚拟机内存布局 4.1 Java堆结构 ART堆内存布局: [Image Space] - 0x7100000000 ├── boot.art (系统类) └── boot-framework.art [Zygote Space] - 0x7110000000 ├── 预加载的系统类 └── 字符串常量池 [Allocation Space] - 0x7120000000 ├── Young Generation (新生代) │ ├── Eden Space │ └── Survivor Space └── Old Generation (老年代) [Large Object Space] - 0x7200000000 └── 大对象存储区域 [Non-Moving Space] - 0x7300000000 └── 不可移动对象 4.2 方法编译和缓存 编译后的代码布局: [OAT文件映射] - 0x7400000000 ├── OAT Header ├── DEX文件数据 ├── 编译后的机器码 │ ├── Method 1 机器码 │ ├── Method 2 机器码 │ └── ... └── GC Maps [JIT编译缓存] - 0x7500000000 ├── 热点方法机器码 └── 优化后的代码 5. 内存保护和隔离机制 5.1 ASLR (地址空间布局随机化) 每次启动时基址都会变化: 启动1: libnative.so → 0x7040000000 启动2: libnative.so → 0x7f8a000000 启动3: libnative.so → 0x7c15000000 5.2 内存权限控制 权限标志说明: r-- : 只读 r-x : 只读+可执行 (代码段) rw- : 读写 (数据段) --- : 不可访问 (保护页) 6. 动态加载和链接 6.1 延迟绑定机制 函数调用过程: 应用调用 native_function() ↓ PLT表中的跳转指令 ↓ GOT表查找实际地址 ↓ 如果未解析,调用动态链接器 ↓ 链接器解析符号并更新GOT ↓ 跳转到实际函数地址 6.2 共享库依赖关系 依赖链示例: app_process64 ├── linker64 ├── libc.so ├── libm.so ├── libart.so │ ├── libsigchain.so │ └── libbacktrace.so └── 应用SO库 ├── libssl.so ├── libcrypto.so └── 其他依赖库 7. 实际内存布局查看方法 你可以通过以下方式查看实际的内存布局: # 查看进程内存映射 adb shell cat /proc/$(pidof com.shizhuang.duapp)/maps # 查看进程状态 adb shell cat /proc/$(pidof com.shizhuang.duapp)/status # 使用Frida查看模块信息 Process.enumerateModules().forEach(function(module) { console.log(module.name + ": " + module.base + " - " + module.size); }); 这种内存布局设计确保了: 安全性:通过权限控制和ASLR防止攻击 效率性:共享库减少内存占用 隔离性:每个进程有独立的虚拟地址空间 灵活性:支持动态加载和卸载 这就是为什么你的dump工具能够通过Frida准确定位和提取SO文件的原理基础