首页
社区
课程
招聘
[翻译]SATURN反混淆框架
发表于: 2019-12-30 21:11 16011

[翻译]SATURN反混淆框架

2019-12-30 21:11
16011

SATURN

基于LLVM的反混淆框架

摘要

近几年,软件的混淆强度一直在不断提升。基于编译器的混淆已经成为业界事实上的标准,最近的一些论文也表明软件的保护方式使用的是编译器级别的混淆。在这篇文章中,我们会介绍一个基于LLVM的通用的反混淆和混淆代码重编译的方式。我们会展示如何将二进制代码提升为编译器中间语言LLVM-IR,并解释如何使用基于编译器级的优化和可满足性模理论(SMT)的迭代控制流图控制算法[3],将混淆后的二进制函数还原出它的控制流图。这一方法不会对混淆后的代码做任何假设,取而代之的是使用LLVM中的强编译器级优化以及Souper Otimizer来简化混淆。我们的实验结果表明这一方法能有效的简化甚至移除开源的和商业混淆器中常用的混淆技术,如Constant unfolding,基于不透明表达式的certain arithmetic,死代码插入,虚拟控制流或是整数编码。恢复后的LLVM-IR能被进一步地被其他反混淆器处理,这些其他的反混淆器和混淆时使用的技术处于同一级别,或是会被某种LLVM后端编译到与其同一级别。这篇论文最终的成果是一个叫SATURN的反混淆工具(图1)
图片描述
图1:SATURN反混淆框架的流程图

关键词

逆向工程,llvm,code lifting,混淆,反混淆,静态软件分析,二进制重编译,二进制重写

1. 简介

近些年,我们发现基于中间语言和源码的混淆器变得越来越流行,主要是因为各种目标架构变得越来越多样,尤其是移动市场[11]。传统的基于二进制的混淆方案容易受到基于模式匹配或是简单的静态分析的攻击,而基于中间语言和源代码的混淆则难以被有效地攻击。现代保护工具大多是基于 一些最先进的框架,如LLVM,这种工具支持更复杂的混淆逻辑[11][23]。

 

在这篇论文中,我们会展示一种基于LLVM代码优化的自动反混淆方式。这篇论文的重点集中在反混淆过程中需要解决的几个问题:将机器码翻译成LLVM-IR;控制流图恢复;不透明谓词检测;反混淆;Brightening(重构代码以使它更具可读性)恢复后的函数以及重编译。

 

将机器码转为LLVM-IR并不能一步到位。二进制操作码不仅仅会执行操作本身,还会操作条件码、条件标志,它们会影响到后续的分支指令。用于将机器码转为类似LLVM-IR的中间语言的信息往往会在编译时丢失,尤其是在处理混淆后的机器码的时候,这一过程会更难。其中一种解决方案是,将每一条机器码的语义存储到结构体中,然后保存当前寄存器的状态。这是将机器码转为虚拟环境又不需要对代码本身做出一些前提假设的常用方法。恢复后的LLVM-IR很实用,但是可读性非常差。这 篇论文中我们使用了Remil[21][14]来处理这一转化过程。

 

控制流混淆是一项用于隐藏原始函数的控制流的技术。要将函数反混淆,攻击者必须将混淆后的代码进行恢复。基于LLVM-IR的现代混淆工具能够对控制流图进行重度混淆。我们引入了一种算法,它使用Remill中的State结构体来恢复提升(将机器码转为更高一级的语言,本文中指LLVM-IR,后同)后的基本块的边。这些边和提升后的基本块构成了恢复后的控制流图。在提升(译者注:lifted,意为将机器码转为LLVM-iR)混淆后代码的过程中,控制流图的恢复是自动静态完成的。在控制流图的恢复过程中,相比于前人的方式([13][37][12][35][26]),我们的方法不需要任何机器码相关的先验知识,也不依赖函数追踪。相反,路径的恢复是基于部分反混淆后的基本块以及它们的前驱块。我们的算法和文章[3]“迭代控制流图构建”比较像,不过我们的更高级,算法结果与分支被访问的顺序是无关的。

 

隐藏控制流图的一种方法是插入不透明谓词(OP,opaque predicates),以使native级控制流图重建算法失效。不透明谓词是指插入到控制流图中用于增加逆向难度的条件分支。不过它的条件总是固定的,因此不会影响到程序的原有逻辑[7]。我们提出了一个检测并移除不透明谓词的方法。该方法基于LLVMSouer Optimizer优化。对于那些和编译器优化相冲突的不透明谓词,我们使用SMT求解器来处理。用SMT求解器来识别不透明谓词并不新鲜[19],但我们相信把这几个工具和算法结合起来的方式是一种不错的方法。

 

常量折叠,基于数论的不透明表达式,死代码,虚拟控制流和整数编码不仅仅能在加固后的代码中找到,它也出现在一些未经混淆的代码中。通常而言,在源代码编译阶段,编译器会检测源代码的特征并对它进行优化以获得更高的执行效率。我们提出的方法基于LLVM-IR重构,因此,Remill将机器码提升的方式可能会使我们难以达到最好的效果。重新生成LLVM-IR需要的步骤都是通用的,并不需要任何关于混淆器的先验知识。

 

如果不进行brightening,LLVM-IR已经够用了,但本文的目标是使提升后的函数能够达到 vanilla(尽可能地达到与混淆和编译前的源码相近)状态。要达到这一状态,我们需要重构原来的函数参数并基于State结构体(代码1)将Remill指定的函数转换一个没有原始签名的LLVM函数。

struct State {
    VectorReg vec[kNumVecRegisters];
    ArithFlags aflag;
    Flags rflag;
    Segments seg;
    AddressSpace addr;
    GPR gpr;
    X87Stack st;
    MMX mmx;
    FPUStatusFlags sw;
    XCR0 xcr0;
    FPU x87;
    SegmentCaches seg_caches;
}

代码1:Remill中x86_64的State结构体

 

在控制流图恢复后,我们就可以认为这个函数已经被反混淆了,我们提出的方法的其中一个目标是重编译并执行提升后的函数。因为选择将LLVM-IR作为提升后函数的目标语言,所以我们可以使用任意一种LLVM后端(X86,ARM,AArch64,RISC-V以及其他)轻易地将恢复后的代码重新编译成机器码。

 

实验表明我们的方法可以和当前的一些先进的混淆器相结合,以对抗文章[22]中的反符号反混淆技术。

 

我们的成果不仅仅可以用于反混淆,事实上这一方法还有一些其他的作用,比如模糊测试,作为动态符号执行(DSE,dynamic symbolic execution)引擎的输入,如KLEE[4],作为基于LLVM的混淆器的输入,如O-LLVM[11],在漏洞利用中实现payload的自动生成[34],或是以最高的CPU优化级别(-march=native)重新编译成机器码以提高程序的性能或是使用有安全保护功能的编译器重新进行编译。上述功能主要是针对那些没有源代码的应用。

1.1 目标和挑战

我们想要现有的应用开发一个基于LLVM及其强优化的反混淆框架。在开始阶段使用LLVM进行逆向似乎过于复杂了,但它和源代码编译的过程很类似。LLVM编译器框架有大量的工具可用于创建并修改控制流图,基本块和指令。真正困难的地方在于将机器码提升为LLVM-IR,并将之重构为未经过编译和混淆的源代码。要达成这一目标,这项技术需要是一门通用,稳定和轻量级的技术。这一框架生成的LLVM-IR需要能够重新被编译和执行。我们希望这一框架生成的LLVM-IR非常易于理解,在LLVM生态系统中,有大量成熟的工具可以对LLVM-IR进行操作。我们的最终目标是将攻击点还原到它实现的地方——编译级。

1.2 贡献

我们的贡献可以总结如下.

  • 提出了一个通用的自动化反混淆工具,足以应用多种混淆技术。
  • 提出了一个能够重编译并将LLVM-IR注入到给定二进制程序中的框架
  • 提出了一个高效识别LLVM-IR级的不透明谓词的方法,然后使用编译器级的优化和SMT求解器处理、验证该方法。
  • 提出一个使用Remill又不需要Remill的State结构体,将机器码转为LLVM-IR的通用方法,其中包括栈和函数参数的恢复。
  • 我们会证明如何使用我们的框架将诸如文章[22]中的一些反符号执行的手段弱化甚至完全移除,及将之用于源码级的动态符号执行工具。
  • 我们提出了一个框架,这个框架可以生成一个模糊约束的简洁表示,使其被更好地解析及检查可满足性。

1.3 讨论

将混淆后的机器码提升为LLVM-IR的过程可以分为好几步。在SATURN中,我们实现了几种算法来处理混淆后的机器码的各种情况。据我们所知,SATURN的实现足够先进,它能将攻击面从混淆后的机器码提升到编译器级别。我们的成果对混淆后的二进制安全也有足够的影响,它使得源码级的或是IR级的动态符号执行工具,如KLEE,能够进一步地对恢复后的代码进行分析。我们列举了几个可能使代码提升失效的小案例,以及将我们的方法用在强混淆的程序中的实例。

2. 背景

2.1 LLVM

LLVM最初是伊得诺大学的一项研究项目,目的是提供一种基于SSA的现代编译方法[33],以使其支持任意语言的动态和静态编译。后来,LLVM逐渐发展为一个由许多子项目组成的大项目,其中许多子项目也被用于各种各样的商业或开源项目,在学术研究中也有了广泛的使用[16]。要理解本文的框架并不需要我们对LLVM及其中间语言LLVM-IR有多深的了解,但你需要知道LLVM-IR是基于静态单赋值(SSA,Single Assignment form)[8]的,这使得它更容易被构造成传给SMT求解器的公式。

2.2 Remill

Remill是一个将机器码转为LLVM bitcode的静态二进制转换器。它支持x86和amd64架构。本文优化了Remill将机器码转换成IR的过程。Remill并不会对栈和提升后的函数参数做任何假设,因为它是基于单指令的。

2.3 Souper优化器

Souper使用起来很方便,因为它是基于LLVM的项目,它使用KLEE将一系列LLVM-IR指令转换为SMT公式,并使用SMT求解器来寻找可行的优化。我们可以使用它的结果来确定条件分支中的不透明谓词。它可以将SMT的查询结果缓存起来,并放入Redis数据库中以提高性能[24]。它会生成一个由不透明谓词和混淆特征组成的数据库,方便我们进一步分析。

2.4 KLEE

KLEE是一个基于LLVM-IR的符号执行工具,它可以自动生成测试用例,并能在一系列复杂的和环境密集型项目上实现高覆盖率。KLEE并不仅仅是一个用于测试软件的工具,它也能用于还原被混淆的代码。文章[22]的工作试图使类似KLEE这样的工具难以达到其预期的效果。

3. 动机

3.1 攻击模型

目标:我们设想的是man-at-the-end(MATE)场景,即攻击者对被保护的应用程序拥有所有的访问权限,但是获取不到源码或是未经保护的应用程序。我们的攻击模型和使用的方法和[28]相似,同时也类似于[22]。具体的说,我们主要关注下述目标:

  1. 控制流图的恢复。要理解原函数的程序逻辑,还原混淆后的控制流图是一步非常关键的步骤。
  2. 不透明谓词的检测。只有检测出不透明谓词并移除它后,才能正确地还原控制流图。
  3. 几种混淆技术的反混淆。要使程序更具可读性,则必须将注入的混淆后的特征代码移除。
  4. 栈和参数的恢复。如果攻击者能重建栈和参数,那么函数代码会变得非常简洁。
  5. 恢复后代码的运行。如果攻击者能够执行反混淆后的代码并保证执行效果与未反混淆前一样,那么就能基于此做更多的事,比如使用调试器进行调试。

3.2 分析案例

我们先来介绍几个在文章[22]中提到的小程序,它们使用了反符号执行的面向路径的保护方案。代码2展示了一个没有使用混淆优化的小例子,它使用了文章[22]中提到的FORSPLIT来进行反符号执行,并额外加上了不透明谓词来保护条件分支的计算。

intfunc(charchr,charch1,charch2) {
    chargarb = 0;charch = 0;
    // FOR trickfor(inti = 0; i < chr; i++)
        ch++;
    // SPLIT trick
    if(ch1 > 60)
        garb++;
    else
        garb--;
    if(ch2 > 20)
        garb++;
    else
        garb--;
    // MBA based opaque predicate
    if((chr + ch2) == ((chr ^ ch2) + 2 * (chr & ch2)))
        ch ^= 97;
    else
        ch ^= 23;
    return(ch == 31);
}

代码2:基于文章[22]的小程序,使用了反符号执行,面向路径和FOR及SPLIT的保护方案

define dso_local i32 @func(i8 signext) local_unnamed_addr #0 {
    %2 =icmp eq i8%0, 126
    %3 =zext i1%2to i32
    ret i32%3
}

代码3:编译成LLVM-IR的未经保护的小程序

 

反符号执行的手段不能防御编译器级的优化,因此在编译时使用clang -O3级的优化,可以轻松地将它去掉。不透明谓词对编译器级的优化依然有效,只能通过SMT求解器来进行恢复。在我们的测试用例中,我们使用clang -O0对程序进行编译,以防止优化器将我们的反符号执行优化掉。输出的二进制包含几个栈slots,需要我们在重构代码时对它进行还原。如果我们不能还原栈slots和参数,那么LLVM优化就无法生效,反符号执行也没办法移除掉。如果我们成功地还原了,那么我们获取的LLVM-IR看起来应该和代码3中使用clang -O3 -S -emit-llvm对未混淆的代码编译后,编译出的IR相似。

4. 函数恢复

SATURN的两个核心功能分别是控制流图重构和遍历。LLVM生态系统依赖于算法的强大和准确,在开发SATURN的pass的过程中,我们使用的就是这些算法。在本节中,我们会介绍SATURN是如何对函数的机器码进行恢复的。

4.1 代码提升为LLVM-IR

SATURN非常依赖Remill。这也是为什么我们说理解Remill是如何将native指令提升为LLVM-IR是一件很重要的事情。Remill利用目标体系架构的CPU指令集来提升指令。在表1中,我们可以看到x86_64架构中的State结构体。
为了模拟x86_64中如add rax,rcx这样的指令,Remill会生成一个辅助函数的调用语句,以模拟对应的指令。辅助函数将State结构体作为参数(代码4),并根据指令的语义计算其结果,以及修改Flags寄存器。当基本块中的所有指令转为IR后,生成的调用会作为一个内联函数放进调用者中。这一步中输出的LLVM-IR可读性还不高,但是它的功能和native指令完全一致。

Memory *__remill_basic_block(State &state, addr_t curr_pc, Memory* →
memory);

代码4:Remill基本块的C/C++函数签名

 

在转化为IR的过程中,SATURN将每个恢复后的基本块存储进自身的LLVM-IR函数中。接下来,这些基本块函数会和一个个LLVM-IR函数连接起来,用于组成恢复后的控制流图(图2)
图片描述
图2:地址1000处的包含恢复后的控制流图的LLVM函数,基本块会被转为LLVM函数,并根据其用法被调用。调用的结果决定了条件分支的目标地址。

 

通过这种设计方式,我们可以直接优化提升为IR的基本块函数,并在后续的反混淆步骤中优化性能。在这一步中进行优化也能移除一些简单的混淆特征。控制流函数需要保持尽量简单,理想的情况是在不需要修改提升后的代码及处理LLVM的PHI节点的情况下,能够自由地添加或移除边[33]。

 

SATURN中规定了在提升基本块为IR的过程中,是怎样进行路径搜索的。SATURN使用Remill中的指令分类方式来对生成的提升函数进行分类。SATURN会基于这个分类来判断基本块是不是以不透明谓词(表1)结尾的。如果基本块可能是不透明谓词,SATURN会首先尝试使用LLVM的优化passes来确定它的出边。如果优化过后,出边的数量大于1,SATURN会尝试使用Souper和Z3 SMT求解器[9]计算出它真正的出边具体是哪一条。SATURN会尽量优先使用LLVM的优化,因为相比于SMT求解器,它的性能消耗更低。我们对各种混淆引擎的测试表明大多数不透明谓词都无法对抗LLVM的优化。我们会在第5节中具体描述不透明谓词的处理细节。

 

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2019-12-31 20:23 被梦野间编辑 ,原因:
上传的附件:
收藏
免费 5
支持
分享
最新回复 (4)
雪    币: 3712
活跃值: (1541)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
2
翻译的不错,赞
2019-12-31 09:43
0
雪    币: 6
活跃值: (1226)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
看起来思路不错,要是有用这个理论反混淆vmp的实例就好了
2019-12-31 10:28
0
雪    币: 1001
活跃值: (723)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
工具还没开源?
2020-2-4 04:12
0
雪    币: 3757
活跃值: (1757)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
5
TinyMin 工具还没开源?
没有
2020-2-19 12:22
0
游客
登录 | 注册 方可回帖
返回