普通视图

发现新文章,点击刷新页面。
昨天以前思泉|Jev0n

OLLVM混淆学习(1)——控制流平坦化(FLA)

作者 Jev0n
2022年7月8日 14:42

0x01 控制流平坦化基本介绍

控制流平坦化是指将正常程序控制流中基本块之间的跳转关练删除,用一个集中的主分发块来调度基本块的执行顺序。相当于把原有程序正常的逻辑改为一个循环嵌套一个switch的逻辑。

正常情况:

正常情况

控制流平坦化之后:

混淆后

控制流平坦化的基本结构如下:

image-20220708115813228

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

修改了程序的控制流,导致逆向分析人员不容易直接的理清程序执行流程,增加分析难度。

0x02 实现方式

本节以https://github.com/bluesadi/Pluto-Obfuscator/tree/kanxue 项目为基准进行分析。主要的代码实现分为五大块如下图所示:

image-20220708122724034

1. 保存原基本块

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

    // 将除入口块(第一个基本块)以外的基本块保存到一个 vector 容器中,便于后续处理
    // 首先保存所有基本块
    vector<BasicBlock*> origBB;
    for(BasicBlock &BB: F){
        origBB.push_back(&BB);
    }
    // 从vector中去除第一个基本块
    origBB.erase(origBB.begin());
    BasicBlock &entryBB = F.getEntryBlock();
    // 如果第一个基本块的末尾是条件跳转,单独分离
    if(BranchInst *br = dyn_cast<BranchInst>(entryBB.getTerminator())){
        if(br->isConditional()){
            BasicBlock *newBB = entryBB.splitBasicBlock(br, "newBB");
            origBB.insert(origBB.begin(), newBB);
        }
    }

2. 创建分发块和返回块

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

// 创建分发块和返回块
    BasicBlock *dispatchBB = BasicBlock::Create(*CONTEXT, "dispatchBB", &F, &entryBB);
    BasicBlock *returnBB = BasicBlock::Create(*CONTEXT, "returnBB", &F, &entryBB);
    BranchInst::Create(dispatchBB, returnBB);
    entryBB.moveBefore(dispatchBB);
    // 去除第一个基本块末尾的跳转
    entryBB.getTerminator()->eraseFromParent();
    // 使第一个基本块跳转到dispatchBB
    BranchInst *brDispatchBB = BranchInst::Create(dispatchBB, &entryBB);

3. 实现分发块调度

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

// 在入口块插入alloca和store指令创建并初始化switch变量,初始值为随机值
    int randNumCase = rand();
    AllocaInst *swVarPtr = new AllocaInst(TYPE_I32, 0, "swVar.ptr", brDispatchBB);
    new StoreInst(CONST_I32(randNumCase), swVarPtr, brDispatchBB);
    // 在分发块插入load指令读取switch变量
    LoadInst *swVar = new LoadInst(TYPE_I32, swVarPtr, "swVar", false, dispatchBB);
    // 在分发块插入switch指令实现基本块的调度
    BasicBlock *swDefault = BasicBlock::Create(*CONTEXT, "swDefault", &F, returnBB);
    BranchInst::Create(returnBB, swDefault);
    SwitchInst *swInst = SwitchInst::Create(swVar, swDefault, 0, dispatchBB);
    // 将原基本块插入到返回块之前,并分配case值
    for(BasicBlock *BB : origBB){
        BB->moveBefore(returnBB);
        swInst->addCase(CONST_I32(randNumCase), BB);
        randNumCase = rand();
    }

4. 实现调度变量自动调整

在每个原基本块最后添加修改 switch 要使用的变量值的指令,以便返回分发块之后,能够正确执行到下一个基本块。删除原基本块末尾的跳转,使其结束执行后跳转到返回块,这一步需要注意判断原基本块末尾跳转的语句。(类似于VMP3每一个handler的末尾指定下一个要跳转的handler)

     // 在每个基本块最后添加修改switch变量的指令和跳转到返回块的指令
    for(BasicBlock *BB : origBB){
        // retn BB
        if(BB->getTerminator()->getNumSuccessors() == 0){
            continue;
        }
        // 非条件跳转
        else if(BB->getTerminator()->getNumSuccessors() == 1){
            BasicBlock *sucBB = BB->getTerminator()->getSuccessor(0);
            BB->getTerminator()->eraseFromParent();
            ConstantInt *numCase = swInst->findCaseDest(sucBB);
            new StoreInst(numCase, swVarPtr, BB);
            BranchInst::Create(returnBB, BB);
        }
        // 条件跳转
        else if(BB->getTerminator()->getNumSuccessors() == 2){
            ConstantInt *numCaseTrue = swInst->findCaseDest(BB->getTerminator()->getSuccessor(0));
            ConstantInt *numCaseFalse = swInst->findCaseDest(BB->getTerminator()->getSuccessor(1));
            BranchInst *br = cast<BranchInst>(BB->getTerminator());
            SelectInst *sel = SelectInst::Create(br->getCondition(), numCaseTrue, numCaseFalse, "", BB->getTerminator());
            BB->getTerminator()->eraseFromParent();
            new StoreInst(sel, swVarPtr, BB);
            BranchInst::Create(returnBB, BB);
        }
    }

当原基本块出现switch-case等大于2个分支的情况时,我们可以在优化过程中使用lowerswitch将其变成只有2个及以下分支的状态,使用命令如下:

opt -lowerswitch -S TestProgram_orig.ll -o TestProgram_lowerswitch.ll

关于lowerswitch的使用也可以直接在代码中实现,该项目是在LLVM12.0.1中编译的,所以不能直接使用createLowerSwitchPass函数否则会导致崩溃,应该在Flattening中添加如下函数

void getAnalysisUsage(AnalysisUsage &AU) const override{
    errs() << "Require LowerSwitchPass\r\n";
    AU.addRequiredID(LowerSwitchID);
    FunctionPass::getAnalysisUsage(AU);
}

image-20220708144027160

5. 修复PHI指令和逃逸变量

PHI 指令的值由前驱块决定,平坦化后所有原基本块的前驱块都变成了分发块,因此 PHI 指令发生了损坏。

逃逸变量指在一个基本块中定义,并且在另一个基本块被引用的变量。在原程序中某些基本块可能引用之前某个基本块中的变量,平坦化后原基本块之间不存在确定的前后关系了(由分发块决定),因此某些变量的引用可能会损坏。

修复的方法是,将 PHI 指令和逃逸变量都转化为内存存取指令。

void fixStack(Function &F) {
    vector<PHINode*> origPHI;
    vector<Instruction*> origReg;
    BasicBlock &entryBB = F.getEntryBlock();
    // 搜索PHI指令和逃逸变量添加到对应vector容器
    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)){
                origReg.push_back(&I);
            }
        }
    }
    for(PHINode *PN : origPHI){
        DemotePHIToStack(PN, entryBB.getTerminator());
    }
    for(Instruction *I : origReg){
        DemoteRegToStack(*I, entryBB.getTerminator());
    }
}

0x03 参考链接

https://security.tencent.com/index.php/blog/msg/112

https://www.kanxue.com/book-88-2111.htm

https://github.com/bluesadi/Pluto-Obfuscator/blob/kanxue/Transforms/src/Flattening.cpp

https://github.com/obfuscator-llvm/obfuscator/blob/llvm-4.0/lib/Transforms/Obfuscation/Flattening.cpp

https://www.52pojie.cn/thread-1369130-1-1.html

OLLVM混淆学习(0)——环境搭建及混淆初体验

作者 Jev0n
2022年7月7日 16:09

0x01 LLVM环境搭建

该小节环境搭建和原版OLLVM混淆使用无关,仅为后续LLVM PASS编写做准备,如仅需要进行OLLVM混淆该小节可跳过

我选择的环境为Ubuntu 18.04、LLVM 12.0.1、CMake 3.16.6

先下载LLVM和Clang的源码并利用CMake进行源码编译

https://github.com/llvm/llvm-project/releases/tag/llvmorg-12.0.1

image-20220706151310564

image-20220706151325543

因为Ubuntu 18.04利用包管理器默认安装的CMake最高版本达不到LLVM编译需求,所以我们需要自行安装,具体命令如下:

wget http://www.cmake.org/files/v3.16/cmake-3.16.6.tar.gz
tar xf cmake-3.16.6.tar.gz
cd cmake-3.16.6
sudo apt-get install build-essential
sudo apt-get install libssl-dev
sudo chmod -R 777 cmake-3.16.6
./bootstrap
make
sudo make install
cmake --version

然后如下图创建build、llvm、clang目录,将llvm和clang对应源码放入目录中,后面来编写build.sh文件

image-20220706152104662

cd build
cmake -G "Unix Makefiles" -DLLVM_ENABLE_PROJECTS="clang" \
-DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD="X86" \
-DBUILD_SHARED_LIBS=On ../llvm
make
make install

最后给予build.sh执行权限并执行等待一段时间后就编译好了,可以通过clang --version进行验证是否编译成功,在编译的时候最好给虚拟机大一点的运行内存避免发生未知意外

LLVM编译基本命令

LLVM IR 有两种表现形式,一种是人类可阅读的文本形式,对应文件后缀为 .ll ;另一种是方便机器处理的二进制格式,对应文件后缀为 .bc 。

clang -S -emit-llvm hello.cpp -o hello.ll

clang -c -emit-llvm hello.cpp -o hello.bc

使用 opt 指令对 LLVM IR 进行优化

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

-load 加载特定的 LLVM Pass (集合)进行优化(通常为.so文件)
-hlw 是 LLVM Pass 中自定义的参数,用来指定使用哪个 Pass 进行优化

从 LLVM IR 到可执行文件中间还有一系列复杂的流程,Clang 帮助我们整合了这个过程:

clang hello_opt.ll -o hello

0x02 Obfuscator-LLVM环境搭建

为避免出现冲突,这一节我是专门拿了个新的Ubuntu虚拟机进行操作的

使用 nickdiego/ollvm-build这个docker环境进行OLLVM源码编译,docker的安装参考网络上教程即可

docker pull nickdiego/ollvm-build

然后将源码下载下来

git clone https://github.com/nickdiego/docker-ollvm.git
git clone -b llvm-4.0 https://github.com/obfuscator-llvm/obfuscator.git

在ollvm-build.sh的第150行添加DOCKER_CMD+=" -DLLVM_INCLUDE_TESTS=OFF"

image-20220707142910600

最后执行ollvm-build.sh

chmod 777 ollvm-build.sh
sudo ./ollvm-build.sh ../obfuscator

编译完成后在 obfuscator/build_release 目录执行指令创建硬链接

sudo ln ./bin/* /usr/bin/
clang --version

OLLVM基本用法

使用的demo(hello.cpp)

#include <stdio.h>
#include <stdlib.h>

int encryptFunc(int inputNum_1,int inputNum_2){
    int tmpNum_1 = 666, tmpNum_2 = 888, tmpNum_3 = 777;
    return tmpNum_1 ^ tmpNum_2 + tmpNum_3 * inputNum_1 - inputNum_2;
}

int main(int argc,char *argv[]){

    int printNum = 55;
    if (argc > 1)
    {
        printNum = encryptFunc(printNum, atoi(argv[1]));
    }else{
        printNum = encryptFunc(printNum, argc);
    }
    
    printf("Hello OLLVM %d\r\n", printNum);

    return 0;
}

控制流平坦化(Control Flow Flattening)

可用选项:

  • -mllvm -fla : 激活控制流平坦化
  • -mllvm -split : 激活基本块分割
  • -mllvm -split_num=3 : 指定基本块分割的数目
clang -mllvm -fla -mllvm -split -mllvm -split_num=3 hello.cpp -o hello_fla

image-20220707155342174

如果提示stdio.h头文件找不到可以尝试下载g++和gcc,如果提示stddef.h或者stdarg.h头文件找不到可以sudo find -L /usr -name "*stddef*" -type f将其复制到 /usr/include目录下

image-20220707155253075

虚假控制流(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 hello.cpp -o hello_bcf

image-20220707160139661

指令替换(Instruction Substitution)

可用选项:

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

image-20220707160516874

通过LLVM IR生成多平台可执行文件,以控制流平坦化为例

clang -mllvm -fla -mllvm -split -mllvm -split_num=3 -S -emit-llvm hello.cpp -o hello_fla.ll

# 切换至Windows(提前安装好clang)
clang hello_fla.ll -o hello_fla.exe

0x03 去混淆

可以参考TSRC的那篇文章使用符号执行脚本deflat的方式进行去混淆,对于指令替换可以使用D810这个IDA插件进行操作,详细的去混淆方法后续文章在深入讨论。

参考链接

https://www.cnblogs.com/jsdy/p/12689470.html

https://www.kanxue.com/book-section_list-88.htm

https://blog.wuxu92.com/stdargs.h-no-such-file-or-directory/

https://security.tencent.com/index.php/blog/msg/112

https://github.com/joydo/d810

去除Steam中Unity单机游戏的联网登录验证(Mono&IL2CPP)

作者 Jev0n
2022年1月16日 23:54

目前Unity的游戏主要分为Mono和IL2CPP这两种,最主要的区别就是看游戏目录,如果有Assembly-CSharp.dll这个Dll则该游戏使用的是Mono虚拟机,否则就是使用IL2CPP。

image-20220115215515860

steam游戏的本体可以在 xxx\steamapps\common\游戏名字\ 找到。

Mono

使用Mono虚拟机的unity游戏,它的核心逻辑都是写在Assembly-CSharp.dll这个Dll里,所以我们可以使用(dnspy)[https://github.com/dnSpy/dnSpy/releases/tag/v6.1.8]进行反编译,并且找到SteamManager这个类。

image-20220115234334490

然后修改Awake这个函数最方便的就是修改方法,如果不行就直接修改IL指令和后续的IL2CPP修改汇编类似。

image-20220115234927623

image-20220115234956156

image-20220115234722473

替换游戏目录下的Assembly-CSharp.dll(记得备份原来的)即可。

image-20220115234820221

IL2CPP

使用IL2CPP作为跨平台虚拟机就比Mono稍微麻烦一点,我主要使用到了(IL2CPPDumper)[https://github.com/Perfare/Il2CppDumper/releases/tag/v6.7.6]和IDA,先将游戏目录下的GameAssembly.dll(安卓的为libil2cpp.so)和游戏_data\il2cpp_data\Metadata\global-metadata.dat 复制到同一目录。

image-20220115224249794

执行命令行Il2CppDumper.exe ,得到几个json和il2cpp.h。

image-20220116234624063

使用IDA打开GameAssembly.dll执行ida_with_struct_py3.py(在Il2CppDumer目录下)脚本,分别载入以下json和h文件,即可获得该DLL的符号信息(感觉类似pdb的作用)。

image-20220115224315481

image-20220115230450065

同样找到SteamManager.Awake这个函数,直接Hex修改函数头使其直接返回,即可绕过验证。

image-20220115232106653

最后保存并替换原始的GameAssembly.dll(记得备份)。

image-20220115232205460

image-20220115232541421

声明

以上内容仅为Unity游戏逆向和Il2CppDumper的学习笔记,仅供学习参考,切勿用于违法违规行为

初探VMProtect 3.x虚拟保护壳原理(学习中。。。)

作者 Jev0n
2022年1月3日 21:27

本文应该算作个人学习VMP保护的笔记,所以内容较为空泛。详细的VMP虚拟保护壳的学习可以参考文末的参考资料。

0x01 外层壳保护

使用VMProtect保护后的程序添加了两个新节区

壳代码也是虚拟化过的

image-20211230185633948

寻找OEP的方法

  1. 通过对ZwProtectVirtualMemory下断,观察栈顶
  2. 对代码节下硬件执行断点(感觉略鸡肋,得知道原始函数在大致哪个范围
  3. 对mainCRTstartup内使用的一些API下断点(IsProcessorFeaturePresent, GetSystemTimeAsFileTime),然后回溯找到OEP

修复IAT表

虽然能看到程序使用了哪些API,却不能通过交叉引用来静态分析,因为VMP保护后的程序导入地址是运行时动态计算的

image-20211230211131287

参考源哥用Unicorn还原IAT表的文章

image-20211230213959428

mov     ebx, offset byte_407DD1
mov     ebx, [ebx+198694h]
lea     ebx, [ebx+44C25846h]
xchg    ebx, [esp+0]
retn
[407DD1 + 198694] + 44C25846 = IAT(MessageBoxW)

image-20211231104400935

0x02 代码混淆引擎

代码混淆引起所使用的指令都是不常见的指令,我们可以一眼就识别出来比如 rcr,bt,btc,sbb,lahf等。

image-20220102150822685

code2

block

0x03 虚拟化引擎

这一部分比较复杂,我主要参考的是这篇文章 ,以x86的VMP保护为讲解例子,如果启动了VMP加外层壳VMP的handle和混淆变异的代码会在.vmp1这个节区里,否则都在.vmp0节区。

进入虚拟机的标志是push uint32 加上 call function 跳转到.vmp1的节区进行操作,在大多数情况下这个call是不会返回的,更像是一个跳转。

img

其中这个push的32位数是虚拟opcode表起始位置加密后的值。

call进去后就开始依次执行每一个handle了,在每个handle里面都存在的大量的代码混淆阻碍逆向分析。

image-20220126212411013image-20220126213041291

原文中的例子
======================================================================
0x7ae901:    mov    ecx, dword ptr [esi]
0x7ae905:    lea    esi, [esi + 4]
0x7ae914:    movzx    eax, byte ptr [ebp]
0x83e3c2:    lea    ebp, [ebp + 1]
0x7d7bf8:    mov    dword ptr [esp + eax], ecx
...
0x8429bf:    add    edi, ecx
0x6d015e:    jmp    edi
======================================================================
0x755912:    mov    ecx, dword ptr [esi]
0x75591a:    lea    esi, [esi + 4]
0x755925:    movzx    eax, byte ptr [ebp]
0x6c94c6:    mov    dword ptr [esp + eax], ecx
0x6c94d7:    lea    ebp, [ebp + 4]
...
0x79cdbd:    add    edi, ecx
0x79cdbf:    push    edi
0x79cdc0:    ret    
======================================================================
0x7b821a:    mov    ecx, dword ptr [esi]
0x7b8222:    lea    esi, [esi + 4]
0x7b822b:    movzx    eax, byte ptr [ebp]
0x7b695c:    mov    dword ptr [esp + eax], ecx
0x7b6966:    lea    ebp, [ebp + 4]
...
0x7637cb:    add    edi, ecx
0x78cc6a:    jmp    edi

我们可以详细分析每个handle的实际作用,在这篇文章中较为详细的分析了VPUSH16 [VCTX + *] 这个handle具体实现方式

0x45bf82:    VUNKNOWN: (VIP = esi, VSP = ebp)

# update VIP to point on operand (current VIP is pointing on opcode offset)
0x45bf82:    lea    esi, [esi - 1]

# get the ciphered operand (1 byte)
0x45bf8c:    movzx    eax, byte ptr [esi]

# mutated operand decryption (keychain)
# NOTE : ebx contain the rolling key
0x45bf94:    xor    al, bl
0x45bf99:    ror    al, 1
0x40a4fa:    dec    al
0x40a505:    not    al
0x40a507:    dec    al
0x40a514:    xor    bl, al

# push a value into vm stack from vm context
# eax = 8; VCTX[8] -> [VSP-2] = VPUSH R8
0x40a51a:    movzx    dx, byte ptr [esp + eax]
0x40a51f:    sub    ebp, 2
0x40a529:    mov    word ptr [ebp], dx

# update VIP to the next ciphered opcode offset
0x40a531:    lea    esi, [esi - 4]

# get next ciphered opcode offset
0x40a537:    mov    ecx, dword ptr [esi]

# mutated next handle offset decryption routine (keychain)
# NOTE : ebx contain the rolling key
0x40a53e:    xor    ecx, ebx
0x438108:    sub    ecx, 0x5eac74dd
0x43810e:    cmc    
0x43810f:    not    ecx
0x41743d:    bswap    ecx
0x41743f:    rol    ecx, 1
0x4513d8:    neg    ecx
0x4513da:    stc    
0x4513db:    xor    ebx, ecx

# update absolute handle position with the next handle offset
0x4513e0:    add    edi, ecx

# reset the next rolling key operand
0x4752b4:    lea    ecx, [esp + 0x60]

# jump to the next handle
0x461417:    push    edi
0x461418:    ret    

VM的体系结构

VIP和VSP是存储在一个随机的寄存器中(register base),下图为VMP 上下文的包含关系图。

img

一条指令由两部分组成,一个加密的handle偏移量和它加密后的参数(操作码和操作数)

img

某段VMP例程以一个VENTER指令开始,以一个VEXIT指令结束,以下为常见的VM指令。

VENTER, VEXIT, VADDU*, VNANDU*, VNORU*, VPUSHV, VPOPR, VPOPVSP, VPUSHVSP, VPUSHI*
VFETCH*, VJUMP_*, VMOV*, VSHLU*, VSHRU*, VMULU*, VDIVU*, ....

0x04 参考资料

关于unicorn去搞VMP的iat那点事

Quick look around VMP 3.x

VMP导入表修复

(VMProtect 分析)跟着ida和WinDbg逛VirtualMachine

VM保护攻防

❌
❌