第十二章 软件壳(三)(动态加载型壳)

文章目录

  • 动态加载型壳
    • 缓存脱壳法
    • 内存 Dump 脱壳法
    • 动态调试脱壳法
      • 总结
    • Hook 脱壳法
    • 系统定制脱壳法

动态加载型壳

  • 即第一代壳
  • 其发展时期正是从 Android 4.4 向 Android 5.0 迈进的从 Dalvik 虚拟机向 ART 虚拟机转型时期
  • 这一时期的软件壳,早期版本主要针对 Dalvik 虚拟机的实现,特点是对本地 APK 中的数据加密,运行时在内存中解密

缓存脱壳法

  • 动态加载型壳用 DexClassLoader 方式将加密后的 DEX 在内存中解密后动态加载,但一些软件壳没有处理 DEX 优化时缓存的路径,最终使得系统执行 dexopt 命令对加载的 DEX 进行优化时,将优化结果放到默认的 /data/dalvik-cache 目录,因此只要将此目录下的 ODEX 取出,进行一次 deodex 操作,即可完成脱壳

内存 Dump 脱壳法

  • 因第一代壳在内存中完全解密,可从内存中 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 时机


总结

  • 从 Dump 时机升级过程可看出,动态调试脱壳法是个实时性较强的方法,有效性和当前软件壳对 DEX 加载代码的处理方式有关
  • 以 IDA 的动态调试结合 libdvm.so 找到上述关键函数,下好断点,配合 IDA 脚本,即可 dump dex

Hook 脱壳法

  • 和动态调试脱壳法一样,都要找到合适的脱壳时机

  • 不同点

    • 动态调试脱壳法:手工操作调试器完成脱壳
    • Hook 脱壳法:使用 Hook 框架,配合 Hook 代码,实现工具的自动化脱壳
  • 以运行在 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

你可能感兴趣的:(《Android,软件安全权威指南》学习笔记)