rod1onov98/aarch-packer
GitHub: rod1onov98/aarch-packer
面向 Android .so 库的构建后 ELF 保护器,通过 ChaCha20 加密代码段并在运行时以直接系统调用完成自解密,以提升逆向分析门槛。
Stars: 0 | Forks: 0
# aarch-packer
用于 android .so 库的构建后保护器。使用 chacha20 加密 .text section,将内部调用点重写为基于 pc 相对地址的 stub,并修补 .init_array 以便解密器在 JNI_OnLoad 之前运行。支持 arm32 和 arm64,无需 root。
## 演示

## 工作原理
### 保护器 (protector.exe)
接收已完成的 .so 文件,执行所有所需操作并保存结果。可以将同一文件作为输入和输出传递——会直接在原文件上覆盖。
工作流 (run64 / run32):
1. 解析跳过函数的范围——通过 symtab 查找,通过 `_prot_init_end` 标记查找,默认取 512 字节并在日志中警告
2. 收集 exec section 列表(排除 .prot_text, .prot_data, .prot, .plt)
3. 扫描库内部所有函数的 bl/blx 调用
4. 在内存中构建 .prot blob:
- header:PROT_MAGIC(可选与 0xC01EBA42 进行异或)+ region 数量
- 加密 region 表:每个 exec section 的 va + size
- 重定向表:指向 stub 的 pc 相对偏移量,通过 nbitrev32 编码(NOT + 32 位反转)
- 原始调用的目标 VA 表
- stub 自身:每个调用点 16 字节
- 完整性表(INTEGRITY_MAGIC + 每个 section 的 xxhash32,在修补调用点**之后**、加密**之前**计算)
- 可选:关键函数表(CRIT_MAGIC)
- 可选:字符串表(STRTBL_MAGIC)
- 可选:库白名单(WLIST_MAGIC,.so 基础名的 xxhash32)
5. graph_breaker — 在分支后插入伪 prologue 和垃圾指令。**目前已禁用** — 会覆盖有效指令并导致运行时 SIGILL,后续会重写
6. 加密 .rodata 类 section(SHT_PROGBITS,已分配,不可执行,不可写,非 .prot_data)中的字符串 — 长度 4 到 512 字节,通过 xxhash32 为每个字符串生成单独的异或 key
7. 修补调用点:每个 bl/blx 被重写为跳转到 .prot 中的 stub
8. 确定最终的完整性哈希
9. 使用 chacha20 加密 exec section,跳过范围除外。每个 region 拥有自己的 nonce,通过 chacha20 块从 region VA 派生
10. 关键函数在第一层之上获得第二层加密(单独的 key,counter=1)
11. 将 .prot blob 作为新的 PT_LOAD segment 追加(PF_R|PF_X,align 0x1000)— 重写 phdrs、shdrs 和 shstrtab
12. 修补 .init_array:
- arm64 PIE:重写覆盖 .init_array 的 .rela.dyn 重定位 — 将类型更改为 R_AARCH64_RELATIVE,重新排序 addend 使 _prot_init 落入 slot 0
- arm32 PIE:与 R_ARM_RELATIVE 执行相同操作
- non-PIE fallback:将原始 VA 写入第一个空 slot
13. 从 .symtab 中清除 _prot_init, _prot_begin, _prot_init_end(将 name 和 st_name 置零)。刻意不触碰 .dynsym — 在不重建 .hash 链的情况下重命名会破坏 dlsym
14. 可选重命名 section(XorShift64,64 字符名称),然后伪装自身的 section:.prot_text → .text.0,.prot_data → .rodata0,.prot → .note0
15. 可选混淆 .symtab/.dynsym 中的符号名,重建 .hash 表,并将 DT_SONAME 和 DT_GNU_HASH 置零
### 解密器 (prot_init.c)
`_prot_init` 是一个直接链接到你的 .so 中的 `__attribute__((constructor))` 函数。
linker 将其放在 .prot_text 中,随后 protector.exe 会将其放在 .init_array 的首位。
运行时执行流程:
```
linker loads .so
→ processes .init_array relocations
→ _prot_init() runs first
→ do_decrypt()
→ dl_iterate_phdr() — look for library base address
→ find_prot_metadata() — scan PT_LOAD segments for PROT_MAGIC with 4-byte alignment
→ fallback: find_prot_metadata_maps() — parse /proc/self/maps, read .prot segment
→ derive keys: kdf_key(TK_A, TK_B, TK_C) / kdf_nonce(TN_A, TN_B, TN_C)
→ for each encrypted region:
derive per-region nonce via rnonce() (chacha20 block with region_va >> 12)
dreg(): sc_mmap_rwx() through SVC #0, memcpy, decrypt, memcpy back
remember page range in g_seal[] for later mprotect
→ dcrit_all(): decrypt critical functions (crit key, counter=1)
→ dstr(): xor-decrypt strings in place (pages already RWX)
→ seal pages: sc_mprotect() g_seal[] → PROT_READ, then PROT_READ|EXEC
→ arm64: dsb ish / isb (icache invalidation)
→ wd_collect(): read integrity table, fill g_wd[] (VA + size + expected hash)
→ wd_start(): launch watchdog thread
→ g_decrypted = 1
→ _prot_init returns
→ remaining constructors
→ JNI_OnLoad on already-decrypted code
```
### 为什么使用 SVC #0 而不是 libc
在启用 SELinux 强制模式下的 android 中,来自 libc 的 `mprotect()` 会在 file-backed exec pages 上对 untrusted_app 被阻止。`mmap(MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, PROT_RWX)` 会替换整个映射 — 这是被允许的,因为它创建了一个新的匿名映射,而不是更改现有映射的权限。
此外,直接 syscall 会绕过 frida 对 libc 包装器的钩子。
syscall 编号:
- arm32:mmap2 = 192,mprotect = 125
- arm64:mmap = 222,mprotect = 226
### key 方案(3 个 share)
key 和 nonce 绝不直接存储。它们通过一个 chacha20 块从三个 share 派生而来:
```
key = chacha20_block(state(TK_A, TK_B, counter=0x4B4446)) XOR TK_C
nonce = chacha20_block(state(TN_A, TN_B, counter=0x4E4F4E45)) XOR TN_C
```
每个 region 的 nonce:
```
region_nonce = chacha20_block(state(key, base_nonce, counter=region_va>>12)) XOR base_nonce
```
完全相同的 kdf_key / kdf_nonce 逻辑在 `src/crypto.h` 和 `runtime/prot_init.c` 中逐字节复制。**如果 share 不匹配 — 解密将无法通过。**
关键函数拥有单独的一对 share:CK_A/B/C 和 CN_A/B/C,counter=1。
### watchdog
解密后,`wd_start()` 会启动一个分离的线程,该线程每约 1.3 秒唤醒一次,并为每个 exec section 重新计算 xxhash32。如果不匹配 → `abort()`。
如果设置了库白名单 — 首先会解析 /proc/self/maps,对 .so 文件的基础名进行哈希处理并与白名单比对。如果全部匹配(意味着它是来自白名单的受信任工具,如 frida)— 我们会更新哈希而不是 abort。
### 调用点 stub
arm64(ADRP + ADD + BR,pc 相对,无重定位,适用于 PIE):
```
adrp x16, target_page
add x16, x16, #lo12
br x16
nop
```
arm32(LDR + ADD + BX,pc 相对):
```
ldr r12, [pc, #4] ; load offset
add r12, pc, r12 ; r12 = pc + (target|thumb - pc) = target|thumb
bx r12 ; interwork arm<->thumb
.word (target|thumb_bit) - (stub_va + 12)
```
## 构建
### 依赖
- android ndk r21+(在 r25c 上测试过)
- protector.exe — 使用 cmake 从源码构建
### 为什么 -O0 -fno-builtin 对 arm64 是必需的
所有解密器函数(_prot_init, do_decrypt, dreg, dstr, cc20x 等)都通过 `__attribute__((section(".prot_text")))` 位于 .prot_text 中。在解密期间,dreg() 调用 sc_mmap_rwx(),它会用新的匿名映射替换 .text 页。如果编译器将 libc 的 memcpy、free 或 NEON 内联到 .prot_text 中 — 这些指令最终会出现在我们要重新映射的同一个内存页上,一切都会崩溃。
`-O0 -fno-builtin` 禁止任何此类内联。这就是为什么 prot_init.c 会使用这些标志作为单独的静态 lib 进行编译。
对 _prot_init 使用 `aligned(4096)` 是另一回事 — 它确保 .prot_text 不与 .text 共享同一个内存页。
### Android.mk
```
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := prot_init_o0
LOCAL_SRC_FILES := ../../../../../runtime/prot_init.c
LOCAL_CFLAGS := -O0 -fvisibility=default -fno-builtin -ffunction-sections -fdata-sections
ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
LOCAL_CFLAGS += -mthumb
endif
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := yourlibname
LOCAL_SRC_FILES := yourlib.cpp
LOCAL_LDLIBS := -llog
LOCAL_CPPFLAGS := -O2 -std=c++14 -fvisibility=hidden -ffunction-sections -fdata-sections
LOCAL_CFLAGS := -O2 -fvisibility=hidden -ffunction-sections -fdata-sections
ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
LOCAL_CPPFLAGS += -mthumb
LOCAL_CFLAGS += -mthumb
endif
LOCAL_STATIC_LIBRARIES := prot_init_o0
# 需要 -u 以便 linker 不会将 prot_init.o 从 archive 中丢弃 — 在 link 时
# 此时还没有任何地方引用 _prot_init,protector.exe 会在稍后添加 .init_array 条目。
# 如果没有 -u,--gc-sections 会悄悄丢弃整个 object,.so 中将完全没有 decryptor。
# --export-dynamic-symbol — 以便 protector.exe 即使在 stripped library 中也能找到 VA。
LOCAL_LDFLAGS := -Wl,-u,_prot_init \
-Wl,-u,_prot_begin \
-Wl,-u,_prot_init_end \
-Wl,--gc-sections \
-Wl,--export-dynamic-symbol=_prot_init \
-Wl,--export-dynamic-symbol=_prot_begin \
-Wl,--export-dynamic-symbol=_prot_init_end
include $(BUILD_SHARED_LIBRARY)
```
### Application.mk
```
APP_ABI := armeabi-v7a arm64-v8a
APP_PLATFORM := android-21
APP_OPTIM := release
APP_STL := c++_static
```
## 用法
```
protector.exe [-v] [-c config.ini] input.so output.so
```
### config.ini
```
rename_sections = true
rename_symbols = true
encrypt_text = true
skip_init_array = true
obfuscate_magic = true
graph_breaker = false
name_seed = 0x06AC82E3148BF15B
[critical]
# va 大小 名称
# 0x1234 0 critical_transform
[skip]
# _prot_init 通常会从 symtab 中自动检测
# 如果未找到 — 请手动添加:
# 0x2000 512 _prot_init
[whitelist]
# libfrida-agent.so
```
| 选项 | 作用 |
|---|---|
| `encrypt_text` | 使用 chacha20 加密 .text 和所有 exec section(除了 .plt) |
| `skip_init_array` | 不重命名 .init_array/.fini_array/.dynamic/.got |
| `rename_sections` | 随机化 section 名称(64 字符名,XorShift64) |
| `rename_symbols` | 混淆未导出的符号,重建 .hash |
| `obfuscate_magic` | 将 PROT_MAGIC 与 0xC01EBA42 进行异或 |
| `graph_breaker` | 在分支后的伪 prologue 和垃圾指令 — **已禁用,会导致崩溃** |
| `name_seed` | 名称生成器的 seed |
| `[critical]` | va,可选 size、name — 第二层 chacha20(crit key,counter=1) |
| `[skip]` | va,可选 size、name — 不加密(解密器函数) |
| `[whitelist]` | .so 基础名 — 如果所有已加载的库都在列表中,watchdog 不会 abort |
跳过函数大小的解析顺序:
1. config 中给定了 va + size — 按原样使用
2. 给定了 va,未给定 size — 通过 va 在 symtab 中查找
3. symtab 未找到 — 如果标记存在,计算 `_prot_init_end - va`
4. fallback — 512 字节,在日志中警告(添加 `_prot_init_end` 以提高精度)
5. 仅限 arm32:如果连 va 也缺失 — 在 config.h 中通过硬编码的字节模式搜索
## 预期输出
```
[*] Loading libyourlib.so
[*] Arch: ARM64
[*] Auto-skip '.prot_text'
[*] Auto-skip '.prot_data'
[*] Auto-skip '.prot'
[*] Found 6 functions
[*] Found 12 call sites
[*] Encrypting strings...
[*] Encrypted 31 strings
[*] Patching 12 call sites...
[*] Finalizing integrity hashes (post-mutation)...
[*] VA=0x10ac: 0x4f23a1b8
[*] Encrypting sections...
[*] off=0x10ac size=0x298
[*] String table: 504 bytes
[*] _prot_init VA inferred from .prot_text: 0x2000
[*] init reloc slot[0] -> RELATIVE addend=0x2000 (_prot_init, runs first)
[*] stripped symbol '_prot_init' (symtab)
[*] masked section .prot_text -> .text.0
[+] Done! 19488 bytes
```
如果你看到 `[!] _prot_init not found in symtab` — 很可能你忘记了 `--export-dynamic-symbol=_prot_init` 或者 prot_init_o0 没有被链接(缺少 `-u _prot_init`)。
## logcat (标签:prot)
| 日志 | 含义 |
|---|---|
| `s` | _prot_init 进入 do_decrypt() |
| `rg` | 找到 .prot 元数据,开始解密 |
| `ds` | 文本已解密,开始处理字符串 |
| `ok` | 全部解密完成,watchdog 已启动 |
| `b` | dl_iterate_phdr 未找到库基址 |
| `p0`–`p9` | 未找到 .prot 元数据(数字 = 失败时的 g_maps_stage) |
| `m` | .prot header 中的 magic 错误 |
| `n` | header 中的 region 数量错误 |
| `nw` | sc_mmap_rwx 返回错误 |
| `tamper` | watchdog 捕获到哈希不匹配,随后执行 abort() |
## 密钥
share 存储在 .prot_data 中,不进行加密。需要在**两个**文件中同时更改 — `src/crypto.h` 和 `runtime/prot_init.c` — 否则运行时将无法解密。
- TK_A, TK_B, TK_C / TN_A, TN_B, TN_C — 文本加密的 key 和 nonce share
- CK_A, CK_B, CK_C / CN_A, CN_B, CN_C — 关键函数的 key 和 nonce share
## 限制
- **内存转储** — 一旦 _prot_init 运行,解密后的代码就会在进程内存中可见。防范的是对 .so 文件的静态分析,而不是针对性的运行时转储
- **extractNativeLibs=false** — 已支持。dl_iterate_phdr 使用 p_filesz(而不是 p_memsz),这对于 APK 内嵌的库能正常工作
- **arm64 页面布局** — 如果 _prot_init VA = 0x1000(与 .text 同一页),dreg() 会重新映射它正在运行的页面,一切都会崩溃。请检查输出显示的 `_prot_init VA` >= 0x2000,并确保 -O0 -fno-builtin 标志确实应用到了 prot_init.c 上
- **graph_breaker** — 已禁用。目前的实现会覆盖真实指令(第一个 ret 后的伪 prologue,thumb 中 B 后的垃圾指令),这会破坏可达代码。后续会重写为仅插入到函数间的 padding 中
- **arm64 模式** — 未实现 PROT_INIT_PATTERN_ARM64(代码中的 TODO)。跳过的 VA 必须从 symtab 或 _prot_init_end 标记中解析。arm32 模式存在,但未在 arm64 分支上使用
## 项目结构
```
aarch-packer/
├── src/
│ ├── main.cpp — entry point, argument parsing (-v, -c)
│ ├── protector.h — Protector class: run32/run64, append32/append64,
│ │ init_array patching, symbol wiping, section masking
│ ├── elf_parser.h — ELF load/save, VA<->offset, function enumeration
│ ├── elf_types.h — Elf32/Elf64 structs and ELF constants without system headers
│ ├── arch_arm32.h — ARM32/Thumb2 BL/BLX scanner, pc-relative stub writer
│ ├── arch_arm64.h — AArch64 BL scanner, ADRP+ADD+BR stub writer
│ ├── crypto.h — chacha20 block/xor, 3-share KDF, per-region nonce,
│ │ crit layer, XorShift64 name generator
│ ├── integrity.h — xxhash32 implementation, table magic values
│ ├── string_enc.h — .rodata string scanner, xor encryption, blob builder
│ ├── graph_breaker.h — fake prologue and junk insertion (disabled)
│ ├── sec_renamer.h — section name randomization (XorShift64, rewrites .shstrtab)
│ ├── sym_obfuscator.h — symbol obfuscation, .hash rebuild, DT_SONAME wipe
│ └── config.h — config.ini parser, CriticalFunc/SkipFunc/whitelist structs
├── runtime/
│ ├── prot_init.c — runtime decryptor (_prot_init, do_decrypt, dreg, dstr,
│ │ dcrit_all, watchdog, whitelist checker, direct syscalls)
│ └── Android.mk — build rules for prot_init as a separate static lib
├── testlib/
│ ├── testlib.cpp — test JNI library (strings, math, critical,
│ │ self-check, tamper-detection test via mprotect + byte xor)
│ ├── Android.mk — testlib build rules + prot_init_o0 linking
│ └── Application.mk — ABI, platform, STL
└── config/
└── config.ini — example config
```
## 作者提示
如果你打算使用所有可能的保护选项并进一步完善这个 packer,我必须立刻提醒你,优化你的游戏将会变得极其糟糕,而且 fps 也会惨不忍睹
标签:Android, Bash脚本, ChaCha20, DNS 反向解析, DSL, ELF, NDK, UML, 云资产清单, 代码保护, 混淆, 逆向工程