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。 ## 演示 ![演示](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/c8cc54c82c194054.png) ## 工作原理 ### 保护器 (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, 云资产清单, 代码保护, 混淆, 逆向工程