老外挑战360加固--实战分析(很详细)

时间:2024-04-02 07:38:05

老外挑战360加固–实战分析(很详细)

概述

  • 移动安全经过近几年的发展,几乎已经赶上了桌面安全的水平,所以在分析一些移动端恶意样本的时候遇到一些磕磕绊绊是很正常的事了。在大多数情况下,我们遇到的样本只是简单的经过类名、变量名以及字符串的混淆,但是也有些样本使用了一些比较复杂的保护方案:
    • 反调试: 反调试的目标有两个,一个是防止分析人员对程序进行调试附加,另一个是在检测到调试器的时候让程序执行异常流程。目前,在Dalvik虚拟机层和Native层都存在相应的反调试方案
    • 反Hook: 主要是针对一些常见Hook框架的检测,比如Xposed, Cydia substrate或者Frida这些知名Hook框架。主要的思路就是检测这些Hook框架带来的一些副作用(译者注:原文是side-effects,副作用不太好理解,其实就是比如加载了Xposed之后会给系统带来了哪些改动,类似这种思路),还有就是检测这些框架的指纹也就是特征码(比如:检测框架注入的模块是否存在,函数调用栈里面是否有这些框架的函数)
    • 反模拟器: 在分析一个可能具有恶意行为的样本时,选择使用Android模拟器来执行样本是一个比较好的办法,比如,可以使用QEMU或者Genymotion这些模拟器来观察样本是怎么执行的,样本会触发哪些异常行为。但是在实际分析中会发现,许多加壳的APK或者恶意APK一旦检测到了自己在模拟器中运行,就停止工作了(比如:恶意样本一旦发现自己在模拟器中运行,就不会执行感染函数,在那给你装好人)
    • 加密: 样本的部分资源文件或者可执行代码是以压缩或者加密的形式存在的,比如,被保护过的代码被切割成多个小段,前面的一段代码先把后面的代码片段在内存中解密,然后再去执行解密之后的代码,如此一块块的迭代执行。对于资源文件也可以同理,在使用资源之前再进行临时解密。
    • 反静态分析工具: 通常情况下,这种手段要求保护壳比较了解常见静态分析工具的缺陷才行。比如 一些classes.dex或者ELF文件按照某些工具的缺陷点经过手工处理之后, 这些静态分析工具按照常规的分析流程就不行了,比如可以通过修改文件结构或者文件里的可执行代码来实现这种保护方案,市面上常见的比如:把ELF文件的 sections 信息给删除掉,部分非关键文件头字段篡改掉或者再加入一些迷惑反汇编引擎的代码等。
    • 代码混淆:
      1. CFG流程混淆: 通常是使用o-llvm对native代码进行流程混淆
      2. 代码虚拟化: 代码虚拟化在桌面平台应用保护中已经是非常的常见了,主要的思路是自建一个虚拟执行引擎,然后把原生的可执行代码转换成自定义的指令进行虚拟执行。这篇文章的后面会涉及到一些虚拟化代码分析和虚拟引擎特征码检测的一些知识。
  • 想要处理掉上述的保护手段,最好的方式就是研究这些手段的实现原理。比如,在Google上搜索 “Android anti-debugging techniques”,会找到很多的相关资料。

有效应对保护的方案

  • 目前,为了应对这些保护方案,市面上已经出现了几种比较有效的方案。下面列出了一些研究型项目,通过这些项目产出的工具可以自动化的获取到被保护APK的原始内容,从实现方案上可以大致的分为两类:
    • 修改Android系统源码的方式: 这种方案的核心思想是修改ART或者Dalvik虚拟机的runtime源码,在源码中Hook加载类和代码执行的函数,然后虚拟机在执行这些函数的时候,就可以把相应的数据抓取下来,这些抓取的数据结果跟原始的未保护的内容是十分接近的,目前有几个项目是这种方式实现的:
      1. DexHunter: 支持dalvik和ART虚拟机,在类加载和初始化的时候从内存中dump出来,然后以DEX文件格式的形式输出
      2. AppSpear: 目前只支持dalvik,核心思路和DexHunter相似,和DexHunter不同的是,它是通过直接读取虚拟机内部的数据结构来获取DEX原始内容的
      3. Android_unpacker: 修改dex_file.cc源码,在虚拟机打开DEX文件的时候,直接dump内容作为结果
    • 辅助脚本: 有时候我们可能无法编译Android源码或者不想编译源码,这个时候我们可能就只能依赖Xposed或者Cydia substrate或者使用一些调试器特性(比如:GDB/ptrace)这些方式来进行了。所以这种思路就是在这些框架下开发辅助脚本来控制程序的执行,然后在合适的时机来保存动态加载的文件,我这里列出一些解决方案:
      1. DexHook: Xposed模块,Hook了加载DEX文件的一些主要函数,这个脚本可以很方便的进行扩展,比如Hook ART或者dalvik虚拟机runtime的更多的函数
      2. gdb scripts: 一套gdb脚本,用来调试APK进程,然后在合适的点dump出动态加载的DEX文件
  • 上面列出的这些已知解决方案确实很好,可以帮助你快速的对目标样本进行分析。但是有时候,作为一个技术人员,我们是有情怀的,于是还是想深挖一下样本是怎么被保护的,里面使用了哪些技术点。另外一点,如果我们不搞清楚保护手段的话,很难保证我们截取到的代码数据是不是完整的。举个例子来说,某些classes.dex可能只在某种条件触发的情况下才会被释放或者某个类还没有被runtime加载,这样以来我们就丢失了这些数据了。鉴于此,上述提到的这些解决方案也在这些方面做了进一步的尝试,力求最大限度的解决动态加载带来的脱壳问题。
  • 鉴于动态dump存在的问题, 有时候就不得不配合静态分析手段,通过静态分析来探究壳的内部机制以及一些鲜为人知的反分析手段
  • 下面附上几张上述解决方案中用到的原理图:
  • 老外挑战360加固--实战分析(很详细)
  • 老外挑战360加固--实战分析(很详细)

实例分析

  • 接下来我们找一个样本进行一次实战分析,保护壳样本我们选取了一个保护了几款来自中国的恶意软件的壳子。这个壳在最近的几个月一直在不断的被加强,但是其核心思路其实没有太大的变化。
  • 这个壳没有在smali层进行代码混淆,而是在最后要执行代码的时候,在内存中直接加载原始的classes.dex文件。接下来的分析中大致描述了分析整个保护的过程,其中的一些点在以后的类似样本分析中也能给你提供思路。

入口点

  • 把加壳的APK直接拖入到jadx中去,分析完毕之后,可以看到AndroidManifest.xml中包含了完整的原始信息(比如:permissions, activities, services, receivers and providers),程序原来的资源信息看上去也没有被压缩或者加密,其他的壳则有可能会尝试混淆资源文件和Manifest文件用以迷惑分析工具,不过很显然,这次我们选取的这个壳子没有做这些功能。
  • 通过查看反编译的结果,我们发现在AndroidManifest.xml中声明的入口点(译者注:估计作者的意思是说那些Activity之类的声明)并没有出现,取而代之的入口点的是com.qihoo.util.StubApp1868252644, 准确的说它是作为壳入口点存在的,这个类继承自android.app.Application, 这么做是为了在程序启动的时候第一时间去的程序的控制权。
  • 反编译的结果中还有另外两个类:
    1. com.qihoo.util.Configuration: 用来保存不同的app配置选项,在这个例子里面,选项ENABLE_CRASH_REPORT设置是false, 直观理解是crash上报被禁用了
    2. com.qihoo.util.QHDialog: 通过分析native代码之后发现,这个东西是用来在壳解压过程中发生异常的时候用来弹提示框的
  • 通过jadx的分析,初步的结论是:
    1. AndroidManifest.xml和资源文件就是原本的内容
    2. 原始的可执行代码丢失了
  • 但是,原始的可执行代码真的没有了吗?
  • classes.dex文件的大小差不多是4.3MB左右, 从jdax里面看却只包含了3个类,这些类的代码也没有这么大,所以还是很奇怪的,于是我们借助工具010Editor来看下DEX文件头:
  • 老外挑战360加固--实战分析(很详细)
  • 从图中可以看出,data_size字段为6104字节,data_off字段为2712, 但是6104+2712 = 8816并不是文件的末尾啊,所以这里面肯定有猫腻了。并且我们跳到2712这个偏移去看,发现这里其实不像是一个有效的数据内容,而且经过我的钛合金双眼的观察,发现了“qh”这样的字符串, 很像是一个magic值呢,会不会是Qihoo的数据段开始的位置呢?
  • 老外挑战360加固--实战分析(很详细)
  • 直接观察这个流数据,其实没有太多的有用的信息了,不过猜测可能是某种方式的加密数据(通过观察数据发现,里面重复出现了很多的0x52, 这种信息提示我们会不会是简单的XOR加密呢?)
  • 经过初步分析,乱猜了一通,现在开始分析com.qihoo.util.StubApp1868252644, 下面是这个类的部分反编译片段:(译者注:原文裂图了,丢图了,自己脑补吧^_^)
  • 还好,代码没有混淆,根据经验,核心逻辑在重载的attachBaseContext里面:
    1. 保存原始context
    2. 根据CPU ABI组合出壳的So文件路径
    3. 拷贝上一步中组合的文件路径中的文件拷贝到files目录
    4. 使用System.load加载So

转入Native继续分析

  • 一旦So开始加载,执行流程就进入了native层了。在继续之前,我们必须确保不要在整个So加载到过程中漏掉一些比较重要的过程。实际上,在linker执行So的入口JNI_OnLoad之前会先执行初始化函数(“Initialization and Termination Routines”)
  • 在生成So动态链接库的时候,链接器会生成 .preinit_array, .init_array, .init这几个节,这几个节的信息在 .dynamic节中也以DT_PREINIT_ARRY, DT_INIT_ARRAY, DT_INIT的类型保存着,其中数组中保存的函数指针会以在数组中保存的先后顺序来顺序执行。
  • 下面我们使用一段010Editor修复脚本LIEF来修复一下So文件,执行修复脚本之后,我们再来看:
  • 老外挑战360加固--实战分析(很详细)
  • 我们发现动态库只在0x1a00的位置包含了一个退出执行函数, 没有任何的初始化函数,不过新版本的壳在 .init_array小节包含了2个初始化函数,用来初始化字符串然后在库加载完毕之后清除了dynamic小节,这么做应该是可能为了防止内存动态dump吧。不过在我们现在这个样本中,我们可以不关心这些了,直接放心的分析JNI_OnLoad函数就好了,废话不多说了,直接祭出IDA
  • 下面,我们开2个IDA,一个加载APK文件,一个加载So文件,跟之前的那篇博客中一样, 接下来就开始在native里面分析了,JNI_OnLoad函数获取JNIEvn指针,然后就直接跳入一个比较有意思的函数开始执行了,这里我把它命名为VM_ENTER:
  • 老外挑战360加固--实战分析(很详细)
  • 这个函数之所以比较特殊是因为如果你比较熟悉虚拟化混淆, 你很快就可以识别出这就是在进入VMP虚拟机循环之前的VM_ENTER,这个函数主要实现了下面几个功能:
    1. 申请了一个0x100字节大小(后期可调整)的虚拟机栈结构(前面的0xC字节不是虚拟机上下文中使用的,所以不包含在里面)
    2. 保存原始SP
    3. 保存R0~R12, LR,PC寄存器
    4. 加载ByteCode地址到R0, 加载ByteCode大小到R1
  • 函数最后跳入到另一个函数开始执行,我这里把它命名为EXECUTE_BYTECODE, 这个函数主要实现了这几个功能:
    1. 保存状态寄存器
    2. 在堆栈上压入0x1024最为一个标记点,后面虚拟机中用这个值作为上下文边界,我这里把它标记为VM_MARK
    3. 最后,跳入真正的虚拟机循环中开始执行,我把它标记为VIRTUAL_MACHINE,里面对字节码进行解析执行(从R0 R1中传过来的值)
  • 老外挑战360加固--实战分析(很详细)
  • VIRTUAL_MACHINE函数的执行流程图乍一看还是比较恶心的,不过整个执行流程是由许多小的代码执行块构成的,整个执行流其实并不复杂,下面是IDA分析的流图:
  • 老外挑战360加固--实战分析(很详细)

虚拟机分析

  • 接下来就开始分析虚拟机循环,其中会详细的分析其中两个虚拟指令。以下分析中涉及的ARM寄存器指令都来自样本。本文分析虚拟机的主要目的是告诉大家一个分析虚拟机的思路,而不是研究出一套通用的解决方案来。在文后的附件部分提供了一个反虚拟化的脚本可以下载,脚本可以把虚拟机指令转换成ARM伪码。
  • 我们分析的这个样本的虚拟机循环算是一个比较常规的虚拟机,在其他的保护壳中的虚拟机实现可能会有所不同,这个虚拟机循环主要功能是:
    1. 把堆栈指针回滚到VM_MARK的位置,然后虚拟机保存这个指针,后续代码中访问VM_REG_CTX的时候,就用这个指针进行访问
    2. 初始化虚拟机上下文中的三个字段:
      1. VM_BYTECODE_SIZE:字节码总长度
      2. VM_BYTECODE_PTR:字节码启始地址
      3. VM_BYTECODE_INDEX:要执行的字节码索引,相当于是虚拟机的PC寄存器
    3. 把虚拟机循环抽象一下,大概是这样的:
    4. 老外挑战360加固--实战分析(很详细)
    5. 每个虚拟指令都有一个单独的标示,执行的时候会更新VM_REG_CTX和VM_BYTECODE_INDEX
  • 接下来分析两条虚拟指令,VM_NOP和VM_CALL,在开始分析的时候,执行上下文环境是:
    • R4 = VM_BYTECODE_PTR
    • R5 = VM_REG_CTX
    • R2 = VM_BYTECODE_INDEX

VM_NOP

  • 这是最简单的一条指令,顾名思义,这条指令啥也不干。实际上这条指令代表了空操作,然后虚拟PC指令会加1
  • 老外挑战360加固--实战分析(很详细)

VM_CALL

  • 这是一个非常重要的指令,用来在虚拟代码中进行函数调用或者API。这个壳的反调试和解压过程调用都是通过这个虚拟指令来进行的,最后再跳入下一阶段的逻辑,下面是VM_CALL的大致逻辑:
  • 老外挑战360加固--实战分析(很详细)

反虚拟化

  • 如果要进行反虚拟化,可以参考下面的几个步骤进行操作(译者注:使用前文提到的脚本):
    1. 在动态库中识别出ByteCode地址和块大小
    2. 跟踪执行过程把ARM指令转换成的Python表示的伪码
    3. 把虚拟码转换成ARM伪码
    4. 根据虚拟机字节码信息最后输出ARM伪码
  • 这个反虚拟化脚本目前还没有产品化,但是目前来看用来分析我们选的这个样本还是够用了,后续也可以基于这个脚本做进一步的扩展加强。其实整个过程中最具挑战性的部分是搞懂虚拟机对条件分支的处理过程(基于CPSR寄存器来完成),主要是通过算术运算、位测试和流程控制指令配合来实现的。
  • 我们分析的这个样本有4个虚拟化代码块, 这里选取第一、第三两个代码块作为例子:
  • 老外挑战360加固--实战分析(很详细)
  • 观察处理的结果,可以看到很多的BLX调用,目的地址也可以看得到。尽管这个结果还不完美,但是已经可以帮我们观察出样本执行了哪些操作,调用了哪些函数这些重要信息了。

新版本壳分析

  • 新版本的壳目前也有相应的分析文章了,不过是从中文翻译成英文的,这篇分析文章中对反调试步骤的分析过程中,执行了大量的跳转,看起来像是还有虚拟化代码一样,不过文章中对这些没有做过多的解释

反调试点

  • 像大多数壳一样,这个壳也在开始解压阶段进行了一系列的反调试检测,其中一个虚拟代码块中通过BLX调用完成了反调试检测并改变执行流程(比如:使用raise(SIGKILL)进程自杀)
  • 下面列出了几处反调试的点:
    1. 打开 proc/self/status文件,读取TracerPid,判断值是否为0
    2. 打开 /system/bin/linker,读取rtld_db_activity函数的第一个字节来判断是否被调试器附加,在没有被附加的情况下,这个函数是个空函数,如果被附加了,这个函数内是一个断电指令或者是一些未知指令,这里还是判断第一个字节是否为0
    3. 打开 proc/{pid}/cmdline,来查找指定的黑名单进程(比如:android_server, gdb, gdbserver或者其他的一些调试器)
    4. 打开 /proc/net/tcp检查是否包含00000000:23946字符串,如果有的话就说明开了IDA调试器
    5. 通过对proc/self/mem proc/self/pagemap进行监控来确保没有未授权内存映射发生
  • 在过了反调试点之后,就开始进行可执行代码解压了,主要通过以下几步进行:
    1. 申请了一块0x2F24D字节大小的内存,然后把一块加密流拷贝进去,内存块记为A
    2. 解密加密流,然后重新申请了一块0x52A88大小的内存块,记为B
    3. 以A B为参数,调用解压函数zlib->uncompress函数进行解压
    4. 解压的结果是一个全新的ELF文件,后续就开始加载执行这个文件

自加载ELF

  • 经过上面的步骤之后,解压出了一个可执行ELF,但是还没有被加载进来,这个时候壳代码就用自带的ELF加载器来进行模块加载,主要步骤如下:
    1. 加载lilbdl.so到内存中
    2. 读取ELF文件头
    3. 查找PT_LOAD段
    4. 定位到PT_DYNAMIC表,然后逐个解析表中的项
    5. 如果遇到PT_NEEDED项,就递归加载依赖库
    6. 然后处理重定位信息
    7. 执行初始化函数,然后跳转到JNI_OnLoad
    8. 然后到这里,代码执行控制权就从libjiagu.so转移到新加载的ELF模块的JNI_OnLoad了
  • 新释放出来的ELF信息如下:
  • 老外挑战360加固--实战分析(很详细)

从JNI到Dalvik

  • 新的ELF释放出来之后,主要的作用就是根据运行时环境信息(ART或者Dalvik),解密并加载原始的classes.dex(如果支持MultiDex的话还有多个Dex需要处理),主要的加载步骤如下:
    • 通过判断libdvm.so或者libart.so是否存在来判定当前runtime环境,还通过系统属性ro.yunos.vm.name来判断是不是YunOS系统
    • 如果是Dalvik虚拟机环境,则new一个0x14字节的类,如果是ART虚拟机环境则new一个0x5C大小的类,这两个类都继承自Runtime类,并且各自加载vtable
    • ART虚拟机环境下,由于牵扯到代码优化的过程,所以过程比较复杂,因此后续的分析只覆盖了基于Dalvik虚拟机的加载过程
    • 通过proc/self/maps来找到(O)DEX文件的内存地址,然后通过内存特征码”qh”来确保文件是经过壳加密过的,(O)DEX文件的其实地址和大小以及DEX索引表位置都存在一个结构体里面
    • 接着,第四段虚拟化代码开始执行(R0 = 指向section + 0xC, R1 = 0x2A0被保护代码的头大小),接着使用一个128位的key与0x52进行XOR操作来对这个头数据进行解密,解出来的数据就是一串METADATA:
    • 老外挑战360加固--实战分析(很详细)
    • 解压出来的这个METADATA数据列表包含了很多后续代码执行需要的配置数据项,比如是否支持x86,是否支持crash处理等
    • 所有的METADATA数据项经过解析之后,存入到一个radix-tree里面,后续代码需要配置项数据的时候,直接过来这个数据结构中取
    • crash上报和签名校验的时候,会来使用appkey这个数据项
    • 接下来的步骤很重要了,开始解析数据段中的class数据了,这些数据可以使用128位的key,使用RC4或者SM4算法来进行解密。每个classes.dex会单独的映射到一个内存段,然后在内存段中进行解密。
    • 每个classes.dex加载并解密之后,壳代码会读取dex的信息,如果dex中包含了AndroidManifest.xml中声明的那些Activity,则会把dex文件的加载结果加入到ClassLoader中去
    • 当时所有的classes.dex都释放到内存之后,接下来就开始按照Android系统的要求开始修复com.qihoo.util.StubApp1868252644和com.qihoo.util.StartActivity中的关键字段了:
      1. strEntryApplication: 依据METADATA中的 com.qihoo360.crypt.entryRunApplication字段进行修复,所以值是com.fake.application.MainApplication
      2. mEntryActivity:在这个样本中没有出现这个字段,如果在其他样本中出现的话,也是使用METADATA中对应的项数据直接修复
    • 现在,基本的加载过程就差不多完成了,其中getClassNameList直接就是一个native函数,就不多说了。通过查看com.qihoo.util.StubApp1868252644的反编译代码可以看到,里面有很多的native函数(比如:interface5, interface6),其实这些接口都是动态注册的:
      1. interface9:对应CrashReportDataFactory的处理逻辑
      2. 其他的还有interface5, interface6, interface7, interface8等
    • 到这里JNI_OnLoad就执行完毕了,接着开始转入com.qihoo.util.StubApp1868252644执行代码了。代码直接new了一个strEntryApplication对象,该对象的attach函数在native层设置关联了相应的ClassLoader
    • 接着执行interface8函数,主要目的是初始化原始application的content provoider, 接着调用initAssetForNative函数对assets的东西进行初始化
    • 然后到了com.qihoo.util.StubApp1868252644的onCreate函数调用,这里初始化crash上报的东西,然后在interface7里执行strEntryApplication的代码,最后的最后执行interface5里面把assetPaths加入到/data/data//files/里
    • 最后所有的东西都准备好了,开始跳入到原始的Application对象开始执行了,也就是com.fake.application.MainApplication

用到的工具

  • 本文分析的过程中,使用了一些三方工具,这里列一下:
    1. Dextra: Dalvik和ART相关的文件格式查看工具,随着Android Internals book一起发行的
    2. 010Edtor: 二进制文件解析工具,支持很多文件格式模版脚本,很强大
    3. LIEF: 简化ELF文件分析的辅助脚本,其实还有很多功能,读者可以慢慢发掘
    4. jadx: APK反编译工具,很强大
    5. IDA: 老牌的分析神器,支持APK分析,Native代码分析以及动态调试

附件

  • 这里把分析过程中用到一些相关资源列一下,可以到这里去下载:
    1. ITR.py: 解析ELF文件,并查看初始化和退出函数的脚本
    2. vm_enumlator.py: 一个虚拟机实现脚本,本文中用来分析虚拟化代码并且生成ARM伪码的辅助脚本

参考文献

  1. https://github.com/zyq8709/DexHunter
  2. https://link.springer.com/chapter/10.1007/978-3-319-26362-5_17
  3. https://github.com/CheckPointSW/android_unpacker
  4. https://github.com/rednaga/DexHook
  5. https://github.com/strazzere/android-unpacker
  6. https://developer.android.com/reference/android/app/Application.html
  7. https://developer.android.com/reference/android/content/
  8. ContextWrapper.html#attachBaseContext(android.content.Context)
  9. https://docs.oracle.com/cd/E19683-01/816-1386/6m7qcobks/index.html
  10. https://android.googlesource.com/platform/bionic/+/android-4.2_r1/linker/README.TXT
  11. https://lief.quarkslab.com/
  12. http://www.sohu.com/a/167000502_354899
  13. http://www.gabriel.urdhr.fr/2015/09/28/elf-file-format/
  14. http://newandroidbook.com/tools/dextra.html

译者