概述
通过阅读UPX源码、Unidbg源码,找到了直接模拟未脱壳的UPX安卓so崩溃原因。目前此贡献已被合并至上游。https://github.com/zhkl0228/unidbg/commit/44fd8c22685e2aeec5b1a41381abd8e36cd5ba33
UPX源码阅读
git版本提交SHA:611d67542ae0ae9f8128ff121ae2a21ea007261d
压缩阶段
graph TD
A[Start: 输入二进制文件] --> B{PackMaster识别格式}
B -->|识别成功| C[创建对应 Packer 对象<br/>如 PackLinuxElf64arm]
B -->|识别失败| Exit[退出: Unsupported Format]
C --> D[pack1: 准备工作]
D --> D1[生成新 ELF 头部]
D --> D2[处理符号表与重定向]
C --> E[pack2: 数据压缩]
E --> E1[按块读取原始数据]
E --> E2[应用指令滤镜 Filter 0x52]
E --> E3[使用 LZMA/NRV 压缩块]
C --> F[pack3: 注入 Stub]
F --> F1[汇编 Entry: arm64-linux.elf-entry.S]
F --> F2[压缩后的 C Stub: amd64-linux.elf-main.c]
C --> G[pack4: 写入 PackHeader]
G --> H[End: 生成加壳二进制文件]
格式识别与分发 (src/packmast.cpp)
PackMaster 类通过 visitAllPackers 遍历所有支持的格式。

每个格式(如 PackLinuxElf64amd 或 PackLinuxElf64arm)通过 canPack() 检查输入文件是否可以被压缩。
对于 ARM64,它会识别
EM_AARCH64机器码。
准备工作 (src/p_lx_elf.cpp, src/packer.cpp)
- 对于 ARM64,会使用
ElfLinkerArm64LE。 - pack1():生成新的可执行文件头。对于安卓so,会特别处理
android_shlib,处理符号表和重定位。
- 对于 ARM64,会使用
数据压缩 (src/p_unix.cpp 中的 pack2)
- UPX 将原文件内容按块(Block)读取。
- 调用 compressWithFilters() 进行压缩。现在版本一般是NRV算法,也见过LZMA的。
注入解压 Stub (src/p_lx_elf.cpp 中的 pack3)
- 从
src/stub提取 ARM64 专用的汇编代码(如arm64-linux.elf-entry.S)和 C 逻辑。 - 所谓的
stub是UPX的一段可执行代码,stub本身也是运行时自解压的,是运行时自脱壳的核心。
- 从
收尾工作 (
src/p_unix.cpp中的pack4)- 写入
PackHeader信息到文件末尾。 - 记录
overlay_offset,这是加壳后原始压缩数据在文件中的偏移量,供解压 stub 使用。这些地方很容易被魔改,随便改一点基本上就不能upx -d脱壳了。
运行阶段
sequenceDiagram participant OS as 内核态 participant Entry as 带壳的程序入口 participant CStub as UPX Stub participant Target as 原始程序空间 OS->>Entry: _start Entry->>OS: syscall: memfd_create Entry->>Entry: 自解压 Folded C Stub 到 memfd Entry->>OS: syscall: mmap (执行权限) Entry->>CStub: 跳转到 C 逻辑入口 CStub->>CStub: 解析程序段 (PT_LOAD) 元数据 CStub->>OS: syscall: mmap (MAP_FIXED) <br/>一次性申请全部空间 loop 每一数据块 CStub->>Target: 解压数据到原始地址 CStub->>Target: Unfilter: 还原 ARM64 跳转偏移 end CStub->>OS: syscall: mprotect (恢复段权限) CStub->>Target: 动态写入 Escpae Hatch (逃生舱) CStub->>Target: 跳转到逃生舱 Target->>OS: syscall: munmap (释放 Stub 内存) Target->>Target: 跳转到 OEP (原始入口点)入口启动 (Entry)
- 运行从
_start开始,首先一次性压所有栈,保护现场。 - 通过
memfd_create系统调用或降级在/dev/shm下创建临时文件,用于存放即将解压的第二阶段 Stub 逻辑。
- 运行从
Stub 自解压
- 初始 Stub 将其自身“折叠(Folded)”部分的 C 逻辑代码解压到上述临时文件中。
- 通过
mmap将该临时文件映射为可执行内存,并跳转过去。
分析与内存规划 (
do_xmap函数)- 读取压缩时的元数据,使用
xfind_pages预定原始程序的总内存范围。 - 获取
AT_PAGESZ以确保内存页对齐一致。
- 读取压缩时的元数据,使用
循环解压块 (
unpackExtent函数)- 循环解压数据块到原始内存位置。
- 对 ARM64 指令进行 “unfilter” 处理(还原跳转偏移)。
权限恢复与清理
调用
mprotect恢复内存段权限。使用 ARM64 专用的
make_hatch_arm64创建一段“逃生舱”代码,负责执行munmap后跳转回原始 OEP。这就是Unidbg无法直接带壳模拟的原因。为什么要用“逃生舱”,因为
munmap不能操作自己所在的内存,所以需要申请一块新的,在新内存上操作。
恢复现场
- 恢复所有寄存器状态并跳转。这是传统的手动脱壳方法的常用点。依稀记得用od调win就是在popad上打个断点然后单步几下找大跳。
- 写入
所有的系统调用均通过
SVC 0执行,这块东西是直接汇编手搓的
分析解决Unidbg报错
根据运行时报错可以定位到如下位置,修改如下。
| |
根据UPX源码得知:运行时mmap会一次申请所有段的内存(防止临近的段被分到别的程序去了),此时这些段的权限必然是各不相同的(参考正常的程序)。因此,在munmap时,就会产生跨不同权限段操作的情况,原来代码中removed.prot != remove.prot必然成立。这样就会throw错误中断。我们仅需把throw改为无害的log warn即可完美解决此问题。
经过测试,原先不能直接模拟的lib,经过这样修改代码可以直接模拟。这完美避免了分析魔改UPX和修复头等工作。
