LLVM

LLVM的核心思想是将编译器分为前端和后端两个部分,前端负责将源代码转换为中间表示LLVM Intermediate Representation (IR)(以库的形式提供接口),后端负责将中间表示转换为目标机器的汇编代码。

LLVM IR

三种表示:

  • .ll 格式:人类可以阅读的文本。
  • .bc 格式:适合机器存储的二进制文件。
  • 内存表示

LLVM Pass

https://llvm.org/docs/WritingAnLLVMPass.html#the-immutablepass-class

Pass就是“遍历一遍IR,可以同时对它做一些操作”的意思,Pass 是 中间 IR 处理环节的主要模块。
llvm pass的作用:

  • 插桩
  • 优化机器无关的代码

ModulePass (基于模块)

如果函数传递不需要任何模块或不可变传递,则模块传递可以使用接口的函数级传递(例如支配者)来提供检索分析结果的函数。

FunctionPass (基于函数)

CallGraphPass (基于调用图)

LoopPass (基于循环)

PASS 注册方式

1、修改 PassRegistry.def 和 PassBuilder.cpp 文件,直接追加 Pass 定义进去。
2、是用插件接口进行注册

LLVM Pass 实现「指令替代混淆」

步骤 操作
注册 通过 llvmGetPassPluginInfo() 注册
遍历 遍历 Function / BasicBlock
修改 进行 IR 控制流改造(添加跳转 / switch / 伪逻辑)

参考:https://3cly.github.io/2023/12/26/llvmpass%E5%AD%A6%E4%B9%A0%E5%8F%8A%E6%8C%87%E4%BB%A4%E6%9B%BF%E6%8D%A2/

  1. Pass 的注册方式:插件式注册,使用 llvmGetPassPluginInfo() 提供插件信息给 LLVM。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
    llvmGetPassPluginInfo() {
    return {LLVM_PLUGIN_API_VERSION, //llvm 版本号环境取值
    "mba-sub", //插件名字
    LLVM_VERSION_STRING, //插件版本号 都是随便传
    [](PassBuilder &PB) { // 第四个是一个回调,
    // 用于告诉 PassBuilder:当用户传了 "mba-sub" 时,就注册你自己的 MBASub Pass。
    PB.registerPipelineParsingCallback(
    [](StringRef Name, FunctionPassManager &FPM,
    ArrayRef<PassBuilder::PipelineElement>) {
    if (Name == "mba-sub") {
    FPM.addPass(MBASub());
    //FPM.addPass (MBASub ()) 这行代码向函数级别 Pass 管理器 FPM 中添加了一个 MBASub Pass 的实例。
    // 在这里,MBASub () 表示创建了 MBASub 结构体的一个实例 (默认构造函数),并将其添加到 Pass 管理器中
    return true;
    }
    return false;
    });
    }};
    }
1
opt -load-pass-plugin ./libMBASub.so -passes=mba-sub < input.ll > output.ll
  1. 定义 Pass 本体(MBASub)
    定义了一个结构体 MBASub,继承自llvm::PassInfoMixin<MBASub>

    1
    2
    3
    4
    5
    6
    7
    8
    struct MBASub : public llvm::PassInfoMixin<MBASub> {
    llvm::PreservedAnalyses run(llvm::Function &F, llvm::FunctionAnalysisManager &);
    //run() 是入口,遍历函数中所有基本块
    bool runOnBasicBlock(llvm::BasicBlock &B);
    //runOnBasicBlock() 实际处理每个基本块中的指令;
    static bool isRequired() { return true; }
    //isRequired() 返回 true 表示这个 Pass 默认启用(不是分析 Pass)。
    };
  2. 指令替换核心逻辑:将 a - b 替换为 (a + ~b) + 1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    bool MBASub::runOnBasicBlock(BasicBlock &BB) {
    for (auto Inst = BB.begin(); Inst != BB.end(); ++Inst) {
    auto *BinOp = dyn_cast<BinaryOperator>(Inst);
    //dyn_cast<BinaryOperator>:从通用 Instruction 类型动态转为二元操作类型;

    if (!BinOp) continue;

    unsigned Opcode = BinOp->getOpcode(); //getOpcode() 判断是不是 sub 指令;
    if (Opcode != Instruction::Sub || !BinOp->getType()->isIntegerTy())
    continue;

    IRBuilder<> Builder(BinOp); //IRBuilder<> 用于构造 IR 新指令;
    Instruction *NewValue = BinaryOperator::CreateAdd(
    Builder.CreateAdd(BinOp->getOperand(0),
    Builder.CreateNot(BinOp->getOperand(1))),
    ConstantInt::get(BinOp->getType(), 1));

    ReplaceInstWithInst(&BB, Inst, NewValue);
    //使用 ReplaceInstWithInst() 替换原始指令。
    }
    return Changed;
    }

ollvm

ollvm三种混淆模式的反混淆思路

虚假控制流 BCF (Bogus Control Flow)

ollvm_BCF 源码阅读

原理:

  • 虚假控制流混淆通过加入包含不透明谓词的条件跳转(永真or永假)和不可达的基本块,来干扰 IDA 的控制流分析和 F5 反汇编。

反混淆:
1、将全局变量赋值并将 segment 设为只读。

  • 对于常规的 ollvm 的 bcf 混淆来说,bcf 的不透明谓词都是处于 .bss段 中。
  • Edit->Segments->Edit segment 将 Write 复选框取消勾选, .bss段 就设为只读

2、d810

3、idapython patch 不透明谓词

指令替换(Instruction Substitution)

原理:

  • 用于将程序中的原始指令替换为等效但更难理解和还原的指令序列。

反混淆:
1、d810
2、GAMBA

控制流平坦化(FLA)

原理:

  • 通过一个主分发器来控制程序基本块的执行流程
  • 序言:函数的第一个执行的基本块
  • 主 (子) 分发器:控制程序跳转到下一个待执行的基本块
  • retn 块:函数出口
  • 真实块:混淆前的基本块,程序真正执行工作的块
  • 预处理器:跳转到主分发器

各块之间的规则:

  1. 函数的开始地址为序言的地址
  2. 序言的后继为主分发器
  3. 后继为主分发器的块为预处理器
  4. 后继为预处理器的块为真实块
  5. 无后继的块为retn块
  6. 剩下的为无用块

反混淆:
1、找到真实块。手撕;idapython 通过各个块之间的联系通过一定的规则找真实块;可以用 unicorn 或 angr 得到函数的 CFG, 利用规则匹配出真实块…
方法多种多样,但是核心都是找到真实块,除真实块和序言块外,其余的块都是虚假块,我们需要 NOP 掉他们。
2、得到真实块之间的联系。模拟执行pr真机调试打断点 trace。
3、用跳转汇编指令将每个真实块起来。

字符串加密

原理:

  • 编写一个pass将其中的字符串信息使用一些加密算法进行加密,然后特定的时间进行还原。一般含有字符串混淆、函数名混淆、不在init_array解密等。

反混淆:
(1)特征搜索:一般在so中可以直接搜索datadiv_decode,一般很多编写解密函数进行操作是这个函数,针对这种情况,一般可以通过frida hook就可以拿到解密后的值,然后进行patch
(2)init_array中解密:字符串解密操作在init_arrray中进行,一般可以通过模拟执行init_array,然后将解密后的字符串全部保存下来
(3)jni_onload解密:在jni_onload函数中进行解密操作,这时候就要进行inlinehook拿到解密后寄存器的值,也可以进行hook,也可以使用unicorn进行操作