本文为看雪论坛精华文章
看雪论坛作者ID:梦野间
摘要:近几年,软件的混淆强度一直在不断提升。基于编译器的混淆已经成为业界事实上的标准,最近的一些论文也表明软件的保护方式使用的是编译器级别的混淆。
在这篇文章中,我们会介绍一个基于LLVM的通用的反混淆和混淆代码重编译的方式。我们会展示如何将二进制代码提升为编译器中间语言LLVM-IR,并解释如何使用基于编译器级的优化和可满足性模理论(SMT)的迭代控制流图控制算法[3],将混淆后的二进制函数还原出它的控制流图。
这一方法不会对混淆后的代码做任何假设,取而代之的是使用LLVM中的强编译器级优化以及Souper Otimizer来简化混淆。
我们的实验结果表明这一方法能有效的简化甚至移除开源的和商业混淆器中常用的混淆技术,如Constant unfolding,基于不透明表达式的certain arithmetic,死代码插入,虚拟控制流或是整数编码。
恢复后的LLVM-IR能被进一步地被其他反混淆器处理,这些其他的反混淆器和混淆时使用的技术处于同一级别,或是会被某种LLVM后端编译到与其同一级别。这篇论文最终的成果是一个叫SATURN的反混淆工具。(图1)
图1:SATURN反混淆框架的流程图
近些年,我们发现基于中间语言和源码的混淆器变得越来越流行,主要是因为各种目标架构变得越来越多样,尤其是移动市场[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]。
我们提出了一个检测并移除不透明谓词的方法。该方法基于LLVM和Souer 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. 目标和挑战
2. 贡献
提出了一个通用的自动化反混淆工具,足以应用多种混淆技术。
提出了一个能够重编译并将LLVM-IR注入到给定二进制程序中的框架
提出了一个高效识别LLVM-IR级的不透明谓词的方法,然后使用编译器级的优化和SMT求解器处理、验证该方法。
提出一个使用Remill又不需要Remill的State结构体,将机器码转为LLVM-IR的通用方法,其中包括栈和函数参数的恢复。
我们会证明如何使用我们的框架将诸如文章[22]中的一些反符号执行的手段弱化甚至完全移除,及将之用于源码级的动态符号执行工具。
我们提出了一个框架,这个框架可以生成一个模糊约束的简洁表示,使其被更好地解析及检查可满足性。
3. 讨论
1. LLVM
2. Remill
3. Souper优化器
4. KLEE
1. 攻击模型
控制流图的恢复。要理解原函数的程序逻辑,还原混淆后的控制流图是一步非常关键的步骤。
不透明谓词的检测。只有检测出不透明谓词并移除它后,才能正确地还原控制流图。
几种混淆技术的反混淆。要使程序更具可读性,则必须将注入的混淆后的特征代码移除。
栈和参数的恢复。如果攻击者能重建栈和参数,那么函数代码会变得非常简洁。
恢复后代码的运行。如果攻击者能够执行反混淆后的代码并保证执行效果与未反混淆前一样,那么就能基于此做更多的事,比如使用调试器进行调试。
3. 分析案例
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);
}
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
}
1. 代码提升为LLVM-IR
Memory *__remill_basic_block(State &state, addr_t curr_pc, Memory* →
memory);
1. 常量
2. 栈指针别名
uint<T>_t __remill_read_memory_<T>(Memory *, addr_t);
Memory *__remill_write_memory_<T>(Memory *, addr_t, uint<T>_t)
0x146253057:learsp, [rsp-8]
0x14625305F:pushrcx
0x146253060:xchgrcx, [rsp+8]
0x146253065:movrcx, r14
0x146253068:mov[rsp+8], rcx
0x14625306D:movrcx, [rsp]
0x146253071:mov[rsp], r14
0x146253075:pushrcx
0x146253076:learcx, [rsp+8]
0x14625307B:notr14
0x14625307E:xorr14, [rcx]
0x146253081:poprcx
0x146253082:pushrbx
0x146253083:mov ebx, 0xD4469D6E
0x146253088:pushrsi
0x146253089:mov esi, 0xB7E07B2A
0x14625308E:add esi,ebx
0x146253090:mov ebx,esi
0x146253092:xor ebx, 0x533C089A
0x146253098:mov esi, 0xAB832EC0
0x14625309D:ror ebx, 0x14
0x1462530A0:and esi, 0x5B171CFB
0x1462530A6:rcl ebx, 0x1E
0x1462530A9:or ebx, 0xE4E97533
0x1462530AF:shld esi,ebx, 6
0x1462530B3:rcl ebx, 0xD
0x1462530B6:jb 0x1465C8B69
3. 使用LLVM-IR优化解决不透明谓词
extern "C" uint64_t __saturn_slice_rip(State state, addr_t curr_pc,Memory *memory, uint64_t *Stack) {
// 1 Allocate a local Remill State structure and initialize it
State S;
S.gpr.rax.qword = state.gpr.rax.qword;
...
S.gpr.rsp.qword = (uint64_t) Stack;
S.gpr.r15.qword = state.gpr.r15.qword;
S.aflag.af = state.aflag.af;
...
S.aflag.zf = state.aflag.zf;
// 2 Concretize RIP
S.gpr.rip.qword = curr_pc;
// 3/4 Call opaque basic block with initialized State struct
// This function call will be replaced with the lifted one
__remill_basic_block(S, curr_pc, memory);
// 5 Inspect the value of RIP
return
S.gpr.rip.qword;
}
define dso_local i64 @__saturn_slice_rip(%struct.State*,i64, %struct.Memory*,i64*) {
entry:
ret i645475437417 ; 0x1465C8B69
}
4. 使用Souper和Z3处理不透明谓词
define i64 @__saturn_slice_rip(%struct.State.32* %state,i64 %curr_pc, %struct.Memory* %memory,i64 * %Stack)#2 {
entry:
%0 =getelementptr inbounds%struct.State.32, %struct.State.32* %state,i64 0,i32 6,i32 17,i32 0,i32 0
%1 =load i64 ,i64 * %0, align 8, !tbaa !9
%2 =getelementptr inbounds%struct.State.32, %struct.State.32* %state,i64 0,i32 6,i32 19,i32 0,i32 0
%3 =load i64 ,i64 * %2, align 8, !tbaa !9
%4 =shl i64 %1, 56
%5 =ashr exact i64 %4, 56
%6 =add i64 %5, %3
%7 =xor i64 %3, %1
%8 =shl i64 %7, 56
%9 =ashr exact i64 %8, 56
%10 =and i64 %3, %1
%11 =shl i64 %10, 56
%12 =ashr exact i64 %11, 55
%13 =add nsw i64 %12, %9
%14 =trunc i64 %6to i32
%15 =trunc i64 %13to i32
%16 =icmp eq i32 %14, %15
%17 =select i1%16, i64 5368713261,i64 5368713259
ret i64 %17
}
(set-logic QF_BV )
(declare-fun arr () (_ BitVec 8) )
(declare-fun arr0 () (_ BitVec 8) )
(assert (let ( (?B1 arr0 ) (?B2 arr ) ) (let ( (?B3 ((_ sign_extend
24) ?B1 ) ) (?B4 ((_ sign_extend 24) ?B2 ) ) (?B5 (bvand ?
B2 ?B1 ) ) (?B6 (bvxor ?B2 ?B1 ) ) ) (let ( (?B11 ((_
sign_extend 24) ?B6 ) ) (?B10 ((_ sign_extend 24) ?B5 ) )
(?B8 (bvadd ?B4 ?B3 ) ) (?B9 (bvashr ?B4 (_ bv31 32) ) ) (?
B7 (bvashr ?B3 (_ bv31 32) ) ) ) (let ( (?B14 ((_ extract 0
0) ?B9 ) ) (?B12 ((_ extract 0 0) ?B7 ) ) (?B15 (bvshl ?
B10 (_ bv1 32) ) ) (?B13 (bvashr ?B8 (_ bv31 32) ) ) (?B16
(bvashr ?B11 (_ bv31 32) ) ) ) (let ( (?B17 ((_ extract 0
0) ?B13 ) ) (?B22 ((_ extract 0 0) ?B16 ) ) (?B19 (bvadd ?
B15 ?B11 ) ) (?B20 (bvashr ?B15 (_ bv31 32) ) ) (?B21 (
bvashr ?B15 (_ bv1 32) ) ) (?B18 (= ?B14 ?B12 ) ) ) (let (
(?B27 ((_ extract 0 0) ?B20 ) ) (?B25 (bvashr ?B19 (_ bv31
32) ) ) (?B23 (= ?B17 ?B14 ) ) (?B28 (= ?B21 ?B10 ) ) (?B24
(=false?B18 ) ) (?B26 (= ?B19 ?B8 ) ) ) (let ( (?B31 (
ite ?B26 (_ bv5368713423 64) (_ bv5368713442 64) ) ) (?B30
((_ extract 0 0) ?B25 ) ) (?B29 (or?B24 ?B23 ) ) (?B32 (=
?B27 ?B22 ) ) ) (let ( (?B35 (=false?B32 ) ) (?B33 (= ?
B30 ?B27 ) ) (?B34 (= (_ bv5368713423 64) ?B31 ) ) ) (let (
(?B36 (or?B35 ?B33 ) ) ) (let ( (?B37 (and?B36 ?B28 ) )
) (let ( (?B38 (and?B37 ?B29 ) ) ) (let ( (?B39 (=false?
B38 ) ) ) (let ( (?B40 (or?B39 ?B34 ) ) ) (and?B38 (=
false?B34 ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) )
(check-sat)
(exit)
1. IR优化
2. 代码重构
define dllexport i64 @F_140001000(%struct.State.32* %S,i64 %curr_pc,%struct.Memory.0* %memory) {
entry:
%0 =getelementptr inbounds%struct.State.32, %struct.State.32* %S,i64 0,i32 6,i32 33,i32 0,i32 0
%1 =getelementptr inbounds%struct.State.32, %struct.State.32* %S,i64 0,i32 13
store i80,i8* %1, align 1
%2 =getelementptr inbounds%struct.State.32, %struct.State.32* %S,i64 0,i32 6,i32 5,i32 0
%3 =bitcast%union.anon.2* %2to i8*
%4 =getelementptr inbounds%struct.State.32, %struct.State.32* %S,i64 0,i32 6,i32 17,i32 0
%5 =bitcast%union.anon.2* %4to i8*
%6 =load i8,i8* %5, align 1
%7 =load i8,i8* %3, align 1
store i64 5368713251,i64 * %0, align 8
%8 =sext i8%7to i64
%phitmp =icmp eq i8%7, 126
%9 =getelementptr inbounds%struct.State.32, %struct.State.32* %S,i64 0,i32 6,i32 1,i32 0,i32 0
%10 =getelementptr inbounds%struct.State.32, %struct.State.32* %S,i64 0,i32 6,i32 5,i32 0,i32 0
%11 =sext i8%6to i64
%12 =and i64 %11, 4294967295
store i64 5368713372,i64 * %0, align 8
%13 =getelementptr inbounds%struct.State.32, %struct.State.32* %S,i64 0,i32 6,i32 7,i32 0,i32 0
%14 =xor i8%7, %6
%15 =sext i8%14to i64
%16 =getelementptr inbounds%struct.State.32, %struct.State.32* %S,i64 0,i32 6,i32 17,i32 0,i32 0
store i64 %12,i64 * %16, align 8
%17 =and i64 %12, %8
%18 =shl nuw nsw i64 %17, 1
%19 =and i64 %18, 4294967294
store i64 %19,i64 * %13, align 8
%20 =add nsw i64 %18, %15
%21 =and i64 %20, 4294967295
store i64 %21,i64 * %10, align 8
store i80,i8* %1, align 1
%22 = zext i1%phitmpto i8store i8%22,i8* %3, align 1
%23 = zext i1%phitmpto i64
store i64 %23,i64 * %9, align 8
ret i64 %23}
extern "C" Memory * F_Lifted(State &state, addr_t curr_pc, Memory *memory);
extern
"C" uint64_t x64_MS_2_ARG(uint64_t *RCX, uint64_t *RDX) {
struct State S;
// Set 1. arg
S.gpr.rcx.qword = (uint64_t) RCX;
// Set 2. arg
S.gpr.rdx.qword = (uint64_t) RDX;
// Call lifted function which will be replaced and inlined
F_Lifted(S, 0, nullptr);
// Return result
return S.gpr.rax.qword;
}
define dllexport i64 @F_140001000_args(i64* %RCX,i64* %RDX,i64* %R8) {
entry:
%0 =ptrtoint i64* %RCXto i64
%1 =trunc i64%0to i8
%2 =icmp eq i8%1, 126
%3 =zext i1%2to i64
ret i64%3
}
看雪ID:梦野间
https://bbs.pediy.com/user-706972.htm
推荐文章++++
* 实战栈溢出漏洞
好书推荐