01
整体壳:把整个 DEX 文件加密打包,运行时整体解密加载。静态下看不到任何代码,但解密后是完整的 DEX。 抽取壳:把每个方法的代码数据(CodeItem)从 DEX 里“掏空”,在原位置填入 NOP 指令;被掏出的 CodeItem 加密后另存,运行时在类加载时再“填回去”。静态看方法体是空的,必须还原才能看到真实逻辑。dpt-shell 走的就是这条路。

02
assets/OoooooOooo —— 加密存储的方法代码数据(CodeItem),是还原逻辑的唯一来源。 assets/dshelldata_001 —— 壳配置文件,使用 AES-256-CBC 加密,保存壳运行所需的各种配置。 assets/vwwwwwvwww/libe01ce7566771bf96.so —— Native 库,支持 arm/arm64/x86/x86_64 四种架构,负责运行时解密回填。

03
void init_dpt() {DECRYPT_BITCODEdecrypt_bitcode(); // 解密 .bitcode 段(RC4)dpt_hook(); // 安装 Hook}
void dpt_hook() {bytehook_init(BYTEHOOK_MODE_AUTOMATIC, false);g_sdkLevel = android_get_device_api_level();hook_execve(); // Hook execvehook_mmap(); // Hook mmap(脱壳对抗的关键点)hook_write(); // Hook writebool ok = hook_DefineClass();if (!ok) hook_LoadClass(); // DefineClass 失败则退回 LoadClass}
DPT_ENCRYPT bool hook_DefineClass() {const char *path = GetClassLinkerDefineClassLibPath();char sym[256] = {0};find_symbol_in_elf_file(path, sym, ARRAY_LENGTH(sym), 2,"ClassLinker", "DefineClass");void *addr = DobbySymbolResolver(path, sym);// 用 Dobby 把 ClassLinker::DefineClass 替换成 DefineClassV22return DobbyHook(addr, (dobby_dummy_func_t) DefineClassV22,(dobby_dummy_func_t *) &g_originDefineClassV22) == 0;}
DPT_ENCRYPT void *DefineClassV22(void *thiz, void *self,const char *descriptor, size_t hash, void *class_loader,const void *dex_file, const void *dex_class_def) {if (LIKELY(g_originDefineClassV22 != nullptr)) {patchClass(descriptor, dex_file, dex_class_def); // 先回填 CodeItemreturn g_originDefineClassV22(thiz, self, descriptor, hash,class_loader, dex_file, dex_class_def);}return nullptr;}
DPT_ENCRYPT void patchClass(const char *descriptor,const void *dex_file, const void *dex_class_def) {// 1. 垃圾类检测:发现非法调用直接崩溃(对抗脱壳机)...// 2. 依据 SDK 版本解析 DexFile 结构,拿到 begin/location/dexSize...auto *class_def = (dex::ClassDef *) dex_class_def;if (LIKELY(class_def->class_data_off_ != 0)) {auto *class_data = (uint8_t *) begin + class_def->class_data_off_;size_t read = 0;// ULEB128 变长编码依次读出字段数、方法数read += DexFileUtils::readUleb128(class_data, &static_fields_size);read += DexFileUtils::readUleb128(class_data + read, &instance_fields_size);read += DexFileUtils::readUleb128(class_data + read, &direct_methods_size);read += DexFileUtils::readUleb128(class_data + read, &virtual_methods_size);// 跳过字段区,再读出直接方法与虚方法read += DexFileUtils::getFieldsSize(class_data + read, static_fields_size);read += DexFileUtils::getFieldsSize(class_data + read, instance_fields_size);read += DexFileUtils::readMethods(class_data + read, directMethods, direct_methods_size);read += DexFileUtils::readMethods(class_data + read, virtualMethods, virtual_methods_size);// 逐个方法回填 CodeItemfor (uint64_t i = 0; i < direct_methods_size; i++)patchMethod(begin, location, dexSize, dexIndex,directMethods[i].method_idx_delta_,directMethods[i].code_off_);for (uint64_t i = 0; i < virtual_methods_size; i++)patchMethod(begin, location, dexSize, dexIndex,virtualMethods[i].method_idx_delta_,virtualMethods[i].code_off_);}}
DPT_ENCRYPT ALWAYS_INLINE void patchMethod(uint8_t *begin,const char *location, uint32_t dexSize, int dexIndex,uint32_t methodIdx, uint32_t codeOff) {// location:类描述符,用于日志定位;核心解密回填逻辑见下auto it = dexMap.find(dexIndex);if (LIKELY(it != dexMap.end())) {auto codeItem = it->second->at(methodIdx);if (LIKELY(codeItem != nullptr)) {if (codeOff == 0) return; // 无方法体,跳过// 目标地址:DEX 内存中该方法指令区(begin + codeOff)auto *dexCodeItem = (dex::CodeItem *)(begin + codeOff);auto *realInsnsPtr = (uint8_t *)(dexCodeItem->insns_);// 加密后的指令数据const uint8_t *enc = codeItem->getInsns();uint32_t sz = codeItem->getInsnsSize();// RC4 密钥 = aes_key(壳配置密钥) + methodIndex(4 字节)uint8_t rc4_key[sizeof(g_shell_config.aes_key) + sizeof(uint32_t)];memcpy(rc4_key, g_shell_config.aes_key, sizeof(g_shell_config.aes_key));uint32_t methodIndex = codeItem->getMethodIdx();memcpy(rc4_key + sizeof(g_shell_config.aes_key), &methodIndex,sizeof(methodIndex));struct rc4_state state;rc4_init(&state, rc4_key, sizeof(rc4_key));rc4_crypt(&state, enc, realInsnsPtr, sz); // 解密写回 DEX 内存}}}

DPT_ENCRYPT void init_app(JNIEnv *env, jclass) {void *package_addr = nullptr; size_t package_size = 0;load_package(env, &package_addr, &package_size);if (!g_codeItemFileData.has_value()) {// 从 assets 读取加密后的 CodeItem 数据(即 OoooooOooo)auto entry = read_zip_file_entry(package_addr, package_size,AY_OBFUSCATE(CODE_ITEM_NAME_IN_ZIP));if (entry.has_value()) g_codeItemFileData = std::move(entry);}auto [entry_data, entry_size] = g_codeItemFileData.value();readCodeItem((uint8_t *) entry_data, entry_size); // 解析成 dexMappthread_mutex_lock(&g_write_dexes_mutex);extractDexesInNeeded(env, package_addr, package_size); // 释放被抽空的 DEXpthread_mutex_unlock(&g_write_dexes_mutex);unload_package(package_addr, package_size);}
反 Frida:通过扫描进程内存映射里的 "frida-agent" 字符串、遍历线程名找 "frida-thread",检测到 Frida 注入后立即崩溃。 反调试:利用 ptrace(PTRACE_TRACEME) 自追踪,一旦失败(说明已被调试器附加)就崩溃。 进程保护:通过 fork() 创建子进程互相监控、保护,防止被轻易终止。 垃圾类保护:在 patchClass 里检查“垃圾类”是否被恶意调用,若类描述符倒数第二个字符是数字(正常类名不会这样)则视为非法调用触发崩溃。
__arm__extern "C" void dpt_crash() { __asm__ __volatile__("b ."); } // 死循环defined(__aarch64__)extern "C" void dpt_crash() { __asm__ __volatile__("b ."); } // 死循环extern "C" void dpt_crash() { *(int*)0 = 0; } // 触发段错误
04
入口类 MainActivity 里出现字符串 "dpt-shell seem not working.\nApplication:"。 assets 目录同时存在 OoooooOooo 与 dshelldata_001 两个标志性文件。 assets/vwwwwwvwww/ 下存在 Native 库 libe01ce7566771bf96.so。 AndroidManifest.xml 中 Application 类名被替换为代理类,如 oUqSLEomaDCs.ProxyApplication(不同 APK 名字不同,但都是 *.ProxyApplication 形式)。

内存 dump:在 App 运行时(类已被加载、CodeItem 已回填到内存后),从进程内存中 dump 出完整的 DEX。这是通用做法,但 dpt-shell 的 mmap/write Hook 与反调试会给 dump 制造障碍。 静态还原:不走运行时,而是直接解析 assets/OoooooOooo 的数据结构,把每个 methodIndex 对应的加密指令 解密后原位写回 DEX 的 code_off 位置,再重算 DEX 头 SHA1 签名与 Adler-32 校验和。这需要对壳的数据格式与加密算法有精确理解,也是针对性最强、最干净的做法。
05
init_dpt()└─ dpt_hook()└─ hook_DefineClass() // Dobby Hook ART└─ DefineClassV22() // 类加载前介入└─ patchClass() // 解析类数据、遍历方法└─ patchMethod() // RC4 解密写回 CodeItem└─ 原始 DefineClass() // 完成真正的类加载
06
第一,逆向分析早已不是“跑一个反编译工具”这么简单。读懂壳的工作机制、在正确的时机取数,往往比工具本身更重要。 第二,加固与脱壳本质上是同一枚硬币的两面:理解了壳如何“藏”,自然就知道如何“找”——无论是识别特征(OoooooOooo、ProxyApplication、特征字符串),还是脱壳思路(内存 dump / 静态还原),背后都是同一套机制在起作用。
