侧边栏壁纸
博主头像
Fup1p1 's Blog 博主等级

梦想是财富自由~

  • 累计撰写 38 篇文章
  • 累计创建 24 个标签
  • 累计收到 9 条评论

目 录CONTENT

文章目录

【Reverse】LLVM与代码混淆技术(笔记)

Fup1p1
2023-08-16 / 0 评论 / 9 点赞 / 1994 阅读 / 0 字 / 正在检测是否收录...

LLVM(基础篇)

所有代码都放到了我的github仓库里了
https://github.com/Fup1p1/OLLVM-Learning

LLVM概述与LLVM编译器

GCC编译器与LLVM编译器的区别

GCC编译器

image-1692164496301

LLVM编译器

image-1692164511976
两者都是三段式编译器,主要差别就是中间代码了

LLVM 的编译流程

image-1692165029744

  1. 先通过前端Clang进行翻译,变成中间代码IR(bc即为二进制格式的LLVM IR文件)
  2. 再通过优化器优化,得到优化之后的中间代码文件
  3. 再经过LLVM IR层面的链接器llvm-link 得到一个中间代码文件
  4. 经过静态编译器llc,得到目标平台的机器代码
    image-1692165041748

LLVM的优势

模块化

image-1692164818718

可拓展

image-1692164829330

LLVM Pass

  • LLVM Pass 框架是整个 LLVM 提供给用户用来干预代码优化过程的框架,也是我们编写代码混淆工具的基础。
  • 编译后的 LLVM Pass 通过优化器 opt 进行加载,可以对 LLVM IR 中间代码进行分析修改,生成新的中间代码。

image-1692186211881

LLVM源代码目录结构

llvm/include/llvm

  • llvm/include/llvm 文件夹存放了 LLVM 提供的一些公共头文件。
  • 即我们在开发过程中可以使用的头文件。
    image-1692186593985

llvm/lib

llvm/lib 文件夹存放了 LLVM 大部分源代码(.cpp 文件)和一些不公开的头文件
image-1692186692366

llvm/lib/Transforms

  • llvm/lib/Transforms 文件夹存放所有 LLVM Pass 的源代码。
  • llvm/lib/Transforms 文件夹也存放了一些 LLVM 自带的 Pass。

image-1692186715813

练手编写一个LLVM Pass,遍历程序中的所有函数

  • 在设计一个新的 LLVM Pass 时,你最先要决定的就是选择 Pass 的类型。
  • LLVM 有多种类型的 Pass 可供选择,包括:ModulePass(基于模块)、FuncitonPass(基于函数)、CallGraphPass(基于调用图)、LoopPass(基于环)等等。
    而我们最常用到的就是 FunctionPass

FunctionPass

  • FunctionPass 以函数为单位进行处理。
  • FunctionPass 的子类必须实现 runOnFunction(Function &F) 函数。
  • 在 FunctionPass 运行时,会对程序中的每个函数执行runOnFunction 函数。

编写步骤

  1. 创建一个类(class),继承 FunctionPass 父类
  2. 在创建的类中实现 runOnFunction(Function &F) 函数。
  3. 向 LLVM 注册我们的 Pass 类。

编译

  • 如果选择基于 CMake 的编译方法,直接使用 CMake 进行编译即可。
  • 在 Build 文件夹内可以找到编译好的 so 文件。

LLVM Pass 的加载

使用优化器 opt 将处理中间代码,生成新的中间代码:

opt -load ./LLVMObfuscator.so -hlw -S hello.ll -o hello_opt.ll

-load 加载编译好的 LLVM Pass(.so文件)进行优化

开始实践

image-1692342833158

  • Build 文件夹 :存放编译后的LLVM Pass

  • Test 文件夹:存放测试程序 xxx.cpp

  • Transforms

    • src 文件夹:存放整个LLVM Pass项目的源代码
      • HelloWorld.cpp :HelloWorld Pass的源代码,一个Pass使用一个cpp文件实现即可
    • CMakeLists.txt:整个CMake项目的配置文件
  • test.sh :编译LLVM Pass并且对Test文件夹中的代码进行测试

			//CMakeLists.txt
project(OLLVM++)    #指定项目的名称
cmake_minimum_required(VERSION 3.13.4)   #指定CMake最低版本

#一些和LLVM的环境导入有关的配置
find_package(LLVM REQUIRED CONFIG)       	
list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
include(AddLLVM)										

include_directories("./include")    # 项目的包含文件夹,这样就可以包含 ./include 文件夹中的头文件
													
#LLVM有关的配置项
separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS})
add_definitions(${LLVM_DEFINITIONS_LIST})
include_directories(${LLVM_INCLUDE_DIRS})


#向LLVM注册一个LLVMObfuscator的模块,每个模块都对应一个so文件
add_llvm_library( LLVMObfuscator MODULE
    src/HelloWorld.cpp
)
			//test.sh
cd ./Build       
cmake ../Transforms
make
cd ../Test
clang -S -emit-llvm TestProgram.cpp -o TestProgram.ll
opt -load ../Build/LLVMObfuscator.so -hlw -S TestProgram.ll -o TestProgram_hlw.ll
clang TestProgram_hlw.ll -o TestProgram_hlw
./TestProgram_hlw
//      		HelloWorld.cpp
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;

namespace{     //自定义命名空间
    class HelloWorld :public FunctionPass{     //继承FunctionPass类
        public :
            static char ID;
            HelloWorld():FunctionPass(ID){}     //构造函数
            bool runOnFunction(Function &F);

    };

}
bool HelloWorld::runOnFunction(Function &F){
		//todo
    outs()<<"Hello,"<<F.getName()<<"\n";   //这里通过outs()来获取整个LLVM的输出流,用法和cout一样。  我们可以用过getName去获取到每一个函数的名称。
}
char HelloWorld::ID=0;
static RegisterPass<HelloWorld>X("hlw","My first line of LLVM Pass");   //向LLVM注册我们的pass    其中X()中的第一个参数是指定LLVM Pass的参数 ,这样再使用opt加载这个so文件的时候,用这个参数来指定用哪个Pass来优化,第二个参数就是对Pass的描述。

运行结果

image-1692358056244

LLVM IR 概述

什么是LLVM

  • LLVM IR 是一门低级编程语言,语法类似于汇编
  • 任何高级编程语言(如C++)都可以用 LLVM IR 表示
  • 基于 LLVM IR 可以很方便地进行代码优化

LLVM的两种表示方法

这个前面接触过,一个是人类可阅读的文本形式,后缀为.ll,一个是易于机器处理的二进制格式.bc
.ll 和 .bc 可以通过 llvm-dis 和llvm-as 这两个llvm自带的工具进行转换

LLVM IR 结构

image-1692358858315

模块 Module

image-1692358952037

函数

入口块可以跳转其他基本块执行
image-1692358966549

基本块

image-1692358975250

  • 控制流平坦化 :以函数为基本单位的混淆:
  • 虚假控制流 :以基本块为基本单位的混淆:
  • 指令替换 :以指令为基本单位的混淆:

LLVM IR 的常用指令

终结指令 Terminator Instructions

基本块的最后一个指令,通常是跳转指令或者返回值了

ret 指令

函数的返回指令,对应 C/C++ 中的 return。
image-1692360637203
实例
image-1692360676811

br 指令

  • br 是”分支”的英文 branch 的缩写,分为非条件分支和条件分支,对应 C/C++ 的 if 语句。
  • 无条件分支类似于x86汇编中的 jmp 指令,条件分支类似于x86汇编中的 jnz, je 等条件跳转指令。

image-1692360933415

icmp是一个比较指令 会比较 a和b的值
如果 cond 为true = => IfEqual cond为false ==> IfUnequal

image-1692361598941

switch指令

分支指令,可看做是 br 指令的升级版,支持的分支更多,但使用也更复杂。对应 C/C++ 中的 switch。
image-1692363064619

image-1692362876379

补充icmp指令
  • 整数或指针的比较指令。
  • 条件 cond 可以是 eq(相等), ne(不相等), ugt(无符号大于)等等。

ty是比较数的类型
image-1692362056058

image-1692362236858

fcmp 指令
  • 浮点数的比较指令
  • 条件 cond 可以是 oeq(ordered and equal), ueq(unordered or equal), false(必定不成立)等等。
  • ordered的意思是,两个操作数都不能为 NAN
    image-1692362394885

image-1692362410510

二元运算 Binary Operations

加减乘除等指令,就和x64汇编很像了

add

整数加法指令,对应 C/C++ 中的“+”操作符,类似x86汇编中的 add 指令。

image-1692365053014

image-1692365095427

sub指令

整数减法指令,对应 C/C++ 中的“-”操作符,类似x86汇编中的 sub 指令。
image-1692365372248

image-1692365380917

mul指令

整数乘法指令,对应 C/C++ 中的“*”操作符,类似x86汇编中的 mul 指令。
image-1692365435909

image-1692365440656

udiv指令

无符号整数除法指令,对应 C/C++ 中的“/”操作符。如果存在exact关键字,且op1不是op2的倍数,就会出现错误。
image-1692365494834

image-1692365534776

sdiv 指令

有符号整数除法指令,对应 C/C++ 中的“/”操作符。
image-1692365571607

image-1692365600225

urem 指令

无符号整数取余指令,对应 C/C++ 中的“%”操作符。
image-1692365628475

image-1692365632156

srem指令

有符号整数取余指令,对应 C/C++ 中的“%”操作符。
image-1692365661897

image-1692365668021

按位二元运算 Bitwise Binary Operations

shl指令

整数左移指令,对应 C/C++ 中的“<<”操作符,类似x86汇编中的 shl 指令。
image-1692365792738

image-1692365798036

lshr指令

整数逻辑右移指令,对应 C/C++ 中的“>>”操作符,右移指定位数后在左侧补0。

image-1692365839733

image-1692365851800

ashr

整数算数右移指令,右移指定位数后在左侧补符号位(负数的符号位为1,正数的符号位为0)。
image-1692366229132

image-1692366235438

and指令

整数按位与运算指令,对应 C/C++ 中的“&”操作符。
image-1692366367223

image-1692366372917

or指令

整数按位或运算指令,对应 C/C++ 中的“|”操作符。
image-1692366391331

image-1692366396024

xor指令

整数按位异或运算指令,对应 C/C++ 中的“^”操作符。
image-1692366411544

image-1692366417888

内存访问和寻址操作 Memory Access and Addressing Operations

静态单赋值

  • 在编译器设计中,静态单赋值(Static Single Assignment, SSA),是 IR 的一种属性。
  • 简单来说,SSA 的特点是:在程序中一个变量仅能有一条赋值语句。
  • LLVM IR 正是基于静态单赋值原则设计的。

image-1692537558482

image-1692537589487

image-1692537620332image-1692537655996
假如C++是基于静态单赋值原则的,那么左图就不符合。但是修改为右图就符合SSA原则了。

alloca指令

  • 内存分配指令,在 中分配一块空间并获得指向该空间的指针,类似于 C/C++ 中的 malloc 函数。

image-1692537782697
image-1692537793309

store指令

  • 内存存储指令,向指针指向的内存中存储数据,类似于 C/C++ 中的指针解引用后的赋值操作。
    image-1692538080233

image-1692538097092

load指令

  • 内存读取指令,从指针指向的内存中读取数据,类似于 C/C++ 中的指针解引用操作。
    image-1692538454536

image-1692538671028

类型转换操作 Conversion Operations

trunc … to 指令(大转小)

  • 截断指令,将一种类型的变量截断为另一种类型的变量。对应 C/C++ 中大类型向小类型的强制转换(比如 long 强转 int)
    image-1692538811538

image-1692539354792

zext … to 指令 (小转大)

  • *** 零拓展(Zero Extend)指令,将一种类型的变量拓展为另一种类型的变量,高位补0。对应 C/C++ 中小类型向大类型的强制转换(比如 int 强转 long)
    image-1692539604912

image-1692539597205

sext … to 指令 (符号位扩展)

  • 符号位拓展(Sign Extend)指令,通过复制符号位(最高位)将一种类型的变量拓展为另一种类型的变量。
    image-1692540021057

image-1692540031945

其他操作 Other Operations

phi 指令

image-1692540302331

image-1692540310617

  • phi 指令可以看做是为了解决 SSA 一个变量只能被赋值一次而引起的问题衍生出的指令。
  • phi 指令的计算结果由 phi 指令所在的基本块的 前驱块 确定。
    image-1692540278749
  • 以下是一个用 phi 指令实现for循环的实例:
    image-1692540287959
    0,%LoopHeader 代表程序的前趋块是从外部进入for循环的话,phi的运算结果就是0
    如果基本块的前趋块是loop,那么代表还在循环中,那么运算结果就是上一个循环中的nextindvar这个变量
    后面通过add指令对 %indvar 加一

select 指令

  • select 指令类似于 C/C++ 中的三元运算符”… ? … : …”
    image-1692542421676

image-1692542431304

call指令

  • call 指令用来调用某个函数,对应 C/C++ 中的函数调用,与x86汇编中的 call 指令类似。

image-1692542536465

image-1692542456491

C++ 基础及 LLVM Pass 常用 API

C++ 基础就 继承 多态 封装 还有 STL模板类 等.这里不详细介绍了

LLVM Pass框架中的三个核心类

  • 在 LLVM Pass 框架中,三个最核心的类为 Function, BasicBlock, Instruction,分别对应 LLVM IR 中的函数、基本块和指令。

Function

  • 与 Function 有关的操作主要是获取函数的一些属性,比如名称等等,以及对函数中基本块的遍历:
    image-1692600316679

  • 在 LLVM 中可以通过 foreach 循环对函数 Function 中的每个基本块 BasocBlock 进行遍历:

image-1692600347486

BasicBlock

  • 与 BasicBlcok 有关的操作主要是基本块的克隆、分裂、移动等,以及对基本块中指令的遍历:

image-1692600445236

image-1692600519520

Instruction

  • 指令可以有很多种,亦即 Instruction 类可以拥有多个子类,如:BinaryOpterator, AllocaInst, BranchInst 等。
    image-1692600621574
  • 与 Instruction 有关的操作主要是指令的创建、删除、修改以及指令中操作数的遍历:
    image-1692600646833
  • 在 LLVM 中可以通过 for 循环对指令 Instruction 中的每个操作数 Value* 进行遍历。
    image-1692600679516
  • 所有可以被当做指令操作数的类型都是 Value 的子类,Value 有以下五种类型的子类。
    image-1692600791515

LLVM 部分官方文档介绍

LLVM Doc

image-1692601173900

LLVM(实战篇)

基本块分割

image-1692614263390image-1692614271549

怎么实现?

  • 遍历每个函数中的每个基本块,对每个基本块进行分割即可。

但是注意,有 PHI 指令的基本块我们选择跳过

使用到的API

基本块分割主要用到了以下几个API
image-1692614498283

额外参数指定

  • 在 LLVM 中,可以通过 cl::opt 模板类获取指令中的参数,这里的 opt 是选项 option 的缩写,不是优化器的意思:

image-1692614738757

image-1692614743615

splitBasicBlock函数

  • splitBasicBlock 函数是 BasicBlock 类的一个成员函数。在BasicBlock.h 头文件里可以看到这个函数的两种用法:
    image-1692614996152
    一般都用第二个用法,第一个要迭代器。
  • arg1 :就是在指令I处一分为二。也就是在指令I前的指令都会放在基本块一处,指令I及之后的指令都会放在基本块二处。然后在指令块1处建立一个绝对跳转,跳转到基本块二处。
  • arg2 :指定分裂出来的基本块二的名称
  • arg3 :Before如果为true,那么就会把第二个基本块移到第一个基本块的前面

isa<>函数

  • isa<> 是一个模板函数,用于判断一个指针指向的数据的类型是不是给定的类型,类似于 Java 中的 instanceof。
    image-1692615909388

基本块分割

//      		SplitBasicBlock.cpp
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Instructions.h"
#include "llvm/Support/raw_ostream.h"
#include <vector>
#include <llvm/Support/CommandLine.h>
using namespace llvm;
using namespace std;

static cl::opt<int> splitNum("split_num", cl::init(2), cl::desc("Split<split_num> time(s) each BB"));

namespace{     //自定义命名空间
    class SplitBasicBlock :public FunctionPass{     //继承FunctionPass类
        public :
            static char ID;
            SplitBasicBlock():FunctionPass(ID){}     //构造函数
            bool runOnFunction(Function &F);
            void split(BasicBlock *BB);
            bool containPHI(BasicBlock *BB);
    };

}
bool SplitBasicBlock::runOnFunction(Function &F){
		//todo
    vector<BasicBlock*> origBB;
    for(BasicBlock& BB: F){
        origBB.push_back(&BB);

    }for(BasicBlock *BB : origBB){
        if(!containPHI(BB))
            split(BB);
    }
}
bool SplitBasicBlock::containPHI(BasicBlock *BB){
    for(Instruction &I: *BB){
        if(isa<PHINode>(&I)){
            return true;
        }
    }
}
void SplitBasicBlock::split(BasicBlock *BB){
    int splitsize=(BB->size()+splitNum-1)/splitNum; //向上取整
    BasicBlock *curBB=BB;
    for(int i=0;i<splitNum;i++){
        int cnt=0;
        for(Instruction &I:*curBB){
            if(cnt++==splitsize){
                curBB=curBB->splitBasicBlock(&I);
                break;
            }
        }
    }

}
FunctionPass *createSplitBasicPass(){
    return new SplitBasicBlock();
}
char SplitBasicBlock::ID=0;
static RegisterPass<SplitBasicBlock>X("split","split one basic block into mut");   //向LLVM注册我们的pass    其中X()中的第一个参数是指定LLVM Pass的参数 ,这样再使用opt加载这个so文件的时候,用这个参数来指定用哪个Pass来优化,第二个参数就是对Pass的描述。

对比两个ll文件,确实能发现分割了字符串。但是经过编译优化后,最后的可执行文件使用IDA打开后发现cfg图其实还是一样的。
image-1692667122876

代码混淆基本原理

术语介绍

image-1692667293469

代码混淆

  • 代码混淆是将计算机程序的代码,转换成一种功能上等价,但是难以阅读和理解的形式的行为。

函数

  • 函数是代码混淆的基本单位,一个函数由若干个基本块组成,有且仅有一个入口块,可能有多个出口块。
  • 一个函数可以用一个控制流图(Control Flow Graph, CFG)表示。

基本块

  • 基本块由一组线性指令组成,每一个基本块都有一个入口点(第一条执行的指令)和一个出口点(最后一条执行的指令,亦即终结指令)。
  • 终结指令要么跳转到另一个基本块(br, switch),要么从函数返回(ret)。

控制流

  • 控制流代表了一个程序在执行过程中可能遍历到的所有路径。
  • 通常情况下,程序的控制流很清晰地反映了程序的逻辑,但经过混淆的控制流会使得人们难以分辨正常逻辑。
    image-1692667517698

不透明谓词

  • 不透明谓词指的是其值为混淆者明确知晓,而反混淆者却难以推断的变量。
  • 例如混淆者在程序中使用一个恒为0的全局变量,反混淆者难以推断这个变量恒为0。

控制流混淆

  • 控制流混淆指的是混淆程序正常的控制流,使其在功能保持不变的情况下不能清晰地反映原程序的正常逻辑。
  • 本课程中要学习的控制流混淆有:控制流平坦化、虚假控制流、随机控制流。

计算混淆

  • 计算混淆指的是混淆程序的计算流程,或计算流程中使用的数据,使分析者难以分辨某一段代码所执行的具体计算。
    image-1692667795316

经典代码混淆工具 OLLVM 体验

OLLVM 介绍

Obfuscator-LLVM(简称OLLVM)是2010年6月由瑞士西部应用科学大学(HEIG-VD)的信息安全小组发
起的一个项目。 这个项目的目的是提供一个 LLVM 编译套件的开源分支,能够通过代码混淆和防篡改提
供更高的软件安全性。

OLLVM 提供了三种经典的代码混淆:

  • 控制流平坦化 Control Flow Flattening
  • 虚假控制流 Bogus Control Flow
  • 指令替代 Instruction Subsititution

OLLVM 在国内移动安全的使用非常广泛,虽然 OLLVM 已于2017年停止更新,并且到目前为止,三种
代码混淆方式均已有成熟的反混淆手段 。但是 OLLVM 的各种变体仍然在被开发和使用(如王者荣耀的
某个so文件),OLLVM 至今仍有很大的学习价值。

OLLVM 初体验

控制流平坦化(Control Flow Flattening)

可用选项:

  • -mllvm -fla : 激活控制流平坦化
  • -mllvm -split : 激活基本块分割
  • -mllvm -split_num=3 : 指定基本块分割的数目,这里一个基本款会被分割成3个基本块后再进行控制流平坦化混淆

示例

clang -mllvm -fla -mllvm -split -mllvm -split_num=3 TestProgram.cpp -o TestProgram_fla

若在编译时出现 stddef.h 和 stdarg.h 头文件不存在的错误,可以使用 locate stddef.h 和
locate stdarg.h 指令找到这两个头文件的位置,然后复制到 /usr/include
或 /usr/local/include 目录下。

虚假控制流(Bogus Control Flow)

  • -mllvm -bcf : 激活虚假控制流
  • -mllvm -bcf_loop=3 : 混淆次数,这里一个函数会被混淆3次,默认为 1
  • -mllvm -bcf_prob=40 : 每个基本块被混淆的概率,这里每个基本块被混淆的概率为40%,默认为30

示例

clang -mllvm -bcf -mllvm -bcf_loop=3 -mllvm -bcf_prob=40 TestProgram.cpp -o TestProgram_bcf

指令替换(Instruction Substitution)

  • -mllvm -sub : 激活指令替代
  • -mllvm -sub_loop=3 : 混淆次数,这里一个函数会被混淆3次,默认为 1

示例

clang -mllvm -sub -mllvm -sub_loop=3 TestProgram.cpp -o TestProgram_sub

OLLVM 原理分析

控制流平坦化(Control Flow Flattening)

  • 入口块:进入函数第一个执行的基本块。
  • 主分发块与子分发块:负责跳转到下一个要执行的原基本块。
  • 原基本块:混淆之前的基本块,真正完成程序工作的基本块。
  • 返回块:返回到主分发块。
    image-1692689072296

代码实现与分析

image-1692689167770

第一步:保存原基本块

  • 将除入口块以外的以外的基本块保存到 vector 容器中,方便后续处理。
  • 如果入口块的终结指令是条件分支指令,则将该指令单独分离出来作为一个基本块,加入到 vector 容器的最前面。

image-1692689782449

第二步:创建分发块和返回块

  • 除了原基本块之外,我们还要继续创建一个分发块来调度基本块的执行顺序。并建立入口块到分发块的绝对跳转。
  • 再创建一个返回块,原基本块执行完后都需要跳转到这个返回块,返回块会直接跳转到分发块。
    image-1692691863308

其中,这个BasicBlock::Create函数中的 参数说明

  • 第一个参数是一个LLVMContext
  • 第二个参数是指创建的基本块的名称
  • 第三个参数是函数指针(也就是你要在哪个函数里创建基本块)
  • 第四个参数指向了一个基本块,代表我们创建的基本块会被移动到这个基本块的前面(是物理上的移动)

但是这样会破坏之前的物理顺序与其逻辑关系相对应的规律,所以我们要通过moveBefore方法来把我们的入口块移动到最前面,然后删除入口块的最后一条指令,然后跳转到分发块。

第三步:实现分发块调度

image-1692693238777

  • 在入口块中创建并初始化 switch 变量,在调度块中插入switch 指令实现分发功能。
  • 将原基本块移动到返回块之前,并分配随机的 case 值,并将其添加到 switch 指令的分支中。
    image-1692693041716

第四步:实现调度变量自动调整

  • 在每个原基本块最后添加修改 switch 变量值的指令,以便返回分发块之后,能够正确执行到下一个基本块。
  • 删除原基本块末尾的跳转,使其结束执行后跳转到返回块。
    image-1692694076355
    image-1692694089175

第五步:修复 PHI 指令和逃逸变量

  • PHI 指令的值由前驱块决定,平坦化后所有原基本块的前驱块都变成了分发块,因此 PHI 指令发生了损坏。
  • 逃逸变量指在一个基本块中定义,并且在另一个基本块被引用的变量。在原程序中某些基本块可能引用之前某个基本块中的变量,平坦化后原基本块之间不存在确定的前后关系了(由分发块决定),因此某些变量的引用可能会损坏。
  • 修复的方法是,将 PHI 指令和逃逸变量都转化为内存存取指令。
    image-1692694251271

全部代码

Flattening.cpp

#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/Instructions.h"
#include "llvm/Transforms/Utils.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Transforms/Utils/Local.h"
#include "SplitBasicBlock.h"
#include "Utils.h"
#include <vector>
#include <cstdlib>
#include <ctime>
using namespace llvm;
using std::vector;
namespace{
    class Flattening :public FunctionPass{
        public :
            static char ID;
            
            Flattening():FunctionPass(ID){
                srand(time(0));
            }
            void flatten(Function &F);
            bool runOnFunction(Function &F);

    };

}
bool Flattening::runOnFunction(Function &F){
    INIT_CONTEXT(F);
    flatten(F);
    return true;
}
void Flattening::flatten(Function &F){
    //第一步,保存除了入口块的基本块
    vector<BasicBlock*> origBB;
    for(BasicBlock& BB:F){
        origBB.push_back(&BB);
    }
    origBB.erase(origBB.begin());
    BasicBlock &entryBlock=F.getEntryBlock();
    if(BranchInst *br=dyn_cast<BranchInst>(entryBlock.getTerminator())){
        if(br->isConditional()){//判断是不是条件跳转
            origBB.insert(origBB.begin(),entryBlock.splitBasicBlock(br));
        }
    }
    //第二步,创建分发块和返回块
    BasicBlock *dispatchBB=BasicBlock::Create(*CONTEXT,"dispatchBB",&F,&entryBlock);
    BasicBlock *retBB=BasicBlock::Create(*CONTEXT,"retBB",&F,&entryBlock);
    entryBlock.moveBefore(dispatchBB);
    entryBlock.getTerminator()->eraseFromParent();
    BranchInst::Create(dispatchBB,&entryBlock);
    BranchInst::Create(dispatchBB,retBB);

    //第三部,实现分发块的调度功能
    int randNumcase=rand();
    AllocaInst * swVarPtr =new AllocaInst(TYPE_I32,0,"swVar.ptr",entryBlock.getTerminator());
    new StoreInst(CONST_I32(randNumcase),swVarPtr,entryBlock.getTerminator());
    LoadInst *swVar=new LoadInst(TYPE_I32,swVarPtr,"swVAR",dispatchBB);
    BasicBlock *defaultBB = BasicBlock::Create(*CONTEXT,"defaultBB",&F,retBB);
    BranchInst::Create(retBB,defaultBB);  //基本块肯定要有一条中间指令,所以这里随便添加一条跳转指令
    SwitchInst *swInst=SwitchInst::Create(swVar,defaultBB,0,dispatchBB);//插入到分发块的后面

    //为每个基本块分配随机的case值
    for(BasicBlock *BB:origBB){
        BB->moveBefore(retBB);
        swInst->addCase(CONST_I32(randNumcase),BB);
        randNumcase=rand();

    }
    //第四步 实现调度变量自动调整

    for(BasicBlock *BB:origBB){
        if(BB->getTerminator()->getNumSuccessors()==0){//如果这个基本块的最后一条指令是返回指令(没有后继快)
            continue;
        } else if(BB->getTerminator()->getNumSuccessors()==1){//如果这个基本块还有一个后继块(即为绝对跳转)
            ConstantInt *numcase = swInst->findCaseDest(BB->getTerminator()->getSuccessor(0));
            new StoreInst(numcase,swVarPtr,BB->getTerminator());
            BB->getTerminator()->eraseFromParent();
            BranchInst::Create(retBB,BB);
        }else if(BB->getTerminator()->getNumSuccessors()==2){ //有两个后继块
             ConstantInt *numcase1 = swInst->findCaseDest(BB->getTerminator()->getSuccessor(0));
             ConstantInt *numcase2 = swInst->findCaseDest(BB->getTerminator()->getSuccessor(1));
             BranchInst *br1=cast<BranchInst>(BB->getTerminator()); //两个分支也有可能是switch指令,但是在混淆前,使用LLVM IR指令 (使用自带的 LowerSwitch的LLVM Pass),将switch替换成了branch,所以这里最后一条指令只能是branch
             SelectInst *sel=SelectInst::Create(br1->getCondition(),numcase1,numcase2,"",BB->getTerminator());
             new StoreInst(sel,swVarPtr,BB->getTerminator());
            BB->getTerminator()->eraseFromParent(); //删除原先的终结指令
             BranchInst::Create(retBB,BB);
        }
    }
    //第五步 修复 phi指令和逃逸变量
    fixStack(F);
}

char Flattening::ID=0;
static RegisterPass<Flattening>X("fla","My control flow flattening obfuscation");

Utils.cpp

#include"Utils.h"
#include<vector>
#include "llvm/IR/Instructions.h"
#include "llvm/Transforms/Utils/Local.h"
#include "llvm/Transforms/Utils/ValueMapper.h"
#include "llvm/Transforms/Utils/Cloning.h"
using std::vector;
using namespace llvm;
LLVMContext *CONTEXT=nullptr;
void llvm::fixStack(Function &F){
    vector<PHINode*> origPHI;
    vector<Instruction*> origReg;
    BasicBlock &entryBB=F.getEntryBlock();
    for(BasicBlock &BB:F){
        for (Instruction &I:BB)
        {
            if(PHINode *PN=dyn_cast<PHINode>(&I)){
                origPHI.push_back(PN);
            }else if(!(isa<AllocaInst>(&I)&& I.getParent()==&entryBB)&& I.isUsedOutsideOfBlock(&BB)){//判断是不是逃逸变量(除了通过Alloc,并且定义在入口块的,都要处理)
                origReg.push_back(&I);
            }
        }
        
    }
    for(PHINode *PN:origPHI){
        DemotePHIToStack(PN,entryBB.getTerminator());
    }
    for(Instruction *I:origReg){
        DemoteRegToStack(*I,entryBB.getTerminator());
    }
}


头文件啥的就不放了

虚假控制流 (Bogus Control Flow)

  • 虚假控制流指,通过向正常控制流中插入若干不可达基本块(永远不会被执行的基本块)和由不透明谓词造成的虚假跳转,以产生大量垃圾代码干扰攻击者分析的混淆。

原理分析

  • 虚假控制流是以基本块为单位进行混淆的,每个基本块要经过分裂、克隆、构建虚假跳转等操作。
    image-1693275664350

第一步 基本块拆分

  • 通过 getFirstNonPHI 函数获取第一个不是 PHINode 的指令,以该指令为界限进行分割,得到 entryBB 和 bodyBB。(授课人说根据经验,PHI指令往往都会堆积在基本块的最前部,即在第一个不是PHINode指令的前面)
  • 以 bodyBB 的终结指令为界限进行分割,最终得到头部、中部和尾部三个基本块,也就是 entryBB, bodyBB 和 endBB。
    image-1693276150840

第二步 基本块克隆(克隆bodyBB)

  • LLVM 自带 CloneBasicBlock 函数,但该函数为不完全克隆,还需要做一些补充处理。
  • 我们把基本块的克隆操作写到 createCloneBasicBlock 函数中:
    image-1693276829272

在克隆的基本块中,仍然引用了原基本块中的 %a 变量,该引用是非法的,故需要将 %a 映射为 %a.clone:
image-1693277039280

  • 根据 VMap 对克隆基本块中的变量进行修复:
    image-1693277087437

第三步 构造虚假跳转

image-1693277644417

  • 该比较指令等价于 if(y < 10 || x * (x + 1) % 2 == 0):

image-1693277742988

OLLVM其实还对克隆块进行了变异的处理,往里面添加很多随机的指令

代码实现

#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Transforms/Utils/ValueMapper.h"
#include "llvm/Transforms/Utils/Cloning.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Instructions.h"
#include "SplitBasicBlock.h"
#include "Utils.h"
#include <vector>
#include <cstdlib>
#include <ctime> 
using std::vector; 
using namespace llvm;
static cl::opt<int> obfuTimes("bcf_loop",cl::init(1),cl::desc("Obfucase a function <bcf_loop> time()."));
namespace{
    class BogusControlFlow : public FunctionPass{
        public:
            static char ID;
            BogusControlFlow():FunctionPass(ID){
                srand(time(NULL));
            }
            bool runOnFunction(Function &F);
            void bogus(BasicBlock *BB);
            Value * createBogusCmp(BasicBlock *insertAfter);
    };
}

bool BogusControlFlow::runOnFunction(Function &F){
    INIT_CONTEXT(F);
    FunctionPass *pass =createSplitBasicBlockPass();
    pass->runOnFunction(F);
     for(int i=0;i<obfuTimes;i++){
        vector<BasicBlock*> origBB;
        for(BasicBlock& BB:F){
            origBB.push_back(&BB);
        }
        for(BasicBlock *BB:origBB){
            bogus(BB);
        }
     }
     return true;

}
Value *BogusControlFlow::createBogusCmp(BasicBlock * insertAfter){
    Module *M=insertAfter->getModule();
    GlobalVariable *xptr= new GlobalVariable(*M,TYPE_I32,false,GlobalValue::CommonLinkage,CONST_I32(0),"x");  // False ->  不是常量   设置 x 初始值为 0;
    GlobalVariable *yptr= new GlobalVariable(*M,TYPE_I32,false,GlobalValue::CommonLinkage,CONST_I32(0),"y");  
    LoadInst *x=new LoadInst(TYPE_I32,xptr,"",insertAfter); //全局变量不能直接使用,用load来读取
    LoadInst *y=new LoadInst(TYPE_I32,yptr,"",insertAfter);
    ICmpInst *cmp1=new ICmpInst(*insertAfter,CmpInst::ICMP_SLT,y,CONST_I32(10));
    BinaryOperator *opt1=BinaryOperator::CreateAdd(x,CONST_I32(1),"",insertAfter);   
    BinaryOperator *opt2=BinaryOperator::CreateMul(x,opt1,"",insertAfter);   
    BinaryOperator *opt3=BinaryOperator::CreateSRem(opt2,CONST_I32(2),"",insertAfter);   
    ICmpInst *cmp2=new ICmpInst(*insertAfter,CmpInst::ICMP_EQ,opt3,CONST_I32(0));
    return BinaryOperator::CreateOr(cmp1,cmp2,"",insertAfter);


}
void BogusControlFlow::bogus(BasicBlock *entryBB){
    BasicBlock *bodyBB=entryBB->splitBasicBlock(entryBB->getFirstNonPHI(),"BodyBB");
    BasicBlock *endBB=bodyBB->splitBasicBlock(bodyBB->getTerminator(),"endBB");

    //第二步,对中间的基本块 BodyBB 进行克隆,得到cloneBB
    BasicBlock *cloneBB=createCloneBasicBlock(bodyBB);

    //第三部 构建虚假跳转
    entryBB->getTerminator()->eraseFromParent();    
    bodyBB->getTerminator()->eraseFromParent();    
    cloneBB->getTerminator()->eraseFromParent();

     Value *cond1 =createBogusCmp(entryBB);//创建恒为真的比较式
     Value *cond2 =createBogusCmp(bodyBB); 

     BranchInst::Create(bodyBB,cloneBB,cond1,entryBB);
     BranchInst::Create(endBB,cloneBB,cond2,bodyBB);
     BranchInst::Create(bodyBB,cloneBB);


}
char BogusControlFlow::ID=0;
static RegisterPass<BogusControlFlow> X("bcf","My control bogus control flattening obfuscation");   //向LLVM注册我们的pass    其中X()中的第一个参数是指定LLVM Pass的参数 ,这样再使用opt加载这个so文件的时候,用这个参数来指定用哪个Pass来优化,第二个参数就是对Pass的描述。

指令替代

原理分析

  • 指令替代指将正常的二元运算指令(如加法、减法、异或等等),替换为等效而更复杂的指令序列,以达到混淆计算过程的目的。
  • 例如将 a+b 替换为 a - (-b) ,将 a ^ b 替换为 (~a & b) | (a & ~b) 等等
  • 仅支持整数运算的替换,因为替换浮点指令会造成舍入的错误和误差。

总体思路

扫描所有指令,对目标指令(加法、减法、与或非、异或)进行替换:
image-1693314076989

加法替换

image-1693314281911

减法替换

image-1693314304654

与替换

image-1693314317304

或替换

image-1693314326644

异或替换

image-1693314335708

随机控制流

  • 随机控制流是虚假控制流的一种变体,随机控制流通过克隆基本块,以及添加随机跳转(随机跳转到两个功能相同的基本块中的一个)来混淆控制流。
  • 与虚假控制流不同,随机控制流中不存在不可达基本块和不透明谓词,因此用于去除虚假控制流的手段(消除不透明谓词、符号执行获得不可达基本块后去除)失效。
  • 随机控制流的控制流图与虚假控制流类似,都呈长条形。

混淆效果

  • 随机的跳转和冗余的不可达基本块导致了大量垃圾代码,严重干扰了攻击者的分析。
  • 并且 rdrand 指令可以干扰某些符号执行引擎(如 angr)的分析。
    image-1693315953275

原理分析

  • 随机控制流同样是以基本块为单位进行混淆的,每个基本块要经过分裂、克隆、构造随机跳转和构造虚假随机跳转四个操作。
    image-1693316023620

一. 基本块拆分

不多说,和虚假控制流一样,分成三个基本块。

  • 通过 getFirstNonPHI 函数获取第一个不是 PHINode 的指令,以该指令为界限进行分割,得到 entryBB 和 bodyBB。
  • 以 bodyBB 的终结指令为界限进行分割,最终得到头部、中部和尾部三个基本块,也就是 entryBB, bodyBB 和 endBB。
    image-1693316174881

二.基本块克隆

和虚假控制流一样,但是还是有些出入,还要修复逃逸变量
image-1693316911489
所以克隆前要先修复逃逸变量
image-1693317024015

三.构造随机跳转

  • 将生成随机数的指令插入到 entryBB ,将生成的随机数命名为 randVar,并在 entryBB 后插入基于 randVar 的随机跳转指令。

image-1693317163216

  • 向 entryBB 中插入生成随机数的指令和随机跳转,使其能够随机跳转到 bodyBB 或者 bodyBB 的克隆块。
  • 其中随机数指令我们可以使用 LLVM 的内置函数 rdrand:
    image-1693317355830

image-1693317366046

image-1693317393523

第四步 构造虚假随机跳转

  • 在 bodyBB 和 cloneBB 后插入虚假随机跳转指令(实际上仍会直接跳转到 endBB):
    image-1693317428698

image-1693317459418

#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Transforms/Utils/ValueMapper.h"
#include "llvm/Transforms/Utils/Cloning.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Instructions.h"
#include "SplitBasicBlock.h"
#include "llvm/IR/IRBuilder.h"
#include "Utils.h"
#include <vector>
#include <cstdlib>
#include <ctime> 
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/IntrinsicsX86.h"
#include "RandomControlFlow.h"
using namespace llvm;
using std::vector;
static cl::opt<int>obfuTimes("rcf_loop",cl::init(1),cl::desc("Obfuscate a function <bef_loop>time(s)."));
namespace{
    class RandomControlFlow: public FunctionPass{
        public:
            static char ID;
            RandomControlFlow():FunctionPass(ID){
                srand(time(NULL));
            }
            Value* alterVal(Value *origVar,BasicBlock *insertAfter);
            bool runOnFunction(Function &F);
            void insertRandomBranch(Value *randVar,BasicBlock *ifTrue,BasicBlock *ifFalse,BasicBlock *insertAfter);
            bool randcf(BasicBlock *entryBB);
    };
}
bool RandomControlFlow::runOnFunction(Function &F){
    INIT_CONTEXT(F);
    FunctionPass *pass=createSplitBasicBlockPass();
    pass->runOnFunction(F);
    for(int i=0;i<obfuTimes;i++){
        vector<BasicBlock*>origBB;
        for(BasicBlock &BB:F){
            origBB.push_back(&BB);
        }
        for(BasicBlock *BB:origBB){
            randcf(BB);
        }
    }
    return true;
}
//以基本块为单位进行随机控制流混淆
bool RandomControlFlow::randcf(BasicBlock *entryBB){
    //第一步 将基本块拆分
    BasicBlock *bodyBB=entryBB->splitBasicBlock(entryBB->getFirstNonPHI(),"entryBB");
    BasicBlock *endBB=bodyBB->splitBasicBlock(bodyBB->getTerminator(),"endBB");

    //第二步,对基本块进行克隆,并且修复逃逸变量
    BasicBlock *cloneBB=createCloneBasicBlock(bodyBB);

    //第三步,构建随机跳转
    *entryBB->getTerminator()->eraseFromParent();
    Function *randfunc=Intrinsic::getDeclaration(entryBB->getModule(),llvm::Intrinsic::x86_rdrand_32);      //获取32位随机数
    CallInst *callinst=CallInst::Create(randfunc->getFunctionType(),randfunc,"",entryBB);
    Value *randVar=ExtractValueInst::Create(callinst,0,"",entryBB);//将生成的随机数插入到 entryBB的尾部
    insertRandomBranch(randVar,bodyBB,cloneBB,entryBB);

    //第四步 bodyBB和cloneBB添加随机跳转

    bodyBB->getTerminator()->eraseFromParent();
    cloneBB->getTerminator()->eraseFromParent();
    insertRandomBranch(randVar,bodyBB,endBB,cloneBB);
    insertRandomBranch(randVar,endBB,cloneBB,bodyBB);


    
}
//插入随机跳转,随机数为randvar
//如果 randvar %2==1 就跳转到ifTrue  ,否则跳转到ifFalse;
void RandomControlFlow::insertRandomBranch(Value *randVar,BasicBlock *ifTrue,BasicBlock *ifFalse,BasicBlock *insertAfter){
    Value *alteredRandVar=alterVal(randVar,insertAfter);
    Value *randMod2=BinaryOperator::CreateURem(alteredRandVar,CONST_I32(2),"",insertAfter);
    CmpInst *cmp1=new ICmpInst(*insertAfter,ICmpInst::ICMP_EQ,randMod2,CONST_I32(0),"");
    BranchInst *branch=BranchInst::Create(ifTrue,ifFalse,cmp1,insertAfter);

}
//对变量进行恒等变换
Value* RandomControlFlow::alterVal(Value *startVar,BasicBlock *insertAfter){
    uint32_t code = rand() % 3;
  Value *result;
  if (code == 0) {
    // x = x * (x + 1) - x^2
    BinaryOperator *op1 = BinaryOperator::Create(Instruction::Add, startVar,
                                                 CONST_I32(1), "", insertAfter);
    BinaryOperator *op2 = BinaryOperator::Create(Instruction::Mul, startVar,
                                                 op1, "", insertAfter);
    BinaryOperator *op3 = BinaryOperator::Create(Instruction::Mul, startVar,
                                                 startVar, "", insertAfter);
    BinaryOperator *op4 =
        BinaryOperator::Create(Instruction::Sub, op2, op3, "", insertAfter);
    result = op4;
  } else if (code == 1) {
    // x = 3 * x * (x - 2) - 3 * x^2 + 7 * x
    BinaryOperator *op1 = BinaryOperator::Create(Instruction::Mul, startVar,
                                                 CONST_I32(3), "", insertAfter);
    BinaryOperator *op2 = BinaryOperator::Create(Instruction::Sub, startVar,
                                                 CONST_I32(2), "", insertAfter);
    BinaryOperator *op3 =
        BinaryOperator::Create(Instruction::Mul, op1, op2, "", insertAfter);
    BinaryOperator *op4 = BinaryOperator::Create(Instruction::Mul, startVar,
                                                 startVar, "", insertAfter);
    BinaryOperator *op5 = BinaryOperator::Create(Instruction::Mul, op4,
                                                 CONST_I32(3), "", insertAfter);
    BinaryOperator *op6 = BinaryOperator::Create(Instruction::Mul, startVar,
                                                 CONST_I32(7), "", insertAfter);
    BinaryOperator *op7 =
        BinaryOperator::Create(Instruction::Sub, op3, op5, "", insertAfter);
    BinaryOperator *op8 =
        BinaryOperator::Create(Instruction::Add, op6, op7, "", insertAfter);
    result = op8;
  } else if (code == 2) {
    // x = (x - 1) * (x + 3) - (x + 4) * (x - 3) - 9
    BinaryOperator *op1 = BinaryOperator::Create(Instruction::Sub, startVar,
                                                 CONST_I32(1), "", insertAfter);
    BinaryOperator *op2 = BinaryOperator::Create(Instruction::Add, startVar,
                                                 CONST_I32(3), "", insertAfter);
    BinaryOperator *op3 = BinaryOperator::Create(Instruction::Add, startVar,
                                                 CONST_I32(4), "", insertAfter);
    BinaryOperator *op4 = BinaryOperator::Create(Instruction::Sub, startVar,
                                                 CONST_I32(3), "", insertAfter);
    BinaryOperator *op5 =
        BinaryOperator::Create(Instruction::Mul, op1, op2, "", insertAfter);
    BinaryOperator *op6 =
        BinaryOperator::Create(Instruction::Mul, op3, op4, "", insertAfter);
    BinaryOperator *op7 =
        BinaryOperator::Create(Instruction::Sub, op5, op6, "", insertAfter);
    BinaryOperator *op8 = BinaryOperator::Create(Instruction::Sub, op7,
                                                 CONST_I32(9), "", insertAfter);
    result = op8;
  }
  return result;
}
FunctionPass *llvm::createRandomControlFlow() {
  return new RandomControlFlow();
}

char RandomControlFlow::ID = 0;
static RegisterPass<RandomControlFlow> X("rcf","My Random control flow obfuscation");

常量替代

  • 常量替代指将二元运算指令(如加法、减法、异或等等)中使用的常数,替换为等效而更复杂的表达式,以达到混淆计算过程或某些特殊常量的目的。

  • 例如将 TEA 加密中使用的常量 0x9e3779b 替换为 1216716715+1885832146-643678438。

  • 同指令替代,目前仅支持整数常量的替换,因为替换浮点数会造成舍入的错误和误差。且仅支持32位整数的替换,大家课后可以尝试拓展到任意位数整数的替换。

  • 常量替代可进一步拓展为常量数组的替代和字符串替代。

  • 常量数组替代可以抹去 AES, DES 等加密算法中特征数组,字符串替代可以防止攻击者通过字符串定位关键代码。

总体思路

  • 扫描所有指令,对目标指令(操作数类型为32位整数)进行替换:

image-1693385016916

  • 线性替换:val -> ax + by + c
  • 其中 val 为原常量 a, b 为随机常量 x, y 为随机全局变量 c = val - (ax + by)
  • 按位运算替换:val -> (x << 5 | y >> 3) ^ c
  • 其中 val 为原常量x, y 为随机全局变量 c = val ^ (x << 5 | y >> 3)
9

评论区