ART 下 Dex 加载流程源码分析 和 通用脱壳点

版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/

DEF CON

DEF CON 是全球最大的计算机安全会议之一(极客的奥斯卡),自1993年6月起,每年在美国内华达州的拉斯维加斯举办。

官网:https://media.defcon.org/,DEF CON 黑客大会官方的媒体存档站点,提供历年 DEF CON 大会的公开演讲、幻灯片、视频、音频、代码示例和其他相关资源的免费下载。

在 DEF CON 25(2017 年)上,Check Point 的安全研究员 Slava Makkaveev 和 Avi Bashan 发表了题为《Unboxing Android: Everything You Wanted to Know About Android Packers》的演讲,深入探讨了 Android 应用程序中的加壳技术及其安全影响。

报告文件地址:

https://media.defcon.org/DEF%20CON%2025/DEF%20CON%2025%20presentations/DEF%20CON%2025%20-%20Slava-Makkaveev-and-Avi-Bashan-Unboxing-Android.pdf

对于国内加壳厂商也有分析

word/media/image1.png

DEF 的安全研究员选择的两个脱壳点:art::OpenAndReadMagic 和 DexFile::DexFile

word/media/image2.png

Unboxing Android

在 DEF CON 25 (2017) 上,Avi Bashan 和 Slava Makkaveev 提出过一种非常实用的 Android 加壳脱壳技术:

通过修改 DexFile::DexFile() 构造函数和 OpenAndReadMagic() 方法,可以在应用运行时,拦截 DEX 文件加载过程,从而拿到已经解密后的内存数据,完成脱壳。

1. DexFile::DexFile 构造函数

可以看到 DexFile::DexFile() 的构造函数参数里包含了:

  • const uint8_t* base —— DEX 在内存中的起始地址

  • size_t size —— DEX 的内存大小

DexFile::DexFile(const uint8_t* base,
                 size_t size,
                 const uint8_t* data_begin,
                 size_t data_size,
                 const std::string& location,
                 uint32_t location_checksum,
                 const OatDexFile* oat_dex_file,
                 std::unique_ptr<DexFileContainer> container,
                 bool is_compact_dex)
    : begin_(base),
      size_(size),
      data_begin_(data_begin),
      data_size_(data_size),
      location_(location),
      location_checksum_(location_checksum),
      header_(reinterpret_cast<const Header*>(base)),
      string_ids_(reinterpret_cast<const StringId*>(base + header_->string_ids_off_)),
      type_ids_(reinterpret_cast<const TypeId*>(base + header_->type_ids_off_)),
      field_ids_(reinterpret_cast<const FieldId*>(base + header_->field_ids_off_)),
      method_ids_(reinterpret_cast<const MethodId*>(base + header_->method_ids_off_)),
      proto_ids_(reinterpret_cast<const ProtoId*>(base + header_->proto_ids_off_)),
      class_defs_(reinterpret_cast<const ClassDef*>(base + header_->class_defs_off_)),
      method_handles_(nullptr),
      num_method_handles_(0),
      call_site_ids_(nullptr),
      num_call_site_ids_(0),
      hiddenapi_class_data_(nullptr),
      oat_dex_file_(oat_dex_file),
      container_(std::move(container)),
      is_compact_dex_(is_compact_dex),
      hiddenapi_domain_(hiddenapi::Domain::kApplication) {
  CHECK(begin_ != nullptr) << GetLocation();
  CHECK_GT(size_, 0U) << GetLocation();
  // Check base (=header) alignment.
  // Must be 4-byte aligned to avoid undefined behavior when accessing
  // any of the sections via a pointer.
  CHECK_ALIGNED(begin_, alignof(Header));

  InitializeSectionsFromMapList();
}

https://cs.android.com/android/platform/superproject/+/android-10.0.0_r47:art/libdexfile/dex/dex_file.cc;l=96

word/media/image3.png

插入脱壳代码示例

// 打印当前 DEX 文件的位置
LOG(WARNING) << "Dex File: Filename: " << location;

// 判断这个 DEX 是不是从 APP 自身私有目录 加载的。
// 因为系统自己的 framework、boot.oat 里的 DEX 都不是加壳 DEX,只想 dump 应用自己的 DEX。
if (location.find("/data/data/") != std::string::npos) {
    LOG(WARNING) << "Dex File: OAT file unpacking launched";

    // 创建一个新的文件,比如 /data/data/包名/xxx.dex__unpacked_oat。
    std::ofstream dst(location + "__unpacked_oat", std::ios::binary);
    // 把内存里的 DEX 数据完整写入磁盘。
    dst.write(reinterpret_cast<const char*>(base), size);
    // 保存文件,完成脱壳。
    dst.close();
} else {
    LOG(WARNING) << "Dex File: OAT file unpacking not launched";
}

2. DexFile::OpenAndReadMagic()

这是辅助检查 DEX 文件头的函数。

File OpenAndReadMagic(const char* filename, uint32_t* magic, std::string* error_msg) {
  CHECK(magic != nullptr);
  File fd(filename, O_RDONLY, /* check_usage= */ false);
  if (fd.Fd() == -1) {
    *error_msg = StringPrintf("Unable to open '%s' : %s", filename, strerror(errno));
    return File();
  }
  if (!ReadMagicAndReset(fd.Fd(), magic, error_msg)) {
    StringPrintf("Error in reading magic from file %s: %s", filename, error_msg->c_str());
    return File();
  }
  return fd;
}

https://cs.android.com/android/platform/superproject/+/android-10.0.0_r47:art/libartbase/base/file_magic.cc;l=32

word/media/image4.png

插入脱壳代码示例

struct stat st;  // 用于获取文件大小等信息

// 打印当前正在处理的文件路径,便于调试和观察加载的 DEX 来源
LOG(WARNING) << "File_magic: Filename: " << filename;

// 只处理 /data/data 路径下的文件(即应用私有目录中的 dex 文件)
// 这样可以避免处理系统 DEX,提高效率和准确性
if (strstr(filename, "/data/data") != NULL) {
  LOG(WARNING) << "File_magic: DEX file unpacking launched";

  // 构造输出文件路径,加上 "__unpacked_dex" 后缀
  char* fn_out = new char[PATH_MAX];
  strcpy(fn_out, filename);
  strcat(fn_out, "__unpacked_dex");

  // 创建输出文件,设置权限:用户可读写、用户组可读、其他人可读
  int fd_out = open(fn_out, O_WRONLY | O_CREAT | O_EXCL,
                    S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);

  // 如果获取原始 dex 文件信息成功(用于获取文件大小)
  if (!fstat(fd.get(), &st)) {
    // 使用 mmap 将整个 dex 文件映射到内存中
    char* addr = (char*)mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd.get(), 0);

    // 将内存中的内容写入到新文件,完成磁盘级别的 dex dump
    int ret = write(fd_out, addr, st.st_size);

    // 可选防优化代码(保证 ret 被使用,防止编译器优化)
    ret += 1;

    // 解除映射,释放内存
    munmap(addr, st.st_size);
  }

  // 关闭输出文件,清理路径内存
  close(fd_out);
  delete[] fn_out;

} else {
  // 如果不是应用私有路径下的文件,跳过处理
  LOG(WARNING) << "File_magic: DEX file unpacking not launched";
}

ART 下脱壳原理

ART 下常见的两个 dex 加载器:InMemoryDexClassLoader 和 DexClassLoader

InMemoryDexClassLoader 源码分析

InMemoryDexClassLoader 是 Android 8.0(API 级别 26)引入的一个类,用于动态加载内存中的 Dex。

调用示例:

// 假设 dexBytes 是你的 DEX 文件内容(可以通过解密获得)
ByteBuffer buffer = ByteBuffer.wrap(dexBytes);

// 创建 InMemoryDexClassLoader
ClassLoader loader = new InMemoryDexClassLoader(buffer, ClassLoader.getSystemClassLoader());

// 通过反射加载类并调用方法
Class<?> clazz = loader.loadClass("com.example.MyHiddenClass");
Method m = clazz.getDeclaredMethod("secretMethod");
m.invoke(null);

InMemoryDexClassLoader 支持加载 内存中 一个或多个 Dex。源码如下:

word/media/image5.png
https://cs.android.com/android/platform/superproject/+/android-10.0.0_r47:libcore/dalvik/src/main/java/dalvik/system/InMemoryDexClassLoader.java

openInMemoryDexFilesNative

Dex 加载过程如下,最终调用到 native 方法 openInMemoryDexFilesNative

 InMemoryDexClassLoader(ByteBuffer[] dexBuffers, String librarySearchPath, ClassLoader parent)
 └── BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent)
      └── DexPathList.initByteBufferDexPath(ByteBuffer[] dexFiles)
           └── DexFile(ByteBuffer[] bufs, ClassLoader loader, DexPathList.Element[] elements)
                └── DexFile.openInMemoryDexFiles(ByteBuffer[] bufs, ClassLoader loader, DexPathList.Element[] elements)
                     └── DexFile.openInMemoryDexFilesNative(ByteBuffer[] bufs, byte[][] arrays, int[] starts, int[] ends, ClassLoader loader, DexPathList.Element[] elements)
                         └── DexFile_openInMemoryDexFilesNative(JNIEnv* env, jclass, jobjectArray buffers, jobjectArray arrays, jintArray jstarts, jintArray jends, jobject class_loader, jobjectArray dex_elements)

https://cs.android.com/android/platform/superproject/main/+/main:libcore/dalvik/src/main/java/dalvik/system/DexFile.java;l=134

DexFile_openInMemoryDexFilesNative 中 调用 OpenDexFilesFromOat 方法 加载 Dex :

static jobject DexFile_openInMemoryDexFilesNative(JNIEnv* env,
                                                  jclass,
                                                  jobjectArray buffers,
                                                  jobjectArray arrays,
                                                  jintArray jstarts,
                                                  jintArray jends,
                                                  jobject class_loader,
                                                  jobjectArray dex_elements) {
  jsize buffers_length = env->GetArrayLength(buffers);
  CHECK_EQ(buffers_length, env->GetArrayLength(arrays));
  CHECK_EQ(buffers_length, env->GetArrayLength(jstarts));
  CHECK_EQ(buffers_length, env->GetArrayLength(jends));

  ScopedIntArrayAccessor starts(env, jstarts);
  ScopedIntArrayAccessor ends(env, jends);

  // Allocate memory for dex files and copy data from ByteBuffers.
  std::vector<MemMap> dex_mem_maps;
  dex_mem_maps.reserve(buffers_length);
  for (jsize i = 0; i < buffers_length; ++i) {
    jobject buffer = env->GetObjectArrayElement(buffers, i);
    jbyteArray array = reinterpret_cast<jbyteArray>(env->GetObjectArrayElement(arrays, i));
    jint start = starts.Get(i);
    jint end = ends.Get(i);

    MemMap dex_data = AllocateDexMemoryMap(env, start, end);
    if (!dex_data.IsValid()) {
      DCHECK(Thread::Current()->IsExceptionPending());
      return nullptr;
    }

    if (array == nullptr) {
      // Direct ByteBuffer
      uint8_t* base_address = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
      if (base_address == nullptr) {
        ScopedObjectAccess soa(env);
        ThrowWrappedIOException("dexFileBuffer not direct");
        return nullptr;
      }
      size_t length = static_cast<size_t>(end - start);
      memcpy(dex_data.Begin(), base_address + start, length);
    } else {
      // ByteBuffer backed by a byte array
      jbyte* destination = reinterpret_cast<jbyte*>(dex_data.Begin());
      env->GetByteArrayRegion(array, start, end - start, destination);
    }

    dex_mem_maps.push_back(std::move(dex_data));
  }

  // Hand MemMaps over to OatFileManager to open the dex files and potentially
  // create a backing OatFile instance from an anonymous vdex.
  std::vector<std::string> error_msgs;
  const OatFile* oat_file = nullptr;
  std::vector<std::unique_ptr<const DexFile>> dex_files =
      Runtime::Current()->GetOatFileManager().OpenDexFilesFromOat(std::move(dex_mem_maps),
                                                                  class_loader,
                                                                  dex_elements,
                                                                  /*out*/ &oat_file,
                                                                  /*out*/ &error_msgs);
  return CreateCookieFromOatFileManagerResult(env, dex_files, oat_file, error_msgs);
}

https://cs.android.com/android/platform/superproject/+/android-10.0.0_r47:art/runtime/native/dalvik_system_DexFile.cc;l=240

OpenDexFilesFromOat

OpenDexFilesFromOat 调用 OpenDexFilesFromOat_Impl 加载 Dex

std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat_Impl(
    std::vector<MemMap>&& dex_mem_maps,
    jobject class_loader,
    jobjectArray dex_elements,
    const OatFile** out_oat_file,
    std::vector<std::string>* error_msgs) {
  ScopedTrace trace(__FUNCTION__);
  std::string error_msg;
  DCHECK(error_msgs != nullptr);

  // [1] 提取 Dex Header,用于后续校验 checksum、生成路径等
  const std::vector<const DexFile::Header*> dex_headers = GetDexFileHeaders(dex_mem_maps);

  // [2] 生成临时匿名 dex/vdex 文件路径,获取 checksum 和路径
  uint32_t location_checksum;
  std::string dex_location;
  std::string vdex_path;
  bool has_vdex = OatFileAssistant::AnonymousDexVdexLocation(
      dex_headers, kRuntimeISA, &location_checksum, &dex_location, &vdex_path);

  // [3] 尝试打开 vdex 文件,并检查其中的 dex checksum 是否一致
  std::unique_ptr<VdexFile> vdex_file = nullptr;
  if (has_vdex && OS::FileExists(vdex_path.c_str())) {
    vdex_file = VdexFile::Open(vdex_path, /*writable=*/false, /*low_4gb=*/false,
                               /*unquicken=*/false, &error_msg);
    if (vdex_file == nullptr) {
      LOG(WARNING) << "Failed to open vdex " << vdex_path << ": " << error_msg;
    } else if (!vdex_file->MatchesDexFileChecksums(dex_headers)) {
      LOG(WARNING) << "Dex checksum mismatch: " << vdex_path;
      vdex_file.reset(nullptr);
    }
  }

  // [4] 加载内存中的 dex。若存在 vdex 且校验成功,可跳过结构校验
  std::vector<std::unique_ptr<const DexFile>> dex_files;
  for (size_t i = 0; i < dex_mem_maps.size(); ++i) {
    static constexpr bool kVerifyChecksum = true;
    const ArtDexFileLoader dex_file_loader;
    std::unique_ptr<const DexFile> dex_file(dex_file_loader.Open(
        DexFileLoader::GetMultiDexLocation(i, dex_location.c_str()),
        location_checksum,
        std::move(dex_mem_maps[i]),
        /*verify=*/(vdex_file == nullptr) && Runtime::Current()->IsVerificationEnabled(),
        kVerifyChecksum,
        &error_msg));
    if (dex_file != nullptr) {
      dex::tracking::RegisterDexFile(dex_file.get());  // 注册用于调试追踪
      dex_files.push_back(std::move(dex_file));
    } else {
      error_msgs->push_back("Failed to open dex files from memory: " + error_msg);
    }
  }

  // [5] 若 vdex 不存在、加载失败,或 class_loader 为空,直接返回 dex_files
  if (vdex_file == nullptr || class_loader == nullptr || !error_msgs->empty()) {
    return dex_files;
  }

  // [6] 创建 ClassLoaderContext,确保之后的 oat 加载上下文一致
  std::unique_ptr<ClassLoaderContext> context = ClassLoaderContext::CreateContextForClassLoader(
      class_loader, dex_elements);
  if (context == nullptr) {
    LOG(ERROR) << "Could not create class loader context for " << vdex_path;
    return dex_files;
  }
  DCHECK(context->OpenDexFiles(kRuntimeISA, ""))
      << "Context created from already opened dex files should not attempt to open again";

  // [7] 检查 boot class path checksum 和 class loader context 是否匹配
  if (!vdex_file->MatchesBootClassPathChecksums() ||
      !vdex_file->MatchesClassLoaderContext(*context.get())) {
    return dex_files;
  }

  // [8] 从 vdex 创建 OatFile 实例并注册
  std::unique_ptr<OatFile> oat_file(OatFile::OpenFromVdex(
      MakeNonOwningPointerVector(dex_files),
      std::move(vdex_file),
      dex_location));
  DCHECK(oat_file != nullptr);
  VLOG(class_linker) << "Registering " << oat_file->GetLocation();
  *out_oat_file = RegisterOatFile(std::move(oat_file));

  return dex_files;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值