OLLVM移植
将ollvm移植到llvm-14.0.6
https://github.com/buffcow/ollvm-project
ollvm项目相关头文件
在llvm项目中的llvm/include/llvm/Transforms文件夹下创建Obfuscation目录,然后照着这个commit里面一个一个添加
ollvm项目相关源文件
在llvm项目中的llvm/lib/Transforms文件夹下创建Obfuscation目录,然后照着commit(llvm support obfuscator)里面一个一个添加
ollvm项目相关支持文件及配置
按照commit(llvm support obfuscator)对llvm项目中相关文件进行补充或修改
编译
编译release版本的
mkdir build
sudo cmake -S llvm -B build -G Ninja -DLLVM_ENABLE_PROJECTS="clang" -DCMAKE_BUILD_TYPE=Release -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_NEW_PASS_MANAGER=OFF
sudo cmake --build build -j4
编译完成后就跟可以查看一下build/bin文件夹
三个混淆功能
指令替换
随机选择一种功能上等效但更复杂的指令序列替换标准二元运算符。
-mllvm -sub:激活指令替换操作
-mllvm -sub_loop=3:如果该操作被激活,就在一个函数上应用它 3 次。默认值:1
算数运算:
a = b + c =>
a = b - (-c)
a = -(-b + (-c))
r = rand (); a = b + r; a = a + c; a = a - r
r = rand (); a = b - r; a = a + b; a = a + r
a = b - c =>
a = -(-b + c)
r = rand (); a = b + r; a = a - c; a = a - r
r = rand (); a = b - r; a = a - c; a = a + r
逻辑运算:
a = b & c =>
a = (b ^ ~c) & b
a = b | c =>
a = (b & c) | (b ^ c)
a = a ^ b =>
a = (~a & b) | (a & ~b)
这个功能效果会被ida的F5优化,下面我们重点看看源码Substitution.cpp。
可以看到Substitution继承自LLVM中的FunctionPass类
而runOnFunction是FunctionPass类的一个虚函数,Substitution覆盖runOnFunction来实现自己的逻辑。可以看到toObfuscate函数根据传入的 flag 和其他参数来决定是否应该混淆这个函数。如果决定混淆,就调用substitute方法来实际执行混淆逻辑
substitute
substitute代码如下
bool Substitution::substitute(Function *f) {
Function *tmp = f;
// Loop for the number of time we run the pass on the function
int times = ObfTimes;
do {
for (Function::iterator bb = tmp->begin(); bb != tmp->end(); ++bb) {
for (BasicBlock::iterator inst = bb->begin(); inst != bb->end(); ++inst) {
if (inst->isBinaryOp()) {
switch (inst->getOpcode()) {
case BinaryOperator::Add:
// case BinaryOperator::FAdd:
// Substitute with random add operation
(this->*funcAdd[llvm::cryptoutils->get_range(NUMBER_ADD_SUBST)])(
cast<BinaryOperator>(inst));
++Add;
break;
case BinaryOperator::Sub:
// case BinaryOperator::FSub:
// Substitute with random sub operation
(this->*funcSub[llvm::cryptoutils->get_range(NUMBER_SUB_SUBST)])(
cast<BinaryOperator>(inst));
++Sub;
break;
case BinaryOperator::Mul:
case BinaryOperator::FMul:
//++Mul;
break;
case BinaryOperator::UDiv:
case BinaryOperator::SDiv:
case BinaryOperator::FDiv:
//++Div;
break;
case BinaryOperator::URem:
case BinaryOperator::SRem:
case BinaryOperator::FRem:
//++Rem;
break;
case Instruction::Shl:
//++Shi;
break;
case Instruction::LShr:
//++Shi;
break;
case Instruction::AShr:
//++Shi;
break;
case Instruction::And:
(this->*funcAnd[llvm::cryptoutils->get_range(2)])(
cast<BinaryOperator>(inst));
++And;
break;
case Instruction::Or:
(this->*funcOr[llvm::cryptoutils->get_range(2)])(
cast<BinaryOperator>(inst));
++Or;
break;
case Instruction::Xor:
(this->*funcXor[llvm::cryptoutils->get_range(2)])(
cast<BinaryOperator>(inst));
++Xor;
break;
default:
break;
} // End switch
} // End isBinaryOp
} // End for basickblock
} // End for Function
} while (--times > 0); // for times
return false;
}
这里使用两个嵌套的循环来遍历函数中的所有基本块(BasicBlock)和每个基本块中的所有指令(Instruction)
补充 llvm pass操作对象结构
-
Module:c,cpp文件
-
Function:c,cpp代码中的函数。
-
BasicBlock:每个函数会被划分为若干基本块。一个block只有一个入口和一个出口。
-
Instruction:具体的指令
接着对于每种二元操作符,使用一个函数指针数组来存储不同的混淆策略,并随机选择一个策略来应用
虚假控制流
在程序的控制流图(CFG)中引入虚假的分支和基本块
-mllvm -bcf: 虚假控制流
-mllvm -bcf_loop=3: 表示要对基本块混淆这个操作连续进行3次
-mllvm -bcf_prob=40: 表示这些操作发生的概率为 40%
下面来一个例子体验一下
#include <stdlib.h>
int main(int argc, char** argv) {
int a = atoi(argv[1]);
if(a == 0)
return 1;
else
return 10;
return 0;
}
混淆前
混淆后
下面来分析一下源码BogusControlFlow.cpp
和符号替换一样,BogusControlFlow继承自FunctionPass
接着看向runOnFunction
这里同样调用toObfuscate函数来决定是否对函数F应用混淆,接着关注函数bogus和doF
bogus
bogus函数首先将本function的所有basicblock存放到一个list容器中
然后使用一个while循环调用addBogusFlow函数对选中的basicblock进行增加虚假控制流
addBogusFlow
接着进入addBogusFlow
先使用getFirstNonPHIOrDbgOrLifetime函数找到第一个既不是Phi指令、也不是Dbg指令、也不是Lifetime指令的指令的地址,然后调用splitBasicBlock函数根据上述指令的地址将一个basicblock一分为二
接着调用createAlteredBasicBlock函数对Original Basicblock进行拷贝生成一个名为"altered basicblock"(alteredBB)的basicblock,并对该basicblock加入一些垃圾指令
接着清除first basicblock和altered basicblock跟父节点的关系
设置跳转条件
接着创建了一个分支跳转指令(BranchInst).根据前面创建的恒为真的条件判断指令condition,如果条件为真(实际上总是为真),则跳转到原始基本块originalBB,如果条件为假(实际上不会出现这种情况,因为条件恒为真),则跳转到经过修改的基本块alteredBB
然后在alteredBB中设置了一个跳转指令,直接跳转到原始基本块originalBB
此时ir图如下
先获取获取到指向原始基本块末尾的迭代器i,在迭代器i所指向的指令位置前将原始基本块originalBB分割成两部分(OriginalBB和OriginalBBpart2)
然后关键的部分就是接着调用BranchInst的Create 方法创建一个分支跳转指令。传入的参数包括originalBBpart2、alteredBB、condition2以及originalBB。这样设置的目的是根据新创建的恒为真的条件判断指令condition2,如果条件为真(实际上由于设置为恒为真,总是为真),则跳转到originalBBpart2,如果条件为假(实际上不会出现这种情况),则跳转到 alteredBB。
ir控制流程图最终变为:
doF
遍历Module内的所有指令,并将所有的FCMP_TRUE分支指令替换为永真式(y<10 || x*x(x-1)%2 == 0)
控制流平坦化
把程序原有逻辑转化成多个case-swich的扁平状结构
-mllvm -fla: 激活控制流平坦化功能
-mllvm -split:激活基本块分裂
-mllvm -split_num=3: 使每个基本块上分裂3次
还是使用虚假控制流的示例代码
增加分裂次数
分析Flattening.cpp
开始还是一样的流程,转到runOnFunction函数
这里先调用createLowerSwitchPass去除switch,将switch结构换成if结构
下面关注到loopEntry(循环的入口基本块)和loopEnd(循环的出口基本块)
下面使创建loopEntry和loopEnd,且loopEntry,loopEnd基本块将在insert(firstBB)基本块之后插入到函数f的基本块序列中
接下来:
- 将insert指向的基本块移动到loopEntry之前,然后创建从insert到loopEntry和loopEnd到loopEntry的跳转指令
- 接着创建名为swDefault的基本块,并建立从swDefault到loopEnd的跳转
- 创建指令switchI,设置其条件为load,以根据load的值决定指令的跳转方向
- 移除函数f第一个基本块原有的终止器指令,然后创建从函数f到loopEntry第一个基本块的跳转指令
此时ir控制流程图就变成了这样
然后将保存在vector中的每一个basicblock都添加到switch-case语句中,每一个basicblock对应一个case,并且每个case的值都是一个随机值。同时修改每个basicblock块之间的跳转关系
最后ir的控制流程图就变成了大概下图的样子
ollvm移植到ndk
LLVM中的bin、lib、include目录覆盖NDK的toolchains/llvm/prebuilt/linux-86_64/目录下的bin,lib,include
在NDK目录下,将toolchains/llvm/prebuilt/linux-86_64/lib64目录下的clang文件夹覆盖toolchains/llvm/prebuilt/linux-86_64/lib目录下clang
在cpp目录下的CMakeLists.txt中添加混淆配置
例如:
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mllvm -bcf -mllvm -bcf_loop=3 -mllvm -fla -mllvm -split -mllvm -split_num=3")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mllvm -bcf -mllvm -bcf_loop=3 -mllvm -fla -mllvm -split -mllvm -split_num=3")