ART下一代壳通用解决方案

xiaoeryu Lv5

虽然Dalvik的脱壳方案对现在的一些壳还有作用,不过它只能解决一些整体性加壳的加壳方案,所以接下来来了解一下ART下脱壳的原理

本章我们主要把从InMemoryDexClassLoader到DexClassLoader(进行dex2oat和直接加载dex)流程中涉及到的脱壳点过了一遍,并修改编译Android源码,测试了对一代壳的脱壳(未进行函数抽取)。

InMemoryDexClassLoader源码分析

先来分析InMemoryDexClassLoader的源码

从libcore中找到InMemoryDexClassLoader

  • 继承自InMemroyDexClassLoader,在函数体中调用了两次InMemoryDexClassLoader(),先跟进去看一下BaseDexClassLoader()

  • 可以看到第一个参数传入的是ByteBuffer所以跟进这个方法

  • 进来之后先设置了一下父节点,然后new了一个DexPathList(),继续跟进

  • 进来之后忽略掉前面对参数的校验和对so库的一些操作,可以看到makeInMemoryDexElements()来处理了我们传入的dexFiles,继续跟进分析

  • 继续跟进DexFile()

  • 到了这里发现我们的buf传入了两个地方,它们调用完都进到native层了,接下来我们需要去native层继续跟踪

  • native层的代码我们在art里面查找可以直接检索到这两个函数,在图中237行的位置有memcpy()的动作,参数就有dex的起始地址和长度,所以我们在这个地方应该是可以将dex文件dump下来的。继续往下分析,可以看到我们找到的这两个函数在返回的时候都调用了*CreateSingleDexFileCookie()*参数传入了dex在内存中的地址,我们跟进去看一下

  • CreateSingleDexFileCookie()接收到传入的dex文件内存地址,dex地址给到了dex_file指针,之后进行操作调用ConvertDexFilesToJavaArray()将其转为java数组并返回。这里我们再跟一下CreateDexFile()看他对传入的dex文件是怎么操作的。

    • 这边再跟进去看一下DexFile::Open()都进行了什么操作

      • 这里又将参数传递给了OpenCommon()函数,再跟进去看一下

      • 可以看到这里创建DexFile对象的时候也传入了我们dex文件的起始地址和大小,再看一下

      • 通过对上面几个调用传参的分析,发现dex再加载的过程当中,有很多地方都涉及到了dex的起始地址和大小,我们也是可以获取这些信息来进行脱壳的,记录一下这些脱壳点api:

        1. static jobject CreateSingleDexFileCookie(JNIEnv* env, std::unique_ptr<MemMap> data)

        2. static const DexFile* CreateDexFile(JNIEnv* env, std::unique_ptr<MemMap> dex_mem_map)

        3. std::unique_ptr<const DexFile> dex_file(DexFile::Open(location,
                                                  0,
                                                  std::move(dex_mem_map),
                                                  /* verify */ true,
                                                  /* verify_location */ true,
                                                  &error_message));
          
        4. std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),
                                                   map->Size(),
                                                   location,
                                                   location_checksum,
                                                   kNoOatDexFile,
                                                   verify,
                                                   verify_checksum,
                                                   error_msg);
          
        5. DexFile::DexFile(const uint8_t* base,
                           size_t size,
                           const std::string& location,
                           uint32_t location_checksum,
                           const OatDexFile* oat_dex_file)
          
  • 不过InMemoryDexClassLoader()并没有对内存中的DEX信息进行编译生成OAT文件,这点和DexClassLoader()不同

DexClassLoader加载源码流程分析

DexClassLoader加载源码分析的流程会较为复杂一些,因为这当中需要涉及到dex to oat的编译过程。

虽然有很多的一代壳会禁掉Dex2Oat,但是我们先分析一下,把两种情况分开来分析。

这个前面的分析过程几乎跟再Dalvik下面的流程是一样的,简单再过一遍,熟悉的可以忽略。

  • 跟进

  • 跟进

  • 跟进

  • 继续跟进

  • 继续跟进

  • 跟进这个有五个参数的方法,因为我们传入的是五个参数

  • 继续跟进

  • 到这里接下来呢就要进入native层

跟进native层

  • 在art目录下找到openDexFileNative()的实现
  • 这里我们就可以看到函数中定义了一个OatFile指针变量,后面执行了OpenDexFilesFromOat()来进行生成oat的流程,我们跟进去看一下
  • 这段代码里面初始化了一个oat的对象,然后对oat是否为空进行了检测,因为第一次调用的时候它里面肯定是空的,然后进入switch()里面执行了MakeUpToData()方法跟进去看一下
  • 这里返回值是来自于GenerateOatFileNoChecks(),继续跟进看一下
  • 进入该函数,进行了一些校验之后在下面我们可以看到有一个Dex2Oat(),这个函数名的意思很明确我们跟进去看一下
  • 进入这个函数我们可以看到它进行了一些编译oat之前的准备工作,之后调用Exec()执行编译,跟进看一下
  • 进来之后看到它执行了ExecAndReturnCode(),继续跟进看一下
  • 进来这个函数之后,可以看到源码中创建新的进程来开始执行execve()来执行Dex2Oat的编译。
    到这里先暂停一下,接下来我们来分析如果这个壳不执行Dex2Oat它的编译流程

在这整个流程当中如果我们把函数执行的流程进行了修改或者hook,就会导致Dex2Oat流程的结束。如果我们去强制结束这个Dex2Oat的流程是可以让DexClassLoader在第一次加载dex这个过程变得非常的快速,减省去执行Dex2Oat编译花费的时间。如果要实现ART下的函数抽取技术,我们也就需要阻断掉Dex2Oat的流程。这就是ART下的函数抽取实现方案和Dalvik下的区别,因为在Dalvik下不存在Dex2Oat的编译流程。

如果我们阻断这个流程呢,oat文件就无法生成了,就会在前面判断的时候跳转到加载dex文件的分支

  • 转而加载dex文件,在这里面我们跟进它调用的DexFile::Open()看一下
  • 这个文件里面Open()重载比较多,根据传入参数的类型找到这个Open()。在里面可以看到它调用了OpenAndReadMagic()

这个函数是在devcon上来自Check Point的安全研究人员所使用的其中一个脱壳点
File fd = OpenAndReadMagic(filename, &magic, error_msg);

  • 不过这个脱壳点并不是很好,因为此时dex文件还没有加载到内存中,因为是dex文件所以接下来会执行到DexFile::OpenFile()中去,跟进去看一下

  • 在这里我们可以看到将文件映射到了内存,之后又进入了OpenCommon()继续跟进

  • OpenCommon()这里也有基址和大小,所以这个位置也可以作为一个脱壳点,

    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)
    
  • 接下来的流程又进入了new DexFile()继续跟进

  • 这里也有基址和大小可以作为我们的一个脱壳点

DexFile::DexFile(const uint8_t* base,
                 size_t size,
                 const std::string& location,
                 uint32_t location_checksum,
                 const OatDexFile* oat_dex_file)
  • DexFile()对dex文件的对象进行了初始化,到这里我们就把从禁用Dex2Oat到加载dex文件的流程分析完了

  • 在加载dex文件这个过程中我们找到了三个脱壳点

    1. File fd = OpenAndReadMagic(filename, &magic, error_msg);

    2. 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)
      
    3. DexFile::DexFile(const uint8_t* base,
                       size_t size,
                       const std::string& location,
                       uint32_t location_checksum,
                       const OatDexFile* oat_dex_file)
      
  • 这里可以发现第2和第3个脱壳点和我们前面分析InMemoryDexClassLoader流程的时候的脱壳点是重合的,所以不管它使用哪一种ClassLoader去进行dex加载,在经过这两个脱壳点的时候都是可以dump下来的。接下来就实现一下这些脱壳点

修改源码脱壳

在脱壳点获取内存中dex文件的基址和大小将其dump到文件中

// 获取当前进程的ID  
int pid = getpid();  
  
// 创建一个字符数组,用于存储dex文件的路径  
char dexFilePath[100] = {0};  
  
// 使用sprintf函数将路径格式化为字符串,并存储在dexFilePath数组中  
// 路径为"/sdcard/",后面跟着两个整数(size和pid),然后是dex文件的名称"OpenCommon.dex"  
sprintf(dexFilePath, "/sdcard/%d_%d_OpenCommon.dex", (int)size, pid);  
  
// 打开dex文件,使用O_CREAT|O_RDWR标志,文件权限为666(所有用户可读可写)  
int fd = open(dexFilePath, O_CREAT|O_RDWR, 666);  
  
// 如果文件打开成功(文件描述符大于0)  
if(fd > 0){  
    // 向文件中写入数据,数据来源于base指针指向的内存,大小为size  
    int number = write(fd, base, size);  
  
    // 如果写入的字节数大于0,说明写入成功  
    if(number > 0){}  
  
    // 关闭文件  
    close(fd);  
}

PS:注意编译Android源码要给虚拟机起码300MB以上的硬盘空间,因为单单只是源码就有一百多G

编译源码命令

  • 进入源码目录下执行下面的命令
source build/envsetup.sh

lunch

21<根据手机型号选择编译版本>

time make -j4 <线程多开几个编译速度会快点,虚拟机的内存给到16G以上>
编译完成

将编译好的img拷贝出来

写个bat脚本自动刷机,也可以手动刷机。

@echo off  
echo entering Fastboot model...  
adb reboot bootloader  
  
echo Locking device...  
fastboot oem unlock  
  
echo Flashing the system image...  
fastboot flash boot boot.img
fastboot flash vendor vendor.img
fastboot flash system_a system.img
fastboot flash system_b system_other.img
fastboot flash userdata userdata.img
  
echo Restarting the device...  
fastboot reboot  
  
echo Flashing is complete!  
pause
刷机完成

测试脱壳效果

PS:测试之前可以先去把sdcard目录下安装系统时候产生的dex文件删除掉,有点多。

先测试一下我们原来自己写的LoadDex,安装apk把dex放在sdcard路径下

adb install .\loadDex.apk

adb push 4.dex /sdcard/

  • 安装完成后在手机系统设置里给apk打开文件访问权限,才能脱壳写文件

  • 设置完之后运行app,然后再*/sdcard/*目录下找到dump下来的dex文件

然后,新建一个目录将这些文件都pull下来

  • 先把sdcard目录下的所有dex都放在一个文件夹里,然后把整个文件夹pull下来

    mkdir /sdcard/dex

    cp *.dex /sdcard/dex

在电脑上准备好的目录下执行

adb pull /sdcard/dex

同时我们可以在sdcard目录下的dex文件中搜索TestActivity

grep -ril "TestActivity" ./*.dex

然后用GDA打开这些文件查看一下

  • 完全正常

因为这个apk和dex是我们自己写的使用DexClassLoader加载的,所以肯定是可以dump下来的。我们再拿其它的apk试试。

换了两个app,Express100和货拉拉司机版

Express100
  • Express100这个dex脱壳正常所有的函数代码都能正常显示,下面试试货拉拉的。
货拉拉司机版
  • 货拉拉dump下来的dex打开之后发现函数体都是空的,因为这个壳做了函数抽取的保护方案。这次我们使用的整体脱壳的方案不能应对函数抽取壳的脱壳,下一篇我们处理第二代壳函数抽取壳的问题。

小结一下:

前面我们对InMemoryDexClassLOader和Dex2Oat被禁用掉这两个流程进行了脱壳点的总结,找到了它们的脱壳点(虽然大部分壳为了安全都不会这么做,因为执行to oat的时候dex文件时没有加密的),接下来我们再分析一下如果它执行了Dex2Oat怎么脱壳。

进行Dex2Oat流程的脱壳分析

在前面我们分析到了执行ExecAndReturnCode()的时候,执行到了execve()这个函数的时候就会去调用Dex2Oat()这种方式来加载dex文件

  • 如果这里正常执行过execve()之后,我们就进入到了Dex2Oat的流程当中了
  • 这个文件可以看到是一个带有main()的可执行程序,进入main()函数先执行了Dex2Oat()。我们跟进去看看
  • 这个函数的前面是对传进来的命令行参数的解析、初始化ART内存映射、检查要编译的dex文件是否可以打开等操作,我们主要跟进dex2oat->Setup()看一下编译前的设置。
  • 在Setup()的结尾处呢,会对当前所有的dex文件进行一个遍历确保存活然后注册,我们就可以在这个地方进行dex的脱壳
    包括还有一些其它的地方也会有对dex内存地址的引用

  • 我们需要的时候都可以在这些地方添加脱壳代码进行脱壳。在流程中有很多这种脱壳点,不一个一个找出来了。

  • 标题: ART下一代壳通用解决方案
  • 作者: xiaoeryu
  • 创建于 : 2023-09-14 00:09:38
  • 更新于 : 2023-09-23 18:15:38
  • 链接: https://github.com/xiaoeryu/2023/09/14/ART下一代壳通用解决方案/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论