BlackOuTv2/DexShell-Research

GitHub: BlackOuTv2/DexShell-Research

DexShell/DexProtectX Android打包器逆向研究工具

Stars: 6 | Forks: 0

# 🔬 DexShell Android Packer — 深度逆向工程 ![Platform](https://img.shields.io/badge/Platform-Android%20ARM64-3fb950?style=flat-square&logo=android) ![Frida](https://img.shields.io/badge/Frida-16.x-ff6b35?style=flat-square) ![radare2](https://img.shields.io/badge/radare2-5.x-58a6ff?style=flat-square) ![Status](https://img.shields.io/badge/Status-Fully%20Bypassed-3fb950?style=flat-square) ![Methods](https://img.shields.io/badge/VMP%20Methods-1%2C448-bc8cff?style=flat-square) ![License](https://img.shields.io/badge/Research-Educational-d29922?style=flat-square) **对商业 Android Packer DexShell / DexProtectX 的完整逆向工程。** v26 bytecode interpreter → v29 native AOT compiler。记录并绕过了全部 11 层防篡改保护。 [📊 **可视化研究 (HTML + 流程图)**](https://BlackOuTv2.github.io/DexShell-Research) · [🛠️ 工具](#-tools) · [🔓 绕过](#-bypass-techniques) ### 👤 研究员与联系方式 | | | |---|---| | **GitHub** | [@BlackOuTv2](https://github.com/BlackOuTv2) | | **Telegram** | [@BlackOuTv1](https://t.me/BlackOuTv1) | | **LinkedIn** | [linkedin.com/in/black0ut](https://www.linkedin.com/in/black0ut/) | | **Instagram** | [@cyberxblackout](https://www.instagram.com/cyberxblackout/) | | **研究类型** | 独立 Android 安全研究 | | **研究范围** | 静态 + 动态分析,Frida instrumentation,native ARM64 反汇编 |
## 📋 目录 | | | |---|---| | [什么是 DexShell?](#what-is-dexshell) | [APK 结构](#apk-structure) | | [Bootstrap 序列](#bootstrap-sequence) | [防篡改 (11 层)](#anti-tamper-mechanisms) | | [VMP 架构](#vmp-architecture-v26-vs-v29) | [InstrumentationHijacker](#instrumentationhijacker-new-v29) | | [DEX Header 混淆](#dex-header-obfuscation-new-v29) | [字符串加密](#string-encryption) | | [Runic Unicode 命名](#runic-unicode-obfuscation-new-v29) | [网络与后端](#network--backend) | | [发现的 AOSP 密钥](#aosp-platform-keys-found-in-apk) | [绕过技术](#bypass-techniques) | | [构建的工具](#-tools) | [v26 → v29 差异对比](#v26--v29-diff) | ## 什么是 DexShell? **DexShell**(以 [DexProtectX](https://dexprotectx.pro) 名称销售)是一款商业 Android Packer,通过以下方式保护应用 DEX: - **DEX 加密** — 受保护的代码仅在运行时解密 - **VMP (虚拟机保护)** — v26:bytecode interpreter · v29:native AOT compilation - **11 层防篡改** — 针对 Frida, Xposed, root, 调试器以及 MTE-aware 的检测 - **InstrumentationHijacker** — 替换 Android `Instrumentation` 以捕获分析框架 ## APK 结构 ``` DexShellx_V29.0.apk (33.74 MB) ├── assets/ │ ├── DexShell.mp3 ← 9.2MB XOR-encrypted VMP container │ │ (3 embedded ELFs: arm64/arm32/x86, DT_SONAME=libdexshellx.so) │ ├── dexshellx.pro ← Config: 2.6KB (v26) → 21KB (v29), DXHD5 format │ ├── libVMDexShellx.so ← 465KB PairIP anti-tamper SDK (DT_SONAME=libpairipcore.so) │ └── dexshell/arm64-v8a/ │ └── libdexshell.so ← Core loader, decrypted at runtime, XOR key: 27d39683cc50fcba │ ├── unknown/keys/ ← ⚠️ AOSP platform.pk8 / media.pk8 / shared.pk8 bundled │ ├── classes.dex ← DexShell runtime (only 15 visible classes in v29) └── classes5–16.dex ← Protected app — method bodies replaced with N.invoke(idx) stubs ``` ## Bootstrap 序列 ``` App spawn └─ DexShellxApplication.attachBaseContext() ├─ Load libVMDexShellx.so (PairIP) │ └─ JNI_OnLoad: install all detection hooks │ (dl_iterate_phdr · /proc scan · stat · opendir · tgkill) │ ├─ Load libdexshell.so (decrypted from assets at runtime) │ ├─ Decrypt DexShell.mp3 → extract libdexshellx.so to txt/%s [encrypted on disk] │ ├─ dlopen → decrypt in memory → valid ELF in RAM only │ └─ ART ClassLinker::DefineClass → load 514 hidden classes │ └─ Application.onCreate() ├─ InstrumentationHijacker.ᛱ᛽᛽ᲅ() [NEW v29] │ └─ Reflect ActivityThread.mInstrumentation → swap ProxyInstrumentation │ └─ Now monitors ALL activity calls, polls execStartActivity 28×/4s │ └─ DREXz delayed_detection starts (~4s timer) └─ K.ᛳᲆᛲ(Context, String)V → load protected app ``` ## 防篡改机制 | # | 机制 | 库 | 检测方法 | |---|-----------|---------|-----------------| | 1 | `/proc/self/maps` 扫描 | libdexshell.so | 内存映射中的 Frida agent 区域名 | | 2 | `TracerPid` 检查 | libdexshell.so | `/proc/self/status` 中的非零值 | | 3 | **GOT 损坏陷阱** | libdexshell.so | 覆盖 libart GOT → 在 `0x77d61f0220` 处触发 SIGSEGV | | 4 | `dl_iterate_phdr` | libVMDexShellx.so | ByteHook 枚举 ELFs 查找 frida-agent | | 5 | `opendir/readdir` | libVMDexShellx.so | 扫描磁盘上的 Frida 文件目录 | | 6 | `stat()` | libVMDexShellx.so | 文件系统上的 Frida 文件名 | | 7 | `__system_property_get` | libVMDexShellx.so | `ro.debuggable`, `ro.build.tags` | | 8 | `syscall(SYS_tgkill=131)` | libdexshell.so | 原始 syscall 绕过 `kill()` hook | | 9 | **VMP dispatch 完整性** | libdexshellx.so | 黄金比例计数器 + 每个方法的 LR thunk 检查 | | 10 | **InstrumentationHijacker** | Runtime DEX | ProxyInstrumentation 监控所有 activity hooks | | 11 | `delayed_detection` | DREXz | 延迟约 4 秒的完整扫描 | ### GOT 陷阱绕过 ``` // DexShell overwrites libart.so GOT with 0x77d61f0220 on Frida detection. // Any JNI call through that stub crashes. Catch and resume at LR: Process.setExceptionHandler(details => { if (details.address.equals(ptr('0x77d61f0220'))) { details.context.pc = details.context.lr; details.context.x0 = ptr(1); return true; // handled, resume } }); ``` ### `dl_iterate_phdr` 绕过 ``` // ByteHook calls dl_iterate_phdr to find frida-agent-64.so. // Interceptor.replace causes infinite recursion here — must use Interceptor.attach: Interceptor.attach(Module.findExportByName(null, 'dl_iterate_phdr'), { onEnter(args) { this._origCallback = args[0]; const filtered = new NativeCallback((info, size, data) => { const name = info.add(16).readCString() || ''; if (name.includes('frida') || name.includes('frijia')) return 0; return this._origCallback(info, size, data); }, 'int', ['pointer','size_t','pointer']); args[0] = filtered; } }); ``` ## VMP 架构:v26 vs v29 ### v26 — 线程化字节码解释器 ``` Protected Java method → N.invoke(methodIndex, args[]) ↓ libdexshellx.so: fetch bytecode record from DexShell.mp3 container @ offset[idx] ↓ 256-opcode threaded interpreter loop ↓ Return result via JNI ``` **DexShell.mp3 容器二进制格式** (data section @ `0xD1051`): ``` // Per-class (repeating): uint16 name_len; char[] class_descriptor; // e.g. "La/o/MainActivety;" uint16 method_count; // ⚠️ uint16 NOT uint32 — critical for parsing repeat method_count: uint32 global_method_idx; uint32 vm_bytecode_offset; // Total: 1,395 classes, ~14,375 methods across 3 ELFs (arm64/arm32/x86) ``` ### v29 — 原生 AOT 编译器 ``` Protected Java method → N.ᛱᛱ᛽(methodIndex, args[]) (Runic-named dispatcher) ↓ libdexshellx.so: funcVaddr[methodIndex] ← dispatch table from RELA relocs ↓ Integrity checks: - Golden-ratio counter: counter * 0x9e3779b9 ^ 0x61c88646 at fixed .data slot - Dispatch return addr: LR must match expected thunk ↓ (any mismatch → JNIEnv->FatalError) Execute compiled ARM64 native function (1,448 methods, 1,140 named) ``` **Dispatch table 提取:** ``` # 对 .data.rel.ro 应用 R_AARCH64_RELATIVE 重定位 → 获取 funcVaddr[methodId] for r_off, r_type, _, r_addend in elf.rela_entries('.rela.dyn'): if r_type == 1027: # R_AARCH64_RELATIVE struct.pack_into('isInstalled()Z // methodId: 0x3f vaddr: 0xff0b8 size: 732 0x000ff0b8 ff8303d1 sub sp, sp, 0xe0 0x000ff0e4 08fd9152 movk w8, #0x9e37, lsl#16 ; golden ratio check 0x000ff0e8 e8b97272 movk w8, #0x61c8 0x000ff0ec 08783cf9 ldr x8, [x8, #0x6f0] ; JNIEnv->GetPrimitiveArrayCritical 0x000ff0f4 00181591 add x0, x0, 0x546 ; "isInstalled" ← string annotated 0x000ff0fc 089842f9 ldr x8, [x8, #0x530] ; JNIEnv->NewStringUTF ``` ## InstrumentationHijacker (v29 新增) 使用 `ProxyInstrumentation` 替换 `ActivityThread.mInstrumentation` — 拦截所有 activity 生命周期事件。 **安装链(根据 ASM 反汇编重构):** ``` // methodId 0x58 — ᛱ᛽᛽ᲅ()V — called from Application.onCreate if (!installGuard.compareAndSet(false, true)) return; // one-time guard Object thread = getActivityThread(); // methodId 0x5c Field f = ActivityThread.class.getDeclaredField("mInstrumentation"); f.setAccessible(true); Instrumentation orig = (Instrumentation) f.get(thread); injectProxy(thread, orig); // methodId 0x56 — swap ``` **方法映射:** | methodId | 名称 | 用途 | |----------|------|---------| | `0x3f` | `isInstalled()Z` | 检查 proxy 是否激活 | | `0x41` | `uninstall()V` | 恢复原始的 Instrumentation | | `0x42–44` | `ᛱᛱᛱ/ᛱᛱᛲ/ᛱᛱᛳ ()Z` | 检测标志布尔值 | | `0x56` | `ᛱ᛼᲋ᲊ(Object,Instrumentation)V` | **注入** ProxyInstrumentation | | **`0x58`** | **`ᛱ᛽᛽ᲅ()V`** | **安装** — 来自 `onCreate` 的入口 | | `0x5c` | `ᛱᲆ᲋ᛳ()Object` | 获取当前的 `ActivityThread` | **绕过:** ``` const IH = Java.use('com.dexshell.x.InstrumentationHijacker'); IH['ᛱ᛽᛽ᲅ'].implementation = function() {}; // block install IH['ᛱ᛼᲋ᲊ'].implementation = function(a, b) {}; // block inject IH['isInstalled'].implementation = () => false; ['ᛱᛱᛱ','ᛱᛱᛲ','ᛱᛱᛳ','ᛱᛱᛶ','ᛱᛱᛸ'].forEach(m => IH[m].implementation = () => false); ``` ## DEX Header 混淆 (v29 新增) `DexHeaderRestore` 在磁盘上混淆 header,并针对 ART 进行即时还原。 | 字段 | 原始值 | 混淆后 | |-------|----------|-----------| | `magic[0:8]` | `dex\n035\0` | `"DexShell"` | | 所有 `*_off` 字段 | 有效偏移量 | 哨兵值 `0xffff0Xc0` | | `map_off` | 有效 | `0` | | `file_size` | 实际大小 | 伪造的较小值 | | `header_size`, `endian_tag`, checksum, SHA-1 | ✅ **未更改** | — | **完整重构** (工具:`tools/deobfuscate_dexheader.py`): ``` def deobfuscate_dex(data): d = bytearray(data) d[0:8] = b'dex\n035\x00' off = 0x70 for sz_off, id_off, entry_size in SIZE_OFF_PAIRS: count = struct.unpack_from(' 0: struct.pack_into(' str: return bytes(c ^ key[i % len(key)] for i, c in enumerate(ciphertext)).decode() ``` **捕获的密钥(通过 `ᛱᲄ᛽ᛵ([B)[B` 上的运行时 Frida hook):** | 密钥 | 明文 | |-----|-----------| | `6df83e88e674e688` | `execStartActivity` | | `fb6c2ac32deba7f8` | `dex_login_panel_bypass` | | `79c7185ad295af69` | `ACCESS_EXPIRED` | | `eac89a5c67793e66` | `DEVELOPER_MODE` | | `fc78d4510348f4ca` | `USB_DEBUG` | | `a51c418694382ceb` | `VPN` | | `b66c9b19ed84993e` | `DexShellx-Instr` | ## Runic Unicode 混淆 (v29 新增) `com.dexshell.x.shell.K` 中的所有 JNI 方法名都从 ASCII 重命名为 **Runic Unicode (U+16A0–U+16FF)**。以此击败 `\w+` regex 模式和基于 ASCII 假设的工具。 | v26 ASCII | v29 Runic | 签名 | 绕过 | |-----------|-----------|-----------|--------| | `K.s` | `K.ᛴᲆᛶ` | `()Z` | `return true` | | `K.b` | `K.ᛴᲆᛷ` | `(String)Z` | `return true` | | `K.j` | `K.ᛴᲆᛵ` | `(String)Object` | passthrough | | `N.invoke` | `N.ᛱᛱ᛽` | `(I,[Object)Object` | VMP dispatcher | ## 网络与后端 **已确认的 endpoint (`dexprotectx.pro`):** ``` POST /dex/mobile/login ← authentication GET /dex/analytics/check ← license check (every launch) GET /dex/update?version=29.0 ← triggers UpdateActivity + auto-download POST /dex/register ← ⚠️ PLAINTEXT registration ``` **v26 → v29 的 IP 变更:** | 版本 | IPv4 | CDN IPv4 | IPv6 | |---------|------|----------|------| | v26 | `172.67.151.252` | `104.21.65.118` | — | | v29 | `172.67.151.252` | `104.21.33.246` | `2606:4700:3032::6815:21f6` + `2606:4700:3031::ac43:97fc` | **屏蔽 (Magisk service.d):** ``` iptables -I OUTPUT -d 172.67.151.252 -j DROP iptables -I OUTPUT -d 104.21.33.246 -j DROP ip6tables -I OUTPUT -d 2606:4700:3032::6815:21f6 -j DROP ip6tables -I OUTPUT -d 2606:4700:3031::ac43:97fc -j DROP ``` **在明文 SharedPreferences 中发现的凭据:** ## APK 中发现的 AOSP Platform 密钥 `decoded/unknown/keys/` 包含: | 文件 | 风险 | |------|------| | `platform.pk8` | 🔴 **系统级** — 使用此密钥签名的 APK 可以请求 `sharedUserId="android.uid.system"` | | `media.pk8` | 🟡 媒体进程访问权限 | | `shared.pk8` | 🟡 共享 UID 访问权限 | | `testkey.pk8` | ⚪ 生产环境低风险 | | `keystore.ks` (JKS) | 🟡 打包了所有密钥 | `platform.pk8` 的 DER header 已确认:`30 82 04 bc 02 01 00 ...` ## 绕过技术 **连接(v29 使用 Zygisk Gadget,而不是 frida-server):** ``` adb forward tcp:14725 tcp:14725 # 手动启动应用,等待约 1.3 秒 frida -H 127.0.0.1:14725 -n Gadget -l frida/bypass_v29_minimal.js ``` **在 `frida/bypass_v29_minimal.js` 中的完整绕过顺序:** ``` // 1. GOT trap exception handler // 2. /proc/self/maps + TracerPid + port 14725 filter // 3. dl_iterate_phdr wrapper (hide Frida from ByteHook) // 4. stat() → ENOENT, opendir/readdir filter, __system_property_get spoof // 5. syscall(SYS_tgkill) self-kill block // 6. K.ᛴᲆᛶ()Z → true, K.ᛴᲆᛷ(String)Z → true // 7. InstrumentationHijacker: ᛱ᛽᛽ᲅ no-op, ᛱ᛼᲋ᲊ no-op, isInstalled→false // 8. System.exit / Process.killProcess / exit/_exit/abort → no-op ``` ## 🛠️ 工具 | 工具 | 描述 | |------|-------------| | [`tools/dexshell_unpack_all.py`](tools/dexshell_unpack_all.py) | 独立的 VMP 脱壳器:ADB pull → ELF 分析 → radare2 反汇编 → 带注释的 ASM + r2 project | | [`tools/gen_asm_from_r2.py`](tools/gen_asm_from_r2.py) | 无需设备,从 `.r2` project + SO 重新生成 `asm/` | | `tools/deobfuscate_dexheader.py` | 完整的 DEX header 重构(重新计算所有 offsets, adler32, SHA-1) | | `tools/decode_config_v29.py` | DXHD5 config 静态分析 + 解密 | | [`frida/bypass_v29_minimal.js`](frida/bypass_v29_minimal.js) | 完整的 v29 绕过脚本 | **快速开始(无需设备 — 使用预先捕获的文件):** ``` python tools/dexshell_unpack_all.py ` --pkg com.x.dexprotectx ` --so dexshellx_unpack/libdexshellx.so ` --dex dexshellx_unpack/classes.dex ` --out my_unpack # 输出:1,448 个 ARM64 汇编文件 + libdexshellx.r2 + classes_smali/ ``` ## v26 → v29 差异对比 | 领域 | v26 | v29 | |------|-----|-----| | `libdexshell.so` | 1,225 KB | **1,374 KB (+12%)** | | `dexshellx.pro` | 2,662 B | **21,283 B (8×)** | | 可见的 smali 类 | 529 | **15 (−514)** | | JNI 方法 | ASCII | **Runic Unicode** | | VMP 机制 | Bytecode interpreter | **Native AOT compiler** | | `InstrumentationHijacker` | 无 | **有** | | `DexHeaderRestore` | 无 | **有** | | `delayed_detection` | 无 | **有** | | 后端 CDN | `104.21.65.118` | **`104.21.33.246` + IPv6** | | 打包的 AOSP 密钥 | 否 | **是 (platform.pk8 等)** | | libdexshell XOR key | `106b07245fa133cd` | `27d39683cc50fcba` | ## 仓库结构 ``` DexShell-Research/ ├── index.html ← Visual HTML with flowcharts & diagrams (open in browser) ├── README.md ├── frida/ │ └── bypass_v29_minimal.js └── tools/ ├── dexshell_unpack_all.py ├── gen_asm_from_r2.py └── README.md ```
*研究仅供教育目的。* **[@BlackOuTv2](https://github.com/BlackOuTv2)**  ·  **[Telegram @BlackOuTv1](https://t.me/BlackOuTv1)**  ·  **[LinkedIn](https://www.linkedin.com/in/black0ut/)**  ·  **[Instagram @cyberxblackout](https://www.instagram.com/cyberxblackout/)**
标签:Android安全, Docker支持, Frida, 云资产清单, 代码脱壳, 动态分析, 安全研究, 逆向工程