BlackOuTv2/DexShell-Research
GitHub: BlackOuTv2/DexShell-Research
DexShell/DexProtectX Android打包器逆向研究工具
Stars: 6 | Forks: 0
# 🔬 DexShell Android Packer — 深度逆向工程






**对商业 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, 云资产清单, 代码脱壳, 动态分析, 安全研究, 逆向工程