1.简述

总结记录下App启动流程中Dex动态加载流程以及相关知识,加深对App加壳以及脱壳原理的理解。
环境:Android 8.0.0

2.相关知识总结

1. App启动流程

Zygote fork之后会在新进程中调用main()方法,然后 main() 方法会创建 ActivityThread 实例,并调用其 main() 方法,从而启动应用程序的主线程,这里也就是App相关的步骤,所以直接从ActiveThread开始说起。
ActivityThread 是 Android 系统中的一个核心类,负责管理应用程序的主线程以及应用程序中的各种组件(比如 Activity、Service、BroadcastReceiver 等)的生命周期、消息循环、消息处理等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
        SamplingProfilerIntegration.start();
 
        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);
 
        Environment.initForCurrentUser();
 
        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());
 
        // Make sure TrustedCertificateStore looks in the right place for CA certificates
        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
        TrustedCertificateStore.setDefaultUserDirectory(configDir);
 
        Process.setArgV0("<pre-initialized>");
 
        Looper.prepareMainLooper();
 
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
 
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
 
        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }
 
        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();
 
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

源码中这两句是比较关键的代码,第一句实例化ActivityThread类,之后调用attach方法,进行应用的初始化工作。

1
2
ActivityThread thread = new ActivityThread();
       thread.attach(false);

一些初始化工作可能依赖于系统的消息或事件的处理结果。例如,创建 Application 实例可能需要获取系统的一些状态或资源,所以在attach方法中初始化工作做好之后会进入消息循环等待,当系统发来消息就开始时调用handlebindApplication开始初始化。
这里分辨一下ActityThread.attach和handlebindApplication初始化的区别ActivityThread.attach() 主要负责初始化应用程序的运行环境和关键对象,而不是直接加载应用程序的组件(如 Activity)。handleBindApplication() 方法则更侧重于实际绑定应用程序,初始化应用程序的 Application、Activity、Service 等组件,并启动应用程序的主 Activity,所以说到了handlebindApplication这一步就App内的代码开始起作用了。
对于了解加壳脱壳来说handlebindApplication方法中的这两行代码比较关键,第一行最终会通过LoadApk类中的newApplication创建Application对象并调用attachBaseContext方法,第二行调用Application的OnCreate方法,这俩个方法也是App代码中最先运行的两个方法。

1
2
Application App = data.info.makeApplication(data.restrictedBackupMode, null);
 mInstrumentation.callApplicationOnCreate(App);

到这里App开始调用第一个Activity的OnCreate方法。

2. 双亲委派和类加载器

先介绍一下安卓中的四个类加载器

  1. PathClassLoader:
    PathClassLoader 是 Android 应用程序的默认类加载器,用于加载应用程序 APK 文件中的类和资源。
    它负责加载应用程序的 Dex 文件(即 APK 文件中的 classes.Dex)中的类。
  2. DexClassLoader:
    DexClassLoader 用于从外部 Dex 文件加载类和资源。
    它可以加载存储在文件系统上的 Dex 文件,并从中加载类。
  3. InMemoryDexClassLoader:
    InMemoryDexClassLoader 是安卓8.0之后添加的一个类加载器,用于从内存中加载 Dex 字节码数据,并生成对应的 Class 对象。
  4. BootClassLoader:
    BootClassLoader 是 Android 系统的引导类加载器,也是类加载器层次结构的根加载器,负责加载 Android 系统的核心类库。
    它负责加载 Android 系统的核心类,如 java.lang 包中的类等。

再说双亲委派,双亲委派是当一个类加载器收到加载类的请求时使用的机制,这里借用一张图说明。
https://cloud.tencent.com/developer/article/2251252
图片描述
总结就是两步,第一步当类加载器加载一个类时如果没有找到就会向上询问父类加载器是否加载过没有就继续向上直到BootClassLoader,这里到了第二步,如果BootClassLoader也没有加载过,BootClassLoader自己如果可以加载,则BootClassLoader会直接加载,如果不可以会一路向下执行这样的判断,直到最开始的子加载器。
这里要注意加载器的父子关心并不是类的继承父子关系。

3. 整体加壳实现方式

整体加壳的实现,原Dex会被加密保存在某个地方,那一定有一个原Dex加解密的过程,那这个过程一定是在App自身逻辑代码运行之前,所以根据对App启动流程的分析原Dex的解密时机在Application的attachBaseContext方法,OnCreate方法调用的时候,壳代码一般会重写这两个方法,这里可以看一个例子,壳代码继承了Application并重写了两个方法。
图片描述
图片描述
除了加原Dex解密之外,还有一个重要的步骤就是加载解密后的Dex替换壳Dex。这一步就涉及Dex的加载以及双亲委派机制了。
一个App正常启动后加载Dex文件使用的是默认类加载器PathClassLoader,这个加载其不能用于加载外部Dex,通过之前的介绍,壳通常用的是DexClassLoader和InMemoryDexClassLoader来进行解密后的Dex加载,假设我们的代码是这样的。

1
2
3
4
5
6
7
8
dexClassLoader = new DexClassLoader("/sdcard/4.dex", context.getApplicationContext().getCacheDir().getAbsolutePath(), null, pathClassloader);
      try {
          Class TestActivityClass = dexClassLoader.loadClass("com.kanxue.test02.TestActivity");
          Log.e("class", TestActivityClass.toString());
          context.startActivity(new Intent(context, TestActivityClass));
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }

这样运行后会报错,ClassNotFound,这里的话引出另一个问题这两个类加载的Dex,仅仅只是加载了一个普通的类进来,并没有生命周期,这样的话运行起来就会报错ClassNotFound,对于这个为什么没有生命周期这个问题,我找了一些startActivity调用流程的文章最终发现了可能的原因,以下这段话来自ChatGPT

1
当你调用 startActivity() 启动一个 Activity 时,实际上是向系统发送了一个启动 Activity 的请求,系统会通过 ActivityThread 来处理这个请求。在 ActivityThread 中,当收到启动 Activity 的请求时,会调用 handleLaunchActivity() 方法来处理,而在 handleLaunchActivity() 方法中会最终调用 performLaunchActivity() 方法来执行 Activity 的启动流程

也就是说startActivity方法调用后会执行performLaunchActivity方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/frameworks/base/core/java/android/app/ActivityThread.java:
 performLaunchActivity()部分代码
 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
  try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
  }
  ...
  }

这段代码主要调用newActivity创建一个Activity实例,当Activity 实例对象被添加到 Activity 栈中,并由系统管理时,生命周期就会正常触发。

1
2
3
4
5
6
public Activity newActivity(ClassLoader cl, String className,
          Intent intent)
          throws InstantiationException, IllegalAccessException,
          ClassNotFoundException {
     return (Activity)cl.loadClass(className).newInstance();
  }

再看一下newActivity的代码,使用了传入的ClassLoader来加载类
这里调用getClassLoader获取系统默认的PathClassLoader,所以传入的是PathClassLoader,PathClassLoader是DexClassLoader和InMemoryDexClassLoader的父加载器,所以按双亲委派的流程是会报错的。
这里解决方法有很多种,以为例DexClassLoader这里列三种(1)替换PathClassLoader(2)改变父子加载器关系把DexClassLoader改为PathClassLoader的父加载器。(3)合并PathClassLoader和DexClassLoader中的dexElements数组。

3.两种动态加载方式源码分析

接下来是DexClassLoader和InMemoryDexClassLoader两种动态加载方式的源码分析这里就和脱壳有关了。

1.DexClassLoader

DexClassLoader

1
2
3
4
public DexClassLoader(String dexPath, String optimizedDirectory,
           String librarySearchPath, ClassLoader parent) {
       super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
   }

BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent)

1
2
3
4
5
6
7
8
9
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
          String librarySearchPath, ClassLoader parent) {
      super(parent);
     this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
 
      if (reporter != null) {
          reporter.report(this.pathList.getDexPaths());
      }
  }

public DexPathList(ClassLoader definingContext, String dexPath,String librarySearchPath, File optimizedDirectory)

1
2
3
4
5
6
public DexPathList(ClassLoader definingContext, String dexPath,
           String librarySearchPath, File optimizedDirectory) {
...
     this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,                                           suppressedExceptions, definingContext);
...
}

makeDexElements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
309            List<IOException> suppressedExceptions, ClassLoader loader) {
310      Element[] elements = new Element[files.size()];
311      int elementsPos = 0;
312      /*
313       * Open all files and load the (direct or contained) dex files up front.
314       */
315      for (File file : files) {
316          if (file.isDirectory()) {
317              // We support directories for looking up resources. Looking up resources in
318              // directories is useful for running libcore tests.
319              elements[elementsPos++] = new Element(file);
320          } else if (file.isFile()) {
321              String name = file.getName();
322
323              if (name.endsWith(DEX_SUFFIX)) {
324                  // Raw dex file (not inside a zip/jar).
325                  try {
326                      DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
...
}

loadDexFile(File file, File optimizedDirectory, ClassLoader loader,Element[] elements)

1
2
3
4
5
6
7
8
9
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);
       }
   }

DexFile loadDex(String sourcePathName, String outputPathName,int flags, ClassLoader loader, DexPathList.Element[] elements)

1
2
3
4
5
static DexFile loadDex(String sourcePathName, String outputPathName,
      int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
 
      return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
  }

DexFile(String sourceName, String outputName, int flags, ClassLoader loader,DexPathList.Element[] elements)

1
2
3
4
5
6
7
8
  private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
            DexPathList.Element[] elements) throws IOException {
...
        mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
        mInternalCookie = mCookie;
        mFileName = sourceName;
        //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
    }

openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements)

1
2
3
4
5
6
7
8
9
10
11
private static Object openDexFile(String sourceName, String outputName, int flags,
           ClassLoader loader, DexPathList.Element[] elements) throws IOException {
       // Use absolute paths to enable the use of relative paths when testing on host.
       return openDexFileNative(new File(sourceName).getAbsolutePath(),
                                (outputName == null)
                                    ? null
                                    : new File(outputName).getAbsolutePath(),
                                flags,
                                loader,
                                elements);
   }

这里进入ART中 openDexFileNative 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 0;
  }
 
  Runtime* const runtime = Runtime::Current();
  ClassLinker* linker = runtime->GetClassLinker();
  std::vector<std::unique_ptr<const DexFile>> dex_files;
  std::vector<std::string> error_msgs;
  const OatFile* oat_file = nullptr;
 
   dex_files = runtime->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),class_loader,dex_elements,/*out*/ &oat_file,/*out*/ &error_msgs);
  if (!dex_files.empty()) {
    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();
        }
      }
    }
    return array;
  } else {
    ScopedObjectAccess soa(env);
    CHECK(!error_msgs.empty());
    // The most important message is at the end. So set up nesting by going forward, which will
    // wrap the existing exception as a cause for the following one.
    auto it = error_msgs.begin();
    auto itEnd = error_msgs.end();
    for ( ; it != itEnd; ++it) {
      ThrowWrappedIOException("%s", it->c_str());
    }
 
    return nullptr;
  }
}

OatFileManager::OpenDexFilesFromOat,这里有一个Dex2Oat编译流程用于优化编译效率,但是由于oat优化会有.oat文件落地,通过oat2dex即可获得原Dex,所以大部分壳都会阻断Dex2Oat流程,这里就不跟进分析直接看dex_files.empty()之后的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat( const char* dex_location,
    jobject class_loader,
    jobjectArray dex_elements,
    const OatFile** out_oat_file,
    std::vector<std::string>* error_msgs) {
...
if (!oat_file_assistant.IsUpToDate()) {
    // Update the oat file on disk if we can, based on the --compiler-filter
    // option derived from the current runtime options.
    // This may fail, but that's okay. Best effort is all that matters here.
    switch (oat_file_assistant.MakeUpToDate(/*profile_changed*/false, /*out*/ &error_msg)) {
      case OatFileAssistant::kUpdateFailed:
        LOG(WARNING) << error_msg;
        break;
 
      case OatFileAssistant::kUpdateNotAttempted:
        // Avoid spamming the logs if we decided not to attempt making the oat
        // file up to date.
        VLOG(oat) << error_msg;
        break;
 
      case OatFileAssistant::kUpdateSucceeded:
        // Nothing to do.
        break;
    }
  }
...
 if (dex_files.empty()) {
    if (oat_file_assistant.HasOriginalDexFiles()) {
      if (Runtime::Current()->IsDexFileFallbackEnabled()) {
        static constexpr bool kVerifyChecksum = true;
        if (!DexFile::Open(
            dex_location, dex_location, kVerifyChecksum, /*out*/ &error_msg, &dex_files)) {
          LOG(WARNING) << error_msg;
          error_msgs->push_back("Failed to open dex files from " + std::string(dex_location)
                                + " because: " + error_msg);
        }
      } else {
        error_msgs->push_back("Fallback mode disabled, skipping dex files.");
      }
    } else {
      error_msgs->push_back("No original dex files found for dex location "
          + std::string(dex_location));
    }
  }
 
}

DexFile::Open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bool DexFile::Open(const char* filename,
                   const std::string& location,
                   bool verify_checksum,
                   std::string* error_msg,
                   std::vector<std::unique_ptr<const DexFile>>* dex_files) {
  ScopedTrace trace(std::string("Open dex file ") + std::string(location));
  DCHECK(dex_files != nullptr) << "DexFile::Open: out-param is nullptr";
  uint32_t magic;
  File fd = OpenAndReadMagic(filename, &magic, error_msg);
  if (fd.Fd() == -1) {
    DCHECK(!error_msg->empty());
    return false;
  }
  if (IsZipMagic(magic)) {
    return DexFile::OpenZip(fd.Release(), location, verify_checksum, error_msg, dex_files);
  }
  if (IsDexMagic(magic)) {
    std::unique_ptr<const DexFile> dex_file(DexFile::OpenFile(fd.Release(),
                                                              location,
                                                              /* verify */ true,
                                                              verify_checksum,
                                                              error_msg));
    if (dex_file.get() != nullptr) {
      dex_files->push_back(std::move(dex_file));
      return true;
    } else {
      return false;
    }
  }
  *error_msg = StringPrintf("Expected valid zip or dex file: '%s'", filename);
  return false;
}

DexFile::OpenFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
2std::unique_ptr<const DexFile> DexFile::OpenFile(int fd,
                                                 const std::string& location,
                                                 bool verify,
                                                 bool verify_checksum,
                                                 std::string* error_msg) {
  ScopedTrace trace(std::string("Open dex file ") + std::string(location));
  CHECK(!location.empty());
  std::unique_ptr<MemMap> map;
  {
    File delayed_close(fd, /* check_usage */ false);
    struct stat sbuf;
    memset(&sbuf, 0, sizeof(sbuf));
    if (fstat(fd, &sbuf) == -1) {
      *error_msg = StringPrintf("DexFile: fstat '%s' failed: %s", location.c_str(),
                                strerror(errno));
      return nullptr;
    }
    if (S_ISDIR(sbuf.st_mode)) {
      *error_msg = StringPrintf("Attempt to mmap directory '%s'", location.c_str());
      return nullptr;
    }
    size_t length = sbuf.st_size;
    map.reset(MemMap::MapFile(length,
                              PROT_READ,
                              MAP_PRIVATE,
                              fd,
                              0,
                              /*low_4gb*/false,
                              location.c_str(),
                              error_msg));
    if (map == nullptr) {
      DCHECK(!error_msg->empty());
      return nullptr;
    }
  }
 
  if (map->Size() < sizeof(DexFile::Header)) {
    *error_msg = StringPrintf(
        "DexFile: failed to open dex file '%s' that is too short to have a header",
        location.c_str());
    return nullptr;
  }
 
  const Header* dex_header = reinterpret_cast<const Header*>(map->Begin());
 
  std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),
                                                 map->Size(),
                                                 location,
                                                 dex_header->checksum_,
                                                 kNoOatDexFile,
                                                 verify,
                                                 verify_checksum,
                                                 error_msg);
  if (dex_file != nullptr) {
    dex_file->mem_map_.reset(map.release());
  }
 
  return dex_file;
}

DexFile::OpenCommon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
std::unique_ptr<DexFile> DexFile::OpenCommon(const uint8_t* base,
                                             size_t size,
                                             const std::string& location,
                                             uint32_t location_checksum,
                                             const OatDexFile* oat_dex_file,
                                             bool verify,
                                             bool verify_checksum,
                                             std::string* error_msg,
                                             VerifyResult* verify_result) {
  if (verify_result != nullptr) {
    *verify_result = VerifyResult::kVerifyNotAttempted;
  }
  std::unique_ptr<DexFile> dex_file(new DexFile(base,
                                                size,
                                                location,
                                                location_checksum,
                                                oat_dex_file));
  if (dex_file == nullptr) {
    *error_msg = StringPrintf("Failed to open dex file '%s' from memory: %s", location.c_str(),
                              error_msg->c_str());
    return nullptr;
  }
  if (!dex_file->Init(error_msg)) {
    dex_file.reset();
    return nullptr;
  }
  if (verify && !DexFileVerifier::Verify(dex_file.get(),
                                         dex_file->Begin(),
                                         dex_file->Size(),
                                         location.c_str(),
                                         verify_checksum,
                                         error_msg)) {
    if (verify_result != nullptr) {
      *verify_result = VerifyResult::kVerifyFailed;
    }
    return nullptr;
  }
  if (verify_result != nullptr) {
    *verify_result = VerifyResult::kVerifySucceeded;
  }
  return dex_file;
}

流程总结:DexClassLoader->BaseDexClassLoader->DexPathList->makeDexElements->loadDexFile->loadDex->DexFile(String sourceName, ...)->openDexFile->(进入native)openDexFileNativc->OatFileManager::OpenDexFilesFromOat->DexFile::Open->DexFile::OpenFile->DexFile::OpenCommon->new Dexfile()

2.InMemoryDexClassLoader

有两个重载,这里选第一个,用于加载一个dex。

1
2
3
public InMemoryDexClassLoader(ByteBuffer[] dexBuffers, ClassLoader parent) {
     super(dexBuffers, parent);
  }

这里调用了父类构造函数,跟进到父类BaseDexClassLoader中。

1
2
3
4
5
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
      // TODO We should support giving this a library search path maybe.
      super(parent);
      this.pathList = new DexPathList(this, dexFiles);
  }

public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles)

1
2
3
4
5
6
public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
    ...
        this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
    ...
 
    }

makeInMemoryDexElements(ByteBuffer[] dexFiles,
List<IOException> suppressedExceptions)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
           List<IOException> suppressedExceptions) {
       Element[] elements = new Element[dexFiles.length];
       int elementPos = 0;
       for (ByteBuffer buf : dexFiles) {
           try {
               DexFile dex = new DexFile(buf);
               elements[elementPos++] = new Element(dex);
           } catch (IOException suppressed) {
               System.logE("Unable to load dex file: " + buf, suppressed);
               suppressedExceptions.add(suppressed);
           }
       }
       if (elementPos != elements.length) {
           elements = Arrays.copyOf(elements, elementPos);
       }
       return elements;
   }

DexFile(ByteBuffer buf)
这个方法中可以看到出现了DexFile对象和一个叫做mCookie的属性,这也是整体加固脱壳的一个关键点。

1
2
3
4
5
DexFile(ByteBuffer buf) throws IOException {
       mCookie = openInMemoryDexFile(buf);
      mInternalCookie = mCookie;
       mFileName = null; 
    }

openInMemoryDexFile(ByteBuffer buf)

1
2
3
4
5
6
7
8
9
10
private static Object openInMemoryDexFile(ByteBuffer buf) throws IOException {
        if (buf.isDirect()) {
            return createCookieWithDirectBuffer(buf, buf.position(), buf.limit());
        } else {
            return createCookieWithArray(buf.array(), buf.position(), buf.limit());
        }
    }
 
    private static native Object createCookieWithDirectBuffer(ByteBuffer buf, int start, int end);
    private static native Object createCookieWithArray(byte[] buf, int start, int end);

使用了两个native函数,这里就跟进到到ART中了。
DexFile_createCookieWithDirectBuffer,这里已经可以看到很多有关Dex起始地址和大小的脱壳点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static jobject DexFile_createCookieWithDirectBuffer(JNIEnv* env,
                                                    jclass,jobject buffer,jint start,jint end) {
 uint8_t* base_address = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
  if (base_address == nullptr) {
    ScopedObjectAccess soa(env);
    ThrowWrappedIOException("dexFileBuffer not direct");
    return 0;
  }
 
 std::unique_ptr<MemMap> dex_mem_map(AllocateDexMemoryMap(env, start, end));
  if (dex_mem_map == nullptr) {
    DCHECK(Thread::Current()->IsExceptionPending());
    return 0;
  }
 
  size_t length = static_cast<size_t>(end - start);
memcpy(dex_mem_map->Begin(), base_address, length);
  return CreateSingleDexFileCookie(env, std::move(dex_mem_map));
}

CreateSingleDexFileCookie

1
2
3
4
static jobject CreateSingleDexFileCookie(JNIEnv* env, std::unique_ptr<MemMap> data) {
 std::unique_ptr<const DexFile> dex_file(CreateDexFile(env, std::move(data)));
...
}

CreateDexFile

1
2
3
4
5
6
static const DexFile* CreateDexFile(JNIEnv* env, std::unique_ptr<MemMap> dex_mem_map) {
std::string location = StringPrintf("Anonymous-DexFile@%p-%p",dex_mem_map->Begin(),dex_mem_map->End());
std::string error_message;
std::unique_ptr<const DexFile> dex_file(DexFile::Open(location,0,std::move(dex_mem_map),/* verify */ true,verify_location */ true,error_message));
...
}

DexFile::Open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
std::unique_ptr<const DexFile> DexFile::Open(const std::string& location,
                                             uint32_t location_checksum,
                                             std::unique_ptr<MemMap> map,
                                             bool verify,
                                             bool verify_checksum,
                                             std::string* error_msg) {
  ScopedTrace trace(std::string("Open dex file from mapped-memory ") + location);
  CHECK(map.get() != nullptr);
 
  if (map->Size() < sizeof(DexFile::Header)) {
    *error_msg = StringPrintf(
        "DexFile: failed to open dex file '%s' that is too short to have a header",
        location.c_str());
    return nullptr;
  }
 
  std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),
                                                 map->Size(),
                                                 location,
                                                 location_checksum,
                                                 kNoOatDexFile,
                                                 verify,
                                                 verify_checksum,
                                                 error_msg);
...

OpenCommon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
std::unique_ptr<DexFile> DexFile::OpenCommon(const uint8_t* base,
                                             size_t size,
                                             const std::string& location,
                                             uint32_t location_checksum,
                                             const OatDexFile* oat_dex_file,
                                             bool verify,
                                             bool verify_checksum,
                                             std::string* error_msg,
                                             VerifyResult* verify_result) {
  if (verify_result != nullptr) {
    *verify_result = VerifyResult::kVerifyNotAttempted;
  }
  std::unique_ptr<DexFile> dex_file(new DexFile(base,
                                                size,
                                                location,
                                                location_checksum,
                                                oat_dex_file));
  if (dex_file == nullptr) {
    *error_msg = StringPrintf("Failed to open dex file '%s' from memory: %s", location.c_str(),
                              error_msg->c_str());
    return nullptr;
  }
  if (!dex_file->Init(error_msg)) {
    dex_file.reset();
    return nullptr;
  }
  if (verify && !DexFileVerifier::Verify(dex_file.get(),
                                         dex_file->Begin(),
                                        dex_file->Size(),
                                         location.c_str(),
                                         verify_checksum,
                                         error_msg)) {
    if (verify_result != nullptr) {
      *verify_result = VerifyResult::kVerifyFailed;
    }
    return nullptr;
  }
  if (verify_result != nullptr) {
    *verify_result = VerifyResult::kVerifySucceeded;
  }
  return dex_file;
}

流程总结:InMemoryDexClassLoader->BaseDexClassLoader->DexPathList->makeInMemoryDexElements->DexFile(ByteBuffer buf)->openInMemoryDexFile->(进入native)DexFile_createCookieWithDirectBuffer->CreateSingleDexFileCookie->CreateDexFile->DexFile::Open->OpenCommon->new Dexfile()
到这里就结束了,最终是new了一个native层的DexFile对象并返回指向这个对象的指针,Java层的mCookie的值也就知道了,是指向Native层Dexfile对象的指针,在这个过程中可以看到有很多的脱壳点,重点是在于dex的起始地址和大小。

4.脱壳实战演示:

如果是修改内核源码的方式,脱壳点其实相当的多,这里方便演示使用frida来脱壳,先选一个frida的脱壳点,通过分析InMemoryDexClassLoader和DexClassLoader他们最终都会调用OpenCommon函数,所以我们选择这个,主要看脱壳代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
var savepath = "/data/data/com.example.myapplication"
var dex_maps = {};
function DexFile(start, size) {
    this.start = start;
    this.size = size;
}
function hookart() {
    var addrLoadMethod = null;
    var symbols = Module.enumerateSymbolsSync("libart.so");
    for (var i = 0; i < symbols.length; i++) {
        var symbol = symbols[i];
        if (symbol.name.indexOf("VerifyResult") >= 0
            && symbol.name.indexOf("OpenCommon") >= 0
            && symbol.name.indexOf("DexFile") >= 0) {
            addrLoadMethod = symbol.address;
            console.log(addrLoadMethod)
            break;
        }
    }
 
    if (addrLoadMethod != null) {
        Interceptor.attach(addrLoadMethod, {
            onEnter: function (args) {
                this.dexbegin = args[0];
                this.size=args[1]
                console.log(hexdump(this.dexbegin),this.size.toInt32())
            },
            onLeave: function (retval) {
                var dexfilebegin = this.dexbegin;
                var dexfilesize = this.size.toInt32();
                if (this.dexbegin != null) {
                    var dexfile_path = savepath + "/" + dexfilesize + "_openCommong.dex";
                    console.log(dexfile_path)
                    var dexfile_handle = null;
                    try {
                        dexfile_handle = new File(dexfile_path, "r");
                        if (dexfile_handle && dexfile_handle != null) {
                            dexfile_handle.close()
                        }
 
                    } catch (e) {
                        dexfile_handle = new File(dexfile_path, "a+");
                        if (dexfile_handle && dexfile_handle != null) {
                            var dex_buffer = ptr(dexfilebegin).readByteArray(dexfilesize);
                            dexfile_handle.write(dex_buffer);
                            dexfile_handle.flush();
                            dexfile_handle.close();
                            console.log("[dumpdex]:", dexfile_path);
                        }
                    }
                }
                var dexfileobj = new DexFile(dexfilebegin, dexfilesize);
                if (dex_maps[dexfilebegin] == undefined) {
                    dex_maps[dexfilebegin] = dexfilesize;
                    console.log("got a dex:", dexfilebegin, dexfilesize);
                }
            }
        });
    }
}
function main(){
var targetFuncName = "open";
 
Interceptor.attach(Module.findExportByName(null, targetFuncName), {
    onEnter: function(args) {
        console.log("[*] open function called with arguments:");
        console.log("[*] Path: " + Memory.readUtf8String(args[0]));
        if(Memory.readUtf8String(args[0]).indexOf(".dex")!=-1)
        {
            hookart()
        }
        console.log("[*] Flags: " + args[1].toInt32());
    },
    onLeave: function(retval) {
        console.log("[*] open function returned: " + retval.toInt32());
    }
});
 
}
setImmediate(main)

简单说一下代码原理,首先通过遍历符号找到OpenCommon之后选择一个hook时机,这里选择lic的open函数当打开的路径包含.dex就开始hook并保存。
图片描述
这里有两个Dex,最大的那个就是。
原代码:
图片描述
脱壳后的代码,这里OnCreate被壳VMP保护,但是这一行System.loadLibrary("myapplication");这里我们是已经看到了
图片描述