ART下抽取壳实现

xiaoeryu Lv5

函数抽取壳出现之后基本宣告一代壳整体保护的结束,由此进入到二代壳的时代。

接下来我们来分析一下二代壳的原理,看它在Dalvik&ART下分别是怎么实现函数抽取的,以及如何脱二代壳。

Dalvik下的函数抽取

关于Dalvik下函数抽取壳的实现可以去看看《Android应用安全防护和逆向分析》的作者-姜维写的两篇文章和书中都有介绍

Android中实现「类方法指令抽取方式」加固方案原理解析

  • 这篇文章对dex的结构做了一个简单的介绍,怎么去定位函数指令的地址。最后实现了一个函数抽取壳的demo

Android免Root权限通过Hook系统函数修改程序运行时内存指令逻辑

  • 这篇文章是前一篇文章的一个基础

这两篇文章介绍了在Dalvik下的函数抽取壳的原理和代码实现,实现函数抽取壳脱壳重要的一点是我们恢复这个函数的时机一定要早于这个函数被调用的时机,如果我们恢复的时机晚于被调用的时机的话那这个app的逻辑就被破坏掉了,APP自然就会崩溃。我们要保证当一个函数被调用,它的指令流必须是已经被修复了,这个就需要我们去分析源码来找到这个合适的时机点

这里先看一下Android中实现「类方法指令抽取方式」加固方案原理解析 这篇文章当中的时机点是怎么找的

  • 这篇文章里指令还原选用的是dexFindClass(),我们通过源码来看一下这个函数

我们前面讲过类加载的时候有三个步骤:

  1. 装载:查找和导入Class文件
  2. 链接:其中解析步骤是可以选择的
    1. 链接:其中解析步骤是可以选择的
    2. 准备:给类的静态变量分配存储空间
    3. 解析:将符号引用转成直接引用
  3. 初始化:即调用****函数,对静态变量,静态代码块执行初始化工作

在这个类加载的过程中是有很多的时机供我们选择的。

先来看一下这个时机点

当dex被加载完之后,就要去加载dex当中的类了:加载的时候又分两种方式。

类加载的时机:

  1. 隐式加载:

    创建类的实例

    访问类的静态变量,或者为静态变量赋值

    调用类的静态方法

    使用反射方式来强制创建某个类或接口对应的java.lang.Class对象

    初始化某个类的子类

  2. 显示加载:两者又有所区别

    使用LoadClass()加载

    使用forName()加载

下面我们先分析一下LoadClass()加载类的流程

  1. 因为我们是用DexClassLoader加载dex的,所以从DexClassLoader开始进入去找LoadClass()

  2. 继续往下分析loadClass()

  3. 跟进去findClass()

  4. 跟进loadClassBinaryName()

    • 这里经过了几次调用直接进入了Native层
  5. 接下来就进入了Native层

  6. 接下来进入findClassNoInit()

    • 这里的dexFindClass()就是我们前面要找的那个hook点,通过hook这个函数来实现对抽取函数的恢复解决方案。

    • 整个原理等于是hook掉类被加载的时机来实现函数的恢复方案,这个时机肯定是在函数执行之前完成的,因此它也就能够保证APP的正常运行

    • 往下继续分析也能找到其它的hook点,不过再往下可能就没有导出符号了,这个函数是有导出符号的可以直接在libdvm.so的导出函数找到(android-4.4.4_r1/out/target/product/manta/symbols/system/lib/libdvm.so),所以hook起来也容易一些。

    到这里我们基本上搞清楚了这篇文章中Dalvik下函数抽取壳的原理,接下来我们去分析ART下的函数抽取壳原理

ART下的函数抽取以及修复

ART下修复dex有两种方案:

  1. 禁掉dex2oat
  2. 在执行dex2oat之前填充修改dex

首先看来第一种方案这种方案使用比较普遍,代价是牺牲掉了一部分运行效率,因为oat文件的运行效率要比dex文件高。但是,如果用第二种方案在dex2oat的过程中dex文件是完整的,很容易在编译流程中被完整的脱下来。

禁用掉dex2oat的编译流程

再来看一下art下dexClassLoader()的加载,还是分析Android8.0的DexClassLoader流程,前面的流程在上一章分析过了,直接搜索GenerateOatFileNoChecks(DexClassLoader加载dex的流程最终会进入到这里)

  1. 进来之后往下找找能看到Dex2Oat()

  2. 继续跟踪

  • 上面这些流程就是最终用来调用dex2oat的流程,打断这个流程即可终止对dex2oat二进制程序的调用

HOOK execve()

接下来通过HOOK libc库中的execve()来打断对dex2oat的调用

github上有类似的项目不过好多年没有更新了,对有些art版本可能已经不适用了链接

hook代码:

void hooklibc() {
    LOGD("go into hooklibc");
    //7.0 命名空间限制
    void *libc_addr = dlopen_compat("libc.so", RTLD_NOW);
    void *execve_addr = dlsym_compat(libc_addr, "execve");
    if (execve_addr != NULL) {
        if (ELE7EN_OK == registerInlineHook((uint32_t) execve_addr, (uint32_t) myexecve,
                                            (uint32_t **) &oriexecve)) {
            if (ELE7EN_OK == inlineHook((uint32_t) execve_addr)) {
                LOGD("inlineHook execve success");
            } else {
                LOGD("inlineHook execve failure");
            }
        }
    }
}

还要重写execve的代码:让它判断一下如果是dex2oat调用的时候直接返回,如果不是也不影响它原来的流程

void* *myexecve(const char *__file, char *const *__argv, char *const *__envp) {
    LOGD("process:%d,enter execve:%s", getpid(), __file);
    if (strstr(__file, "dex2oat")) {
        return NULL;
    } else {
        return oriexecve(__file, __argv, __envp);
    }
}
  • 这样就可以停止它dex2oat的流程,接下来要找到一个时机点去还原被抽空的函数

参考寒冰的这篇文章

这里手动修改dex抽取一个函数试一下,看一下函数抽取的流程

  • 首先要从这些类列表中找到我们要修改抽取的函数,我们可以看到这里面有一千多个类太多了,可以把它的结构导出搜索一下它的位置

  • 找到之后我们来看一下dex文件格式的结构图,看函数抽取需要修改哪些内容

    • 函数抽取的关键就在于对codeItem部分进行修改

      • 这是它字节对应的结构,前16个字节对应它代表的结构信息,后面16个字节代表对应的代码指令

      这样寻找是为了去理解文件的结构,找起来比较麻烦,简单的办法是直接用jda打开函数用16禁止查看

      • 定位到之后在010Editor里面按ctrl+G搜索这个地址即可

      接下来我们如果要对函数进行抽空就需要对它的代码指令部分(后16个字节)进行清零

      代码指令部分修改完成之后还要重新计算校验和修改dex里面的checksum

      • 计算校验和脚本

        #! /usr/bin/python
        # -*- coding: utf8 -*-
        import binascii  #删除缩进(Tab)
        
        def CalculationVar(srcByte,vara,varb):#删除缩进(Tab)
            varA = vara
            varB = varb
            icount = 0
            listAB = []
        
            while icount < len(srcByte):
                varA = (varA + srcByte[icount]) % 65521
                varB = (varB + varA) % 65521
                icount += 1
        
            listAB.append(varA)
            listAB.append(varB)
        
            return listAB
        
        def getCheckSum(varA,varB): #删除缩进(Tab)
            Output = (varB << 16) + varA
            return Output
        
        if __name__ == '__main__':
            filename = '4_chouqu.dex'				# 计算校验和的文件
            f = open(filename, 'rb', True)
            f.seek(0x0c)
            VarA = 1
            VarB = 0
            flag = 0
            CheckSum = 0
            while True:
                srcBytes = []
                for i in range(1024):               #一次只读1024个字节,防止内存占用过大
                    ch = f.read(1)
                    if not ch:                      #如果读取到末尾,设置标识符,然后退出读取循环
                        flag = 1
                        break
                    else:
                        ch = binascii.b2a_hex(ch)              #将字节转为int类型,然后添加到数组中
                        ch = str(ch)
                        ch = int(ch,16)
                        srcBytes.append(ch)
                varList = CalculationVar(srcBytes,VarA,VarB)
                VarA = varList[0]
                VarB = varList[1]
                if flag == 1:
                    CheckSum = getCheckSum(VarA,VarB)
                    break
            print('[*] DEX FILENAME: '+filename)
            print('[+] CheckSum = '+hex(CheckSum))
        

      用计算的校验和替换掉原来的校验和

    修改完成我们打开修改过后的dex查看这个函数抽取效果

    • 这里就可以看到它已经抽空成功,函数体原来的代码已经变为了nop

还原函数抽取

在这里还原这个函数需要用到dex文件的method列表结构,去定位到testFunc()这个函数的位置,在它初始化的时候把它原来的字节填充回去

if (artmethod->dex_method_index_ == 15203) {//TestClass.testFunc->methodidx
    LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,start repire method", getpid(),
         dexfile->begin, dexfile->size);
    byte *code_item_addr = (byte *) dexfile->begin + artmethod->dex_code_item_offset_;
    LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,beforedumpcodeitem:%p", getpid(),
         dexfile->begin, dexfile->size, code_item_addr);

    int result = mprotect(dexfile->begin, dexfile->size, PROT_WRITE);
    byte *code_item_start = static_cast<byte *>(code_item_addr) + 16;
    LOGD("process:%d,enter loadmethod:dexfilebegin:%p,size:%d,code_item_start:%p", getpid(),
         dexfile->begin, dexfile->size, code_item_start);
    byte inst[16] = {0x1a, 0x00, 0xed, 0x34, 0x1a, 0x01, 0x43, 0x32, 0x71, 0x20, 0x91, 0x05,
                     0x10, 0x00, 0x0e, 0x00};
    for (int i = 0; i < sizeof(inst); i++) {
        code_item_start[i] = inst[i];
    }
    // 可以修改指向的字符串
    code_item_start[2] = 0x43;
    code_item_start[3] = 0x23;
    memset(dexfilepath, 0, 100);
    sprintf(dexfilepath, "/sdcard/%d_%d.dex_15203_2", dexfile->size, getpid());
    fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
    if (fd > 0) {
        write(fd, dexfile->begin, dexfile->size);
        close(fd);
    }
}

在获取结构体字段位置的时候,可以不用解析dex文件结构的方式(这样做代码量会比较大,我们也不需要那么多字段),定义一下我们需要的结构体字段直接使用也可以

struct DexFile {
    // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
    // The class we are a part of.
    uint32_t declaring_class_;
    // Access flags; low 16 bits are defined by spec.
    void *begin;
    /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
    // Offset to the CodeItem.
    uint32_t size;
};
struct ArtMethod {
    // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
    // The class we are a part of.
    uint32_t declaring_class_;
    // Access flags; low 16 bits are defined by spec.
    uint32_t access_flags_;
    /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
    // Offset to the CodeItem.
    uint32_t dex_code_item_offset_;
    // Index into method_ids of the dex file associated with this method.
    uint32_t dex_method_index_;
};
  • 标题: ART下抽取壳实现
  • 作者: xiaoeryu
  • 创建于 : 2023-09-22 23:28:32
  • 更新于 : 2023-09-26 11:34:05
  • 链接: https://github.com/xiaoeryu/2023/09/22/ART下抽取壳实现/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论