/data/dalvik-cache
目录,因此只要将此目录下的 ODEX 取出,进行一次 deodex 操作,即可完成脱壳因第一代壳在内存中完全解密,可从内存中 Dump 要解密 APK 的内存,完成脱壳
脱壳工具:android-unpacker
其核心是通过 find_magic_memory()
读取 /proc/pid/maps
内存映射表,找到 DEX 所在的内存起始位置,通过 dump_memory()
将内存 Dump
int find_magic_memory(uint32_t clone_pid, int memory_fd, memory_region* memory[], char* extra_filter) {
char maps[1024];
snprintf(maps, sizeof(maps), "/proc/%d/maps", clone_pid);
FILE* maps_file = NULL;
if ((maps_file = fopen(maps, "r")) == NULL)
return -1;
char mem_line[1024];
int found = 0;
while (fscanf(maps_file, "%[^\n]\n", mem_line) >= 0) {
if (extra_filter != NULL && strstr(mem_line, extra_filter) == NULL)
continue;
if (extra_filter == NULL && (strstr(mem_line, "/") != NULL || strstr(mem_line, "[") != NULL))
continue;
char mem_address_start[10];
char mem_address_end[10];
sscanf(mem_line, "%8[^-]-%8[^ ]", mem_address_start, mem_address_end);
uint64_t mem_start = strtoul(mem_address_start, NULL, 16);
off64_t offset = peek_memory(memory_fd, mem_start);
if (extra_filter != NULL && offset < 0)
offset = 0;
if (offset >= 0) {
memory[found] = malloc(sizeof(memory_region));
memory[found]->start = mem_start + start;
memory[found++]->end = strtoull(mem_address_end, NULL, 16);
}
else if (offset == -99) {
// No offset found
}
else {
perror(" [!] Error peeking at memory ");
}
}
fclose(maps_file);
return found;
}
int dump_memory(char* class_path, int memory_fd, memory_region* memory, const char* file_name, int ignore_class_path) {
int ret = 0;
char* buffer = malloc(memory->end - memory->start);
printf(" [+] Attempting to search inside memory region 0x%llx to 0x%llx\n", memory->start, memory->end);
if (checkFd < 0) {
perror(" [!] Appears to be an issue with memory fd ");
return -1;
}
ssize_t read = pread64(memory_fd, buffer, (size_t)(memory->end - memory->start), (off64_t)(memory->start));
if (read < 0) {
fprintf(stderr, " [!] pread seems to have failed for 0x%llx to 0x%llx\n", memory->start, memory->end);
return -1;
}
if ((memory->end - memory-start) != read) {
printf(" [!] pread did not read the expected amount of memory!\n");
return -1;
}
off64_t* contained_offset = memmem(buffer, (size_t)(memory->end - memory->start), class_path, strlen(class_path));
FILE* dump = NULL;
if (contained_offset != NULL || ignore_class_path == 1) {
if (contained_offset != NULL)
printf(" [+] Memory region 0x%llx to 0x%llx contained anticipated class path %s\n", memory->start, memory->end);
FILE* dump = fopen(file_name, "wb");
ret = -1;
if (dump != NULL) {
if (fwrite(buffer, memory->end - memory->start, 1, dump) > 0)
ret = 1;
fclose(dump);
}
else {
perror(" [!] Error opening a file to write");
}
}
else {
printf(" [-] Likely a system file found, ignoring...\n");
ret = 0;
}
free(buffer);
return ret;
}
本质仍是内存 Dump 脱壳法
要通过调试器找到合适的 Dump 时机(重点),即 DEX 文件已在内存中完全解密,且其中的代码还没执行
DEX 在 Dalvik 虚拟机中的加载过程中存在 dexopt 优化操作,其起始代码位于 Android 源码文件 dalvik/dexopt/OptMain.cpp
,此 cpp 文件的 main() 即 dexopt 优化 DEX 的入口点:
int main(int argc, char* const argv[]) {
set_process_name("dexopt");
setvbuf(stdout, NULL, _IONBF, 0);
if (argc > 1) {
if (strcmp(argv[1], "-zip") == 0)
return fromZip(argc, argv);
else if (strcmp(argv[1], "-dex") == 0)
return fromDex(argc, argv);
else if (strcmp(argv[1], "-preopt") == 0)
return preopt(argc, argv);
}
...
return 1;
}
上述代码会根据不同的传入参数执行不同的代码,以 DEX 为例,会执行 fromDex():
static int fromDex(int argc, char* const argv[]) {
...
if (dvmPrepForDexOpt(bootClassPath, dexOptMode, verifyMode, flags) != 0) {
ALOGE("VM init failed");
goto bail;
}
vmStarted = true;
if (!dvmContinueOptimization(fd, offset, length, debugFileName, modWhen, crc, (flags & DEXOPT_IS_BOOTSTRAP) != 0)) {
ALOGE("Optimation failed");
goto bail;
}
...
}
dvmContinueOptimization() 执行具体的优化操作:
bool dvmContinueOptimization(int fd, off_t dexOffset, long dexLength, const char* fileName, u4 modWhen, u4 crc, bool isBootstrap) {
...
success = rewriteDex(((u1*)mapAddr) + dexOffset, dexLength, doVerify, doOpt, &pClassLookup, NULL);
if (success) {
DvmDex* pDvmDex = NULL;
u1* dexAddr = ((u1*)mapAddr) + dexOffset;
if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) {
ALOGE("Unable to create DexFile");
success = false;
}
else {
/* If Configured to do so, generate register map
output for all verified classes. The register maps
were generated during verification, and will now
be serialized. */
if (gDvm.generateRegisterMaps) {
pRegMapBuilder = dvmGenerateRegisterMaps(pDvmDex);
if (pRegMapBuilder == NULL) {
ALOGE("Failed generate register maps");
success = false;
}
}
DexHeader* pHeader = (DexHeader*)pDvmDex->pHeader;
updateChecksum(dexAddr, dexLength, pHeader);
dvmDexFileFree(pDvmDex);
}
}
...
}
rewriteDex() 执行 DEX 的优化对齐和验证操作
操作成功后,会执行 dvmDexFileOpenPartial(),将内存中的 DEX 数据转换成 DvmDex 结构体。注意:传入的前两个参数 dexAddr 和 dexLength 为优化验证完成后的 DEX 的数据起始地址和占用的字节数,此时 DEX 数据已在内存中解密
在调试器中给上述函数下断点,执行到断点时,即可用内存 Dump 脚本将其 Dump,完成脱壳
遗憾的是,上述方法很快便因软件壳升级而失效(新的软件壳在内部模拟实现上述函数的内容)
查看 dvmDexFileOpenPartial() 的实现代码:
int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex) {
DvmDex* pDvmDex;
DexFile* pDexFile;
int parseFlags = kDexParseDefault;
int result = -1;
pDexFile = dexFileParse((u1*)addr, len, parseFlags);
if (pDexFile == NULL) {
ALOGE("DEX parse failed");
goto bail;
}
pDvmDex = allocateAuxStructures(pDexFile);
if (pDvmDex == NULL) {
dexFileFree(pDexFile);
goto bail;
}
pDvmDex->isMappedReadOnly = false;
*ppDvmDex = pDvmDex;
result = 0;
bail:
return result;
}
可看出,dvmDexFileOpenPartial() 内部调用的 dexFileParse(),传入的前两个参数与 dvmDexFileOpenPartial() 一样,因此该方法也是脱壳点(各种软件壳在模拟 dvmDexFileOpenPartial() 的实现时在内部调用了 dexFileParse())
同样的,该脱壳法很快失效
但查看 dexFileParse() 的代码:
DexFile* dexFileParse(const u1* data, size_t length, int flags) {
DexFile* pDexFile = NULL;
const DexHeader* pHeader;
const u1* magic;
int result = -1;
if (length < sizeof(DexHeader)) {
ALOGE("too short to be a valid .dex");
goto bail;
}
pDexFile = (DexFile*)malloc(sizeof(DexFile));
if (pDexFile == NULL)
goto bail; // alloc failure
memset(pDexFile, 0, sizeof(DexFile));
// Peel off the optimized header
if (memcmp(data, DEX_OPT_MAGIC, 4) == 0) {
magic = data;
if (memcmp(magic + 4, DEX_OPT_MAGIC_VERS, 4) != 0) {
ALOGE("bad opt version (0x%02x %02x %02x %02x)", magic[4], magic[5], magic[6], magic[7]);
goto bail;
}
...
}
}
dexFileParse() 在进行 DEX 结构体解析时,会调用 memcmp() 判断 DEX 的文件头信息。当第二个参数为 DEX_OPT_MAGIC
时,第一个参数 data 即 DEX 解密后的数据起始地址,而占用的字节数可通过 DEX 文件头中相应字段获取。此处即内存 Dump 时机
libdvm.so
找到上述关键函数,下好断点,配合 IDA 脚本,即可 dump dex和动态调试脱壳法一样,都要找到合适的脱壳时机
不同点
以运行在 Android 4.3 的 Cydia Substrate 框架为例,编写如下代码 Hook 其 dexFileParse():
MSInitialize {
MSImageRef image = MSGetImageByName("/system/lib/libdvm.so");
if (image != NULL) {
void* dexload = MSFindSymbol(image, "_Z12dexFileParsePKhji");
if (dexload == NULL) {
LOGD("error find _Z12dexFileParsePKhji");
}
else {
MSHookFunction(dexload, (void*)&myDexFileParse, (void**)&oldDexFileParse);
}
}
else {
LOGE("error.\n");
}
}
myDexFileParse() 为自己实现的 dexFileParse()。向代码中插入 DEX 脱壳的逻辑,将内存中的 DEX 数据保存到本地,调用 oldDexFileParse() 执行原始的代码逻辑,即可完成脱壳
和 Hook 脱壳法类似
针对第一代壳在 dvmDexFileOpenPartial() 或 dexFileParse() 处设断点脱壳的特点,修改它们在源码中的实现,编译修改后的代码,以刷机方式实现脱壳
以 dvmDexFileOpenPartial() 为例,可将其代码实现修改为:
int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex) {
DvmDex* pDvmDex;
DexFile* pDexFile;
int parseFlags = kDexParseDefault;
int result = -1;
pDexFile = dexFileParse((u1*)addr, len, parseFlags);
if (pDexFile == NULL) {
ALOGE("DEX parse failed");
goto bail;
}
#ifdef __ARM_EABI__
ALOGE("dumpdex: addr = %08x, len = %d\n", (u4)addr, len);
char path[128] = {0};
sprintf(path, "/data/local/tmp/%d.dex", getpid());
ALOGE("path = %s\n", path);
if fd = open(path, O_CREAT | O_RDWR, 0644);
if (fd) {
ALOGE("Dumping dex...\n");
int ret = write(fd, addr, len);
ALOGE("Dump dex finished. ret = %d\n", ret);
close(fd);
}
else {
ALOGE("Dump dex error\n");
}
#endif
pDvmDex = allocateAuxStructures(pDexFile);
if (pDvmDex == NULL) {
dexFileFree(pDexFile);
goto bail;
}
pDvmDex->ismappedReadOnly = false;
*ppDvmDex = pDvmDex;
result = 0;
bail:
return result;
}
刷机后,APK 在加载时会自动将 DEX 保存到 /data/local/tmp
目录,并以当前进程 ID 命名,只要知道进程 ID,即可找到脱壳后的 DEX