BlackDex大法,是如何运作的?

发布时间: 2023-06-09

0x1 前言

在现阶段Android环境下,开发者为了保护自身APP不被反编译以及逻辑的解读、应用重打包,许多开发者都会选择对APP进行加固,APP经过加固,一定程度上保护了APP,使APP没那么容易被反编译。当然,有加固技术的出现,也会有反加固技术的出现,即本文要分析的一种反加固的原理。

0x2 BlackDex

BlackDex是一个运行在Android手机上的脱壳工具,支持5.0~12,无需依赖任何环境任何手机都可以使用,包括模拟器。只需几秒,即可对已安装包括未安装的APK进行脱壳。

项目开源地址:https://github.com/CodingGay/BlackDex

本项目现仅供参考,因开源的原因,已经被许多加固厂商Anti,点到为止,现已停止维护。

0x3 前置知识

本项目涉及知识点,按照重要程度排序

  • Android 虚拟化技术
  • Android Dex加载原理
  • Android DexFile结构
  • Android Hook

总结

BlackDex核心原理是利用虚拟化技术,使APP进程运行,让加固壳进行自解密,最后完成DexFile的Dump,得益于依赖于虚拟化技术,不需要任何手机拥有任何逆向环境,甚至连APP都不用安装到手机上就可以进行脱壳。

本系列将逐步剖析BlackDex是如何进行运作。

BlackDex大法,是如何运作的?:启动篇(二)

0x1 前言

前篇 说到,BlackDex是基于虚拟化技术进行的,本项目实际上是基于 BlackBox 的基础下进行开发,很可惜的是由于某些原因此项目没能继续下去。

本系列将选定发表时最后的提交,朋友们可以Clone下来跟着本文走:https://github.com/CodingGay/BlackDex/tree/5580fa8f5d658afae4eb667f8c8d6632be5b9aaf

0x2 启动

本文主要分析如何从点击BlackDex图标之后,是如何进行启动APP的进程,此处不会过度深究虚拟化的实现过程,有机会更完这个系列之后会再出一系列虚拟化技术的文章。

核心启动dump方法:top.niunaijun.blackbox.BlackDexCore#dumpDex(java.lang.String)

Java
public InstallResult dumpDex(String packageName) {
        // 将软件安装至BlackBox中
        InstallResult installResult = BlackBoxCore.get().installPackage(packageName);
        if (installResult.success) {
            // 安装成功则启动,否则卸载并且返回失败。
            boolean b = BlackBoxCore.get().launchApk(packageName);
            if (!b) {
                BlackBoxCore.get().uninstallPackage(installResult.packageName);
                return null;
            }
            return installResult;
        } else {
            return null;
        }
    }

两步事情:

  1. 将软件安装至BlackBox
  2. 如果安装成功则启动,不成功则返回失败

内部实现原理涉及虚拟化相关知识,此处不多深究,应用启动时,程序感知到的第一个方法就是Application#attachBaseContext,在虚拟环境内也是一样的,在经过一系列调度后。我们直接跟进到虚拟化进程的启动的方法:top.niunaijun.blackbox.app.BActivityThread#handleBindApplication

此函数比较长,代码内注释逐步解析

Java
    private synchronized void handleBindApplication(String packageName, String processName) {
        // 初始化Dump的返回的信息
        DumpResult result = new DumpResult();
        result.packageName = packageName;
        result.dir = new File(BlackBoxCore.get().getDexDumpDir(), packageName).getAbsolutePath();
        try {
            // 以下是获取需要多开应用的信息,然后对当前进程进行重新设置,因为本进程信息是宿主的。
            PackageInfo packageInfo = BlackBoxCore.getBPackageManager().getPackageInfo(packageName, PackageManager.GET_PROVIDERS, BActivityThread.getUserId());
            if (packageInfo == null)
                return;
            .........
            // 以上省略部分虚拟化的代码。

   
            // 此处清除dump目录,防止多次脱壳的dex文件乱窜
            // clear dump file
            FileUtils.deleteDir(new File(BlackBoxCore.get().getDexDumpDir(), packageName));

            // 初始化native层代码
            VMCore.init(Build.VERSION.SDK_INT);
            // 启用IO重定向,支持虚拟应用运行环境
            IOCore.get().enableRedirect(packageContext);

            ......
            // 以上省略部分虚拟化代码

            // 反射LoadedApk获取多开应用的classloader,并且反射LoadedApk#makeApplication函数,makeApplication中会初始化Application,调用其attachBaseContext、onCreate函数,完成Application的初始化。
            try {
                ClassLoader call = LoadedApk.getClassloader.call(loadedApk);
                application = LoadedApk.makeApplication.call(loadedApk, false, null);
            } catch (Throwable e) {
                Slog.e(TAG, "Unable to makeApplication");
                e.printStackTrace();
            }
            
            // 如果走到此处没有发生异常,说明Application已经完成启动,一般在这种时候从理论上来说,应用已经运行起来了,那么自然加固也已经解密完成,我们接下来进行核心的dex的dump工作。
            if (Objects.equals(packageName, processName)) {
                ClassLoader loader;
                // 此处获取需要脱壳的app的classloader
                if (application == null) {
                    loader = LoadedApk.getClassloader.call(loadedApk);
                } else {
                    // 实际上走到这里,理论是启动失败了。不过还可以挣扎一下。
                    loader = application.getClassLoader();
                }
                // 调用核心DumpDex方法,进行dex的dump工作
                handleDumpDex(packageName, result, loader);
            }
        } catch (Throwable e) {
            // 如果发生异常,通知UI并且从BlackBox中卸载该应用
            e.printStackTrace();
            mAppConfig = null;
            BlackBoxCore.getBDumpManager().noticeMonitor(result.dumpError(e.getMessage()));
            BlackBoxCore.get().uninstallPackage(packageName);
        }
    }

其实从上述代码分析下来,整体的流程已经非常的顺畅了,汇总整理一下

  1. 使用虚拟化技术将应用运行起来,初始化Application。
  2. 一般来说,基本上99%的加固整体工作在Application已经完成,所以此时应用真实的Dex文件已经被解密释放并且加载到内存中。
  3. 在启动成功后,调用handleDumpDex核心方法进行dex的dump工作,即可达到脱壳的目的。

本系列将慢慢梳理每一个流程,本篇讲了BlackDex是如何启动且执行脱壳的,剩下的在后续的文章內继续深扒。

BlackDex大法,是如何运作的?:Dump篇(三)

书接上回

想要了解如何dump出Dex,就要先了解Dex文件是如何加载进内存的。上文说到Application是通过LoadedApk#makeApplication完成的,那么我们看下相关实现。

本文源码为:android11_r1

系统源码:android.app.LoadedApk#makeApplication

Java
    @UnsupportedAppUsage
    public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
        if (mApplication != null) {
            return mApplication;
        }

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication");

        Application app = null;

        String appClass = mApplicationInfo.className;
        if (forceDefaultAppClass || (appClass == null)) {
            appClass = "android.app.Application";
        }

        try {
            // 获取App的classloader
            final java.lang.ClassLoader cl = getClassLoader();
            if (!mPackageName.equals("android")) {
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                        "initializeJavaContextClassLoader");
                initializeJavaContextClassLoader();
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            }
            .........省略

            // 内部通过classloader.findClass(appClass),并且进行实例化Application。
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
        } catch (Exception e) {
            if (!mActivityThread.mInstrumentation.onException(app, e)) {
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                throw new RuntimeException(
                    "Unable to instantiate application " + appClass
                    + ": " + e.toString(), e);
            }
        }

        .........省略
        return app;
    }

getClassLoader内部流程过于复杂,感兴趣的自己去跟一下,简单点概括:根据ApplicationInfo中的信息,最终生成并返回PathClassloader

通过Classloader可以findClass我们Dex中的方法,由此可以看出,Classloader跟我们Dex是存在某种关系的。我们看一下PathClassloader是如何对Dex文件进行加载的。

源码:dalvik.system.PathClassLoader

Java
public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }

    public PathClassLoader(
            String dexPath, String librarySearchPath, ClassLoader parent,
            ClassLoader[] sharedLibraryLoaders) {
        super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
    }
}

可以看到都是调用父类BaseClassloader的构造方法,我们主要关注参数dexPath参数,看他是如何打开我们的Dex文件。

源码:dalvik.system.BaseDexClassLoader

Java
    public BaseDexClassLoader(String dexPath,
            String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
            boolean isTrusted) {
        super(parent);
        // Setup shared libraries before creating the path list. ART relies on the class loader
        // hierarchy being finalized before loading dex files.
        this.sharedLibraryLoaders = sharedLibraryLoaders == null
                ? null
                : Arrays.copyOf(sharedLibraryLoaders, sharedLibraryLoaders.length);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

        reportClassLoaderChain();
    }

继续跟进DexPathList,源码:dalvik.system.DexPathList

Java
DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }

        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }

        ........省略
        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        ........省略
    }

splitDexPath主要做的是检查dexPath是否合规,以及加载DEX,ZIP,JAR的不同处理方法,最后返回一个List,里面装载着Dex文件File对象,继续跟进makeDexElements

Java
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;

      for (File file : files) {
          if (file.isDirectory()) {
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              DexFile dex = null;

              // 判断是否.dex结尾
              if (name.endsWith(DEX_SUFFIX)) {
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      ........省略
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

以上可以看出,dexFile最终通过loadDexFile函数打开,返回dalvik.system.DexFile对象,最终通过dalvik.system.DexFile作为参数,new Element,然后返回Element[]。

继续跟进

Java
    private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                       Element[] elements)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
        }
    }

源码:dalvik.system.DexFile

Java
    DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements)
            throws IOException {
        mCookie = openDexFile(fileName, null, 0, loader, elements);
        mInternalCookie = mCookie;
        mFileName = fileName;
        //System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
    }

此处可以看出,通过openDexFile,最终返回mCookie,在Android 5.0的时候类型是long,6.0开始为Object,其实际内容是long[],此处了解即可。

继续跟进

Java
    private static native Object openDexFileNative(String sourceName, String outputName, int flags,
            ClassLoader loader, DexPathList.Element[] elements);

最终调用native中的openDexFileNative方法。

下面进入native的源码
源码:art/runtime/native/dalvik_system_DexFile.cc

C
static jobject DexFile_openDexFileNative(JNIEnv* env,
                                         jclass,
                                         jstring javaSourceName,
                                         jstring javaOutputName ATTRIBUTE_UNUSED,
                                         jint flags ATTRIBUTE_UNUSED,
                                         jobject class_loader,
                                         jobjectArray dex_elements) {
  ScopedUtfChars sourceName(env, javaSourceName);
  if (sourceName.c_str() == nullptr) {
    return nullptr;
  }

  std::vector<std::string> error_msgs;
  const OatFile* oat_file = nullptr;
  std::vector<std::unique_ptr<const DexFile>> dex_files =
      Runtime::Current()->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),
                                                                  class_loader,
                                                                  dex_elements,
                                                                  /*out*/ &oat_file,
                                                                  /*out*/ &error_msgs);
  return CreateCookieFromOatFileManagerResult(env, dex_files, oat_file, error_msgs);
}

最终是通过Runtime::Current()->GetOatFileManager().OpenDexFilesFromOat,打开了Dex,返回了Native中的DexFile,通过CreateCookieFromOatFileManagerResult返回到Java,也就是mCookie。
如果想进一步了解,可以看下其他博文:ClassLoader--02基于Android5.0的openDexFileNative

继续跟进

C
static jobject CreateCookieFromOatFileManagerResult(
    JNIEnv* env,
    std::vector<std::unique_ptr<const DexFile>>& dex_files,
    const OatFile* oat_file,
    const std::vector<std::string>& error_msgs) {
  .......省略
  jlongArray array = ConvertDexFilesToJavaArray(env, oat_file, dex_files);
  if (array == nullptr) {
    ScopedObjectAccess soa(env);
    for (auto& dex_file : dex_files) {
      if (linker->IsDexFileRegistered(soa.Self(), *dex_file)) {
        dex_file.release();  // NOLINT
      }
    }
  }
  return array;
}

继续跟进

C
static jlongArray ConvertDexFilesToJavaArray(JNIEnv* env,
                                             const OatFile* oat_file,
                                             std::vector<std::unique_ptr<const DexFile>>& vec) {
  // Add one for the oat file.
  jlongArray long_array = env->NewLongArray(static_cast<jsize>(kDexFileIndexStart + vec.size()));
  if (env->ExceptionCheck() == JNI_TRUE) {
    return nullptr;
  }

  jboolean is_long_data_copied;
  jlong* long_data = env->GetLongArrayElements(long_array, &is_long_data_copied);
  if (env->ExceptionCheck() == JNI_TRUE) {
    return nullptr;
  }
  
  // 此处为核心转换
  long_data[kOatFileIndex] = reinterpret_cast64<jlong>(oat_file);
  for (size_t i = 0; i < vec.size(); ++i) {
    // 将每个DexFile对象地址强转为long型,放入long_data中
    long_data[kDexFileIndexStart + i] = reinterpret_cast64<jlong>(vec[i].get());
  }

  env->ReleaseLongArrayElements(long_array, long_data, 0);
  if (env->ExceptionCheck() == JNI_TRUE) {
    return nullptr;
  }

  // Now release all the unique_ptrs.
  for (auto& dex_file : vec) {
    dex_file.release();  // NOLINT
  }
  
  // 最终返回
  return long_array;
}

以上可以看出来,art将dexfiles遍历,将dexfile强转为long型地址,最终返回到Java中。

所以可以这样认为,mCookie装载的,是art中的DexFile的对象的内存地址。

所以这跟脱壳有什么关系呢?让我们来看一下DexFile中的构造函数
源码:art/libdexfile/dex/dex_file.cc

C
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();
}

源码:art/libdexfile/dex/dex_file.h

C
  ........省略
  // The base address of the memory mapping.
  const uint8_t* const begin_;

  // The size of the underlying memory allocation in bytes.
  const size_t size_;

  // The base address of the data section (same as Begin() for standard dex).
  const uint8_t* const data_begin_;

  // The size of the data section.
  const size_t data_size_;

  // Typically the dex file name when available, alternatively some identifying string.
  //
  // The ClassLinker will use this to match DexFiles the boot class
  // path to DexCache::GetLocation when loading from an image.
  const std::string location_;

  const uint32_t location_checksum_;

  // Points to the header section.
  const Header* const header_;

  // Points to the base of the string identifier list.
  const dex::StringId* const string_ids_;

  // Points to the base of the type identifier list.
  const dex::TypeId* const type_ids_;

  // Points to the base of the field identifier list.
  const dex::FieldId* const field_ids_;

  // Points to the base of the method identifier list.
  const dex::MethodId* const method_ids_;

  // Points to the base of the prototype identifier list.
  const dex::ProtoId* const proto_ids_;

  // Points to the base of the class definition list.
  const dex::ClassDef* const class_defs_;

  // Points to the base of the method handles list.
  const dex::MethodHandleItem* method_handles_;

  // Number of elements in the method handles list.
  size_t num_method_handles_;

  // Points to the base of the call sites id list.
  const dex::CallSiteIdItem* call_site_ids_;

  // Number of elements in the call sites list.
  size_t num_call_site_ids_;
  ........省略

通过源码中的注释我们可以得知,Dex文件在内存中的映射地址是位于:begin_ ,Dex文件的大小:size_,后来实践中也证实了确实是这么回事。

知道DexFile对象与Dex文件的关系,这样一来就好办了,我们只要拿到所有的mCookie,就等于获取到了所有的DexFile,我们可以将其进行Dump,完成我们的脱壳。

0x2 流程简单梳理

  1. PathClassloader -> BaseClassloader -> DexPathList
  2. DexPathList里面通过 makeDexElements 获得Element[]
  3. Element[]是通过 loadDexFile返回的dalvik.system.DexFile对象创建的
  4. dalvik.system.DexFile最终通过openDexFileNative返回mCookie

反过来,即可得知获取mCookie的线路

  1. 先获取PathClassloader中的DexPathList对象
  2. 再获取DexPathList中的dexElements,也就是Element[]
  3. 再获取每个Element中的dexFile对象
  4. 再获取dexFile中的mCookie值

通过以上路线,即可获取到该Classloader中的所有mCookie

0x3 逻辑梳理

  1. 加固应用需要在最早的流程中进行解密并且加载Dex文件,否则应用将无法运行。
  2. 由于应用的Class被加固隐藏、保护起来了,如果不解密加载,应用会出现ClassNotFoundException。
  3. 既然我们应用能运行后,代表findClass可以找到该应用所需的Class,也同时代表了Classloader已经加载进内存了。
  4. 我们既然知道了Classloader与DexFile的关系,那么我们就可以根据Classloader,使用以上梳理的点获取该Classloader中的所有mCookie。

大体逻辑就如上面所说,我们再回头看看BlackDex时如何处理的。

0x4 BlackDex

书接上回

Java
    private void handleDumpDex(String packageName, DumpResult result, ClassLoader classLoader) {
        new Thread(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException ignored) {
            }
            try {
                VMCore.cookieDumpDex(classLoader, packageName);
            } finally {
                mAppConfig = null;
                File dir = new File(result.dir);
                if (!dir.exists() || dir.listFiles().length == 0) {
                    BlackBoxCore.getBDumpManager().noticeMonitor(result.dumpError("not found dex file"));
                } else {
                    BlackBoxCore.getBDumpManager().noticeMonitor(result.dumpSuccess());
                }
                BlackBoxCore.get().uninstallPackage(packageName);
            }
        }).start();
    }

此处主要的工作是call

Java
VMCore.cookieDumpDex(classLoader, packageName);

如果发生异常,则通知UI脱壳失败

继续跟进,一个大方法,我们逐步分析

Java
public static void cookieDumpDex(ClassLoader classLoader, String packageName) {
        // 核心:根据Classloader获取当前Classloader中所有的Cookie
        List<Long> cookies = DexFileCompat.getCookies(classLoader);
        File file = new File(BlackBoxCore.get().getDexDumpDir(), packageName);

        DumpResult result = new DumpResult();
        result.dir = file.getAbsolutePath();
        result.packageName = packageName;
        // 此处跟开启多线程进行脱壳
        int availableProcessors = Runtime.getRuntime().availableProcessors();
        ExecutorService executorService = Executors.newFixedThreadPool(availableProcessors <= 0 ? 1 : availableProcessors);
        CountDownLatch countDownLatch = new CountDownLatch(cookies.size());
        AtomicInteger atomicInteger = new AtomicInteger(0);

        BlackBoxCore.getBDumpManager().noticeMonitor(result.dumpProcess(cookies.size(), atomicInteger.getAndIncrement()));
        // 此处遍历每一个cookie
        for (int i = 0; i < cookies.size(); i++) {
            long cookie = cookies.get(i);

            ........省略通知UI操作

            executorService.execute(() -> {

                // 调用native方法进行真正脱壳
                cookieDumpDex(cookie, file.getAbsolutePath(), BlackBoxCore.get().isFixCodeItem());

                ........省略通知UI操作
            });
        }
        File[] files = file.listFiles();
        if (files != null) {
            for (File dex : files) {
                if (dex.isFile() && dex.getAbsolutePath().endsWith(".dex")) {
                    // 如果脱壳成功,修复Dex的signature与checksum
                    DexUtils.fixDex(dex);
                }
            }
        }
    }

使用方法

Java
List<Long> cookies = DexFileCompat.getCookies(classLoader);

获取Classloader所有的Cookie,原理我们上面也讲过,感兴趣的可以自行查看代码,此处不再细说。

以下是最核心的脱壳操作,我们逐步分析

C
void DexDump::cookieDumpDex(JNIEnv *env, jlong cookie, jstring dir, jboolean fix) {
    // 如果环境没有初始化则去初始化,初始化内容我们后续分析
    if (beginOffset == -2) {
        init(env);
    }
    // 如果初始化失败,我们则无法脱壳
    if (beginOffset == -1) {
        ALOGD("dumpDex not support!");
        return;
    }

    // 以上初始化核心工作为:获取DexFile->begin_的偏移量,知道了begin_我们才知道Dex在内存中的什么位置,才可以将它Dump出来,否则将无法脱壳。

    // Dex magic,某数字加固为了防止内存搜索脱壳,会将Dex magic抹除变成00 00 00 00 00 00 00,使得内存扫描无法找到Dex文件从而无法脱壳,BlackDex脱壳后会将此修复。
    char magic[8] = {0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00};
    // 将cookie转换成内存地址
    auto base = reinterpret_cast<char *>(cookie);
    // 根据初始化beginOffset,通过base + 偏移,得知begin_所在的位置
    auto begin = *(size_t *) (base + beginOffset * sizeof(size_t));
    // 此处经常会出现bad point,所以检查一下是否野指针
    if (!PointerCheck::check(reinterpret_cast<void *>(begin))) {
        return;
    }
    auto dirC = env->GetStringUTFChars(dir, 0);

    // 此处根据Dex + 0x20获取Dex文件的总大小,方便我们对Dex进行Dump
    auto dexSizeOffset = ((unsigned long) begin) + 0x20;
    int size = *(size_t *) dexSizeOffset;

    void *buffer = malloc(size);
    if (buffer) {
        // 将内存中的Dex复制出来
        memcpy(buffer, reinterpret_cast<const void *>(begin), size);
        // 修复 magic
        memcpy(buffer, magic, sizeof(magic));

        // 以下是检验Dex是否完整,符合格式
        const bool kVerifyChecksum = false;
        const bool kVerify = true;
        const art_lkchan::DexFileLoader dex_file_loader;
        std::string error_msg;
        std::vector<std::unique_ptr<const art_lkchan::DexFile>> dex_files;
        if (!dex_file_loader.OpenAll(reinterpret_cast<const uint8_t *>(buffer),
                                     size,
                                     "",
                                     kVerify,
                                     kVerifyChecksum,
                                     &error_msg,
                                     &dex_files)) {
            // Display returned error message to user. Note that this error behavior
            // differs from the error messages shown by the original Dalvik dexdump.
            ALOGE("Open dex error %s", error_msg.c_str());
            return;
        }

        // 是否需要深度修复,此处下文再讲。
        if (fix) {
            fixCodeItem(env, dex_files[0].get(), begin);
        }
        // 最终我们将Dex写出到指定的目录
        char path[1024];
        sprintf(path, "%s/cookie_%d.dex", dirC, size);
        auto fd = open(path, O_CREAT | O_WRONLY, 0600);
        ssize_t w = write(fd, buffer, size);
        fsync(fd);
        if (w > 0) {
            ALOGE("cookie dump dex ======> %s", path);
        } else {
            remove(path);
        }
        close(fd);
        free(buffer);
        env->ReleaseStringUTFChars(dir, dirC);
    }
}

我们分析一下init方法,看看BlackDex是如何获取begin_偏移量的

C
void init(JNIEnv *env) {
    // 此处使用PLT HOOK处理掉kill、killpg方法,避免在脱壳过程中应用kill掉自己。
    const char *soName = ".*\\.so$";
    xhook_register(soName, "kill", (void *) new_kill,
                   (void **) (&orig_kill));
    xhook_register(soName, "killpg", (void *) new_killpg,
                   (void **) (&orig_killpg));
    xhook_refresh(0);

    // 此处出现了一个loadEmptyDex方法
    jlongArray emptyCookie = VmCore::loadEmptyDex(env);

    .......省略
}

我们脱壳的第一步就是想要获取begin_的偏移量,但是偏偏这个begin_在DexFile中不一定是固定的位置,如果我们根据AOSP的代码,根据每个Android版本区分写死也没有问题,但是如果说某个手机厂商中间加了一个字段,减了一个字段。那么这里必定出现脱壳失败,此处我使用了一个 预测法 ,这种方法在许多Hook框架上也是常见的,比如需要预测ArtMethod的flags偏移的。

下面将说一下是怎么进行预测,首先我们观察每个Android版本的,可以发现。

C
  // The base address of the memory mapping.
  const uint8_t* const begin_;

  // The size of the underlying memory allocation in bytes.
  const size_t size_;

begin_与size_都是保持相对位置的,我们可以先这样决定,我们就认为在实际情况中,他们两个的值就是保持相对位置。
既然这样的话,我们不知道begin_,那么size_,也就是Dex文件的大小,我们是肯定能知道的,我们根据这个大小,来获取size_,然后通过 begin_与size_都是保持相对位置 这个特性,我们减去一个uint8_t的大小,那就能获取到begin_的偏移量。

继续看代码是如何操作的

C
void init(JNIEnv *env) {
    // 此处loadEmptyDex方法,就是加载一个我们指定的Dex文件,这个Dex文件是BlackDex里面准备好的,我已经知道了这个Dex的大小,此处我们加载这个Dex并且获取他的Cookie
    jlongArray emptyCookie = VmCore::loadEmptyDex(env);
    jsize arrSize = env->GetArrayLength(emptyCookie);
    if (env->ExceptionCheck() == JNI_TRUE) {
        return;
    }
    jlong *long_data = env->GetLongArrayElements(emptyCookie, nullptr);

    // 此处遍历cookie
    for (int i = 0; i < arrSize; ++i) {
        jlong cookie = long_data[i];
        if (cookie == 0) {
            continue;
        }
        // 此处我们从cookie的内存地址,也就是DexFile的内存地址,开始往下搜索。
        // 最多搜索10个size_t的大小
        auto dex = reinterpret_cast<char *>(cookie);
        for (int ii = 0; ii < 10; ++ii) {
            auto value = *(size_t *) (dex + ii * sizeof(size_t));
            // 如果此时,搜索到值等于1872,我们的Dex文件大小也是1872,那么可以确定这个内存地址为size_
            if (value == 1872) {
                // 那么我们将size_的偏移 - 1,就取得了begin_的所在内存地址。
                beginOffset = ii - 1;
                env->ReleaseLongArrayElements(emptyCookie, long_data, 0);
                return;
            }
        }
    }
    env->ReleaseLongArrayElements(emptyCookie, long_data, 0);
    beginOffset = -1;
}

这样,我们就可以获取到begin_与size_的偏移量,就可以配合cookieDumpDex进行Dex的Dump操作。至此,BlackDex的脱壳工作就此结束。但是想要真正了解BlackDex的还远远不够,更核心提供脱壳能力的还是虚拟化技术。

BlackDex还有一项加强功能为深度修复,此模式下可以对抗一些指令抽取壳,详情可见Github,有空之后将继续分析是如何对抗指令抽取壳并且实现指令还原。

以上技术点和对于系统源代码分析纯属个人见解,如有不对的地方请指出,感谢。

请在下方留下您的评论.加入TG吹水群