一款可分享密码的解压缩 app 分析学习
207k字 •
413分钟
解压专家 (apk)
依据 ELF 结构
分析一款可以共享密码的解压缩 app,其中用到了 native 的知识点
本文将介绍两种 native 解密方法
java 层

1 | |
getVersionName
1 | |

User-Client-Access
1 | |
其中 getQPWAT 调用 libZMCoreUnZip.so 的 executeGBS native 方法
1 | |
native 层
静态分析+vibe
发现能识别 elf,但是 export 符号很奇怪,不是可见字符,且指示条不正常

发现 JNI_OnLoad export 不存在,所以看 init_array

发现不是很好找,这里根据 elf 结构我们可以直接根据 Elf64_Dyn 类型引用定位到 ELF Dynamic Information 然后找到 DT_INIT_ARRAY


随后发现 init_array 均为空值 0,说明需要重定向。
查看 DT_RELA 元素为 Elf64_Rela
1 | |
注意我们需要 r_info == 0x403 的项目,且 r_offset 需要位于 [DT_INIT_ARRAY, DT_INIT_ARRAY + DT_INIT_ARRAYSZ) 之中,即 [0x55DB00, 0x55DB00+0x20)

所以 init_array 为
1 | |
可以通过编写以下脚本来应用重定向
1 | |
借助 ai 分析,能够找到关键全局变量并创建结构体
1 | |
1 | |
三个 init 函数分别有以下功能
init1 负责环境探测
| 顺序 | 动作 | 结果 |
|---|---|---|
| 1 | dlopen("libc.so") |
获得 libc 句柄 |
| 2 | dlsym("__system_property_get") |
获得系统属性读取函数 |
| 3 | 读取 ro.build.version.sdk |
记录 Android SDK 版本 |
| 4 | 打开 /proc/self/maps |
枚举当前进程映射 |
| 5 | 按 "%lx-%lx %4s %*x %*x:%*x %*d%n" 解析映射行 |
获得区间和页权限 |
| 6 | 结合 SDK、模块位置和权限写入全局状态 | 给 init3 与 load2_load_packed_segments 使用 |
init2 负责恢复 dynsym、dynstr
1 | |
注意到:
| item | value |
|---|---|
DT_SYMTAB |
0x55FD38 |
DT_STRTAB |
0x5BA5E8 |
DT_STRSZ |
0x817E0 |
DT_HASH.nchain |
0x2F28 |
loader_meta.sym_index_begin |
0x3A |
loader_meta.sym_index_end |
0x2F28 |
loader_meta.dynsym_seed3 |
0x815C0 |
dynstr_dynsym_decode_Impl 的解密过程
dynstr_dynsym_decode_Impl @ 0x54D6B8 做两件事:先解 .dynstr,再修 .dynsym
1 | |
给出修复脚本
1 | |
init3 负责还原代码
1 | |
load2_load_packed_segments
函数概括为六个阶段:
- 从 ELF header 后定位主 packed payload,并定位 payload 尾部的 trailer
- 根据
flags获取可选字符串或构造外部 key - 第一次调用
core_decode,恢复 metadata 后方的 ELF 链接信息块 - 以链接信息块为基址回填 Program Header、
.dynsym、.dynamic和 relocation 地址 - 第二次调用
core_decode,把主 packed payload 还原为带 8 字节长度头的 zlib 包 - 分配独立输出缓冲,调用
zlib_uncompress2_wrapper,再按PT_LOAD解释解压结果
flowchart TD
A["从 image_base 定位 packed_src"]
B["准备 packed_dst 与 ext_key"]
C["解码 `meta+0x48` 数据块"]
D{"flags"}
E["bit5: fixed XOR"]
F["bit1: core_decode"]
G["读取 unpacked_size / compressed_size"]
H["解压为最终 ELF 内存镜像"]
I["回填 elf_parse_ctx_t"]
A --> B --> C --> D
D --> E --> G
D --> F --> G
D -->|无解码标志| G
G --> H --> I
RuntimeMalloc、RuntimeFree、RuntimeMemcpy 等名称对应 API 表里的 libc 函数;AcquireFlag4String/AcquireFlag8String/AcquireFlag10String 只是按控制它们的 flag 命名,具体业务语义仍未确认。
1 | |
该样本将会执行两次 core_decode 而不执行 xor_decode_fixed_key_blocks
| 调用点 | 输入范围 | 解密内容 | 解密后的用途 |
|---|---|---|---|
0x54CB7C |
0x63C020 开始,长度 0x6CC18 |
壳放在 loader_meta 后方的 ELF 链接信息块 |
结果保存到 ctx+0x90;后续以它为基址计算 .dynsym、.dynamic、RELA、JMPREL 和 Android relocation 地址 |
0x54CFE8 |
image_base+0x190 开始,长度 0x5062E0 |
主 packed payload,即压缩的最终代码/数据镜像外层 | 结果前 8 字节给出解压长度,packed_dst+8 再送入解压函数生成最终内存镜像 |
core_decode 伪代码
1 | |
zlib_uncompress2_wrapper
sub_557170 @ 0x557170 是壳内置 zlib 的一次性解压包装器,参数语义与 uncompress2() 基本一致:
1 | |
解压后需要按 PT_LOAD 分段映射
zlib 解压后的 main_payload 输出长度为 0x530268
以下 Elf64_Phdr 来自于第一次 core_decode 解出的 metadata 开头
metadata 开头连续保存 6 个 Elf64_Phdr;筛选 p_type == PT_LOAD 后得到:
p_offset |
p_vaddr |
p_filesz |
p_memsz |
|---|---|---|---|
0x0 |
0x0 |
0x5062E0 |
0x5062E0 |
0x5062E0 |
0x50A2E0 |
0x29CF0 |
0x29D20 |
0x52FFD0 |
0x537FD0 |
0x298 |
0xBAE0 |
完整运行时映射为:
1 | |
第二段的 p_memsz - p_filesz = 0x30,第三段为 0xB848;这两块段尾内存必须清零。
当前 IDB 的 VA 0..0x18F 已经保存原始 ELF header 和 Program Header Table,为了避免静态 patch 时覆盖这些结构,可以仅对首段执行:
1 | |
解密脚本
1 | |
unidbg
编写 test 脚本
1 | |
将原来的前 0x190 字节复制到 dump 的即可分析
其他
其实还应该解密 elf 最后的 .data 段用于实现 GOT、LPT 定位的,但是比较麻烦就先不做了(分配到动态内存中了),而上述静态分析方法可以直接解密(或者这里结合一下静态的脚本解密一下也可以),下面给出一个工具脚本,用于 patch GOT,当然不使用也可以
1 | |
分析 executeGBS
1 | |
伪代码
1 | |
POC
1 | |