Group of Software Security In Progress

GoSSIP @ LoCCS.Shanghai Jiao Tong University

Deobfuscation of Virtualization-Obfuscated Software: A Semantics-Based Approach

今天要介绍的一篇论文Deobfuscation of Virtualization-Obfuscated Software: A Semantics-Based Approach 来自CCS’11,这篇论文讨论了一类高级混淆技术——Virtualization-Obfuscation的反混淆对抗。

Abstract

虚拟机反混淆很难,因为混淆过的程序本身很难理解和逆向,所以这种混淆可有效地对抗静态和动态分析技术。

以往的反混淆往往是建立在已经逆向好字节码解释器的基础上,并由此来推导出字节码程序的逻辑。这种outside-in的方式在字节码解释器的原理结构已知的情况下,能够有不错的结果,但无法运用于其他场景。

本文从可观测到的代码行为着手,识别出所有与这些行为相关的指令。这种inside-out的方法只需要更少的假设,弥补了以往反混淆的不足。

Introduction

以前的工作,只要知道了解释器的结构,就能很好地恢复出字节码程序,但是一旦解释器不再符合这些工作中假设的模型(如线程或多层VM混淆)。

在现代OS中,程序往往需要使用一些预定义的接口与系统进行交互,这也就是很多程序的语义。所以只要识别出所有与这些预定义接口相关的指令,就能将其他语义不相关的指令剔除,本文不试图恢复原程序的指令,仅为了提取出代码的相关行为。由于不用预先定义好混淆的模型,所以相对而言更为通用,可对抗多种混淆技术。

Deobfuscation

静态分析往往只能分析出虚拟机解释器的结构,动态trace则会把解释器指令和原程序指令混合起来,但很难把它们区分开来。

本文方法在不需要假设任何解释器或dispathcher行为模型的基础上,试图识别出原程序的指令,剔除剩余的其他指令

方法概览

  1. 动态trace
  2. 从trace中识别出系统调用及其参数,可自定义需要监控的系统调用
  3. 在指令trace上进行分析,提取出相关指令
  4. 重建相关指令的subtrace,这条subtrace就是原程序代码的一个近似

Value-based Dependence Analysis

找出所有直接或间接影响到系统调用参数的值的指令,并将这些指令定义成语义相关的。

这里做依赖性分析的目的是反向识别出所有直接或间接影响系统调用参数值的指令。但是这里没用用动态切片(Dynamic program slicing),slicing算法会找出代码中所有的控制和数据依赖关系,不过这种虚拟机程序中,所有的字节码操作都控制依赖于字节码解释器的dispatch code. 这种嵌套的结果会把解释器的代码也加进来,造成的反混淆结果的不精确。

这里使用传统的use-definition chain,这个不会引入控制依赖性,避免了切片带来的不精确。

use-def会计算I1用到的ecx和edx,以及这个地址上的值,但是这里我们并不关心ecx和edx是怎么来的,而是只关心什么时候往ecx和edx相加后的地址上写入了这个值,所以这里又是和传统的use-def不同的一点。

这里对use的定义如下

做法:以系统调用为出发点,反向遍历trace,如果有使用内存地址,则反向找出往这个地址上写入内容的指令,而不是如何计算出这个地址的指令。

但是如果在系统调用的参数里涉及到结构体指针,且在系统调用里用到了这个结构体的某个成员,仅仅使用上述方法,是无法反向找出def的位置的。

这里采用的解决办法是,先正向遍历syscall的trace,找出其中所有可能用到结构体成员内容的指令,并将此时对应的地址记下来,也将其视为syscall的参数,这样再反向从syscall调用点之前开始找def的位置,就能够避免上述不精确问题的出现了。

Relevant Conditional Control Flow

虚拟机可能有内部的标志寄存器,无法直接通过x86的eflags寄存器看出来,但是可以通过从目标地址的计算对eflags的依赖关系来看。

这里用到了作者之前工作中开发一个equational reasoning系统,这个系统会把指令翻译成等价的易于理解的表达式。

这样就能通过看目标地址的计算表达式中是否包含Flag计算,来判断这个跳转的目标地址是否可能是条件跳转了。

下面看一个没有使用标准条件分支指令的情况

再看个VMProtect中更复杂的例子:

这里的条件分支被间接地隐藏起来了,所以在计算目标地址的表达式时,只用到了直接的依赖关系。

对于每个这样的间接地址访问,都记录下地址是如何计算出来的,也生成一个相应地表达式(也即上图中得LOC),最后,只要这个表达式中没有Flag操作,就能把这个表达式删掉,剩下的,就可能也是条件分支目标地址计算过程中会用到的值。

Relevant Call-Return Control Flow

针对上述这些不同的call/return混淆,归纳出如下定义:

调用者终归要在内存中放入一个返回地址,返回时终归会从内存中取出一个地址,然后做控制流转移。

所以这里返回地址就是call和return之间的纽带。

但是由于跳转表或者函数指针等情况,也会导致误报,所以这里添加一个限制条件。

在前述过程中已经提取出来与syscall相关的指令,所以只需从这些相关指令中识别出call/return,剔除剩余的call/return。

尽管这里仍然存在一些争议,但作者认为这样的定义足以得到想要的call/return对。

Relevant Dynamic Trace

最后就是把前述3步中的结果组合起来,建立相关subtrace。

Experimental Evaluation

Experimental Methodology

同一条指令可能有多种混淆方式(比如乘法展开成循环加法),同一段代码片段也可能在不同的位置会有所不同(内联,循环展开等)。

文中采用了一种不完美的解决办法:将trace和提取出来的subtrace都是为sequence,然后用已有的序列匹配算法来进行比较,然后对比较结果打分,一个序列的分数比其他序列高就表明这个更匹配。

  1. 识别出subtrace中得条件分支和call指令,并对他们进行替换。在匹配的时候也建立等价类,保证功能相同但opcode不同的指令也能够匹配上
  2. 匹配的时候只用了opcode,没有把operand带进来,因为对于同一条指令而言,在结果中和原程序中的operand可能不同。
  3. 剔除原程序trace和subtrace的所有库函数调用、os初始化的指令
  4. 算法将trace切成一个个的segment,每个segment起始于一个syscall之后,结束于下一个syscall或trace结尾。最后对trace中所有匹配的指令数量打个分

    Relevance score: relevant trace中原始trace的指令所占的百分比

    Obfuscation score: 混淆工具添加的,但是在反混淆过程中被成功剔除的指令所占的百分比

最终实验方法:

  1. 测试程序的源码编译成可执行程序
  2. 给定输入集,跑原程序的动态trace
  3. 从第二步里的trace中提取出可执行程序模块部分的subtrace
  4. 用虚拟机混淆技术对程序进行混淆
  5. 跑混淆后程序的动态trace
  6. 使用前述反混淆分析方法进行分析,生成relevant subtrace
  7. 将混淆后的subtrace和原程序subtrace进行比较,并打分
  8. 将relevant subtrace和原程序subtrace进行比较,并打分
  9. 计算relevant score和obfuscation score
  10. 组合所有的输入集合和混淆技术,重复前述过程

实验结果

实验中用了3个作者自己的toy,分别是一个阶乘,矩阵乘法和fibonacci序列的程序,以及两个恶意软件,和一个计算md5的benchmark。

上面这些程序分别直接编译,和用VMProtect和CodeVirtualizer进行混淆,结果如下。