ollvm编译环境搭建和源码调试分析

编译环境搭建

wsl + vscode + c++IntelliSense

下载源码后进入到llvm子目录中进行编译(wsl编译环境依赖自行设置), 这里使用的是生成makefile文件进行编译。

mkdir build_debug
cd build_debug
cmake -G "Unix Makefiles" -DLLVM_ENABLE_PROJECTS="clang"  ../  
make -j4

配置c_cpp_properties.json使用代码提示进行编写代码。

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "${workspaceFolder}/include/**"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64",
            "configurationProvider": "ms-vscode.makefile-tools"
        }
    ],
    "version": 4
}

配置launch.json使用gdb进行源码和pass代码的调试。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "(gdb) 启动",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build_debug/bin/opt",
            "args": ["-load","${workspaceFolder}/pass/build_debug/FirstPass/LLVMFirstPass.so","-firstpass","/mnt/e/llvmstudy/demo/demo.ll"],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "miDebuggerPath": "/usr/bin/gdb",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                },
                {
                    "description": "将反汇编风格设置为 Intel",
                    "text": "-gdb-set disassembly-flavor intel",
                    "ignoreFailures": true
                }
            ]
        }
    ]
}

这样就可以愉快的进行pass的编写和源码的调试了。但是发现c++ IntelliSense的代码提示太慢,可以换用clangd

wsl + vscode + clangd

cmake加上-DCMAKE_EXPORT_COMPILE_COMMANDS=1 生成compile_commands.json,cmake版本需要使用高版本才支持此选项。在clangd插件中设置compile_commands.json默认路径:--compile-commands-dir=${workspaceFolder}/build_debug/

mkdir build_debug
cd build_debug
cmake -G "Unix Makefiles" -DLLVM_ENABLE_PROJECTS="clang" -DCMAKE_EXPORT_COMPILE_COMMANDS=1 ../    
make -j4

wsl中需要安装clangd server,我的一直安装失败直接从github上下载压缩包https://github.com/clangd/clangd/releases,然后解压放在wsl的vscode的目录中/home/xxxx/.vscode-server/data/User/globalStorage/llvm-vs-code-extensions.vscode-clangd/install/clangd版本号/,记得修改bin目录中clangd的执行权限。

然后通过vscode的CodeLLDB插件利用lldb进行源码的调试,整体速度比使用c++ IntelliSense快多了。

bcf虚假控制流

ollvm虚假控制流提供了三个编译选项:激活虚假控制流-bcf,循环次数-bcf_loop,混淆概率-bcf_prob

原理就是将一个基本块拆分成三个基本块entry, OriginalBB, OriginalBBpart2,然后将OriginalBBpart2拷贝一份创建alteredBB基本块并加入垃圾指令。然后通过恒等的不透明谓词作为判断条件将包含程序原逻辑的块串联起来,然后通过永不执行的分支将虚假块alteredBB连接起来。图中①②恒成立,而③④⑤对应跳转指令永远不会被执行。

BogusControlFlow::bogus

pass的runOnFunction函数进行一些简单的判断之后先调用bogus函数。先是一个双层循环,外层循环由-bcf_loop设置的循环次数控制,内层循环其先调用isEHPad判断基本块是否包含异常处理相关指令,将所有不包含异常处理相关指令的基本块全部都保存到一个list列表中。


然后对列表中的每一个基本块都调用addBogusFlow函数。

BogusControlFlow::addBogusFlow

分割基本块生成entry和originalBB

通过调用getFirstNonPHIOrDbgOrLifetime获取第一个不是phi,dbg,lifetime的指令并使用splitBasicBlock进行代码块的分割(这样的目的是不改变phi节点的前驱块)。基本块分割后变成了两个子块分别是entryoriginalBB,并且entry通过直接跳转指令跳转到originalBB

此时的流程图如下

复制originalBB生成alteredBB

调用createAlteredBasicBlock函数,此函数先通过 llvm::CloneBasicBlockoriginalBB基本块copy一份为alteredBB,因为其这个克隆并不是完全的克隆,需要对operandsphimetadata数据进行修复。

接着createAlteredBasicBlock函数会向alteredBB虚假块中添加随机的垃圾指令

createAlteredBasicBlock函数调用完之后,清除alteredBBentry两个基本块与后继块的关系,现在一个基本块就变为了三个游离的子块 entryoriginalBBalteredBB

此时的流程图如下

恒等条件连接entry和originalBB

通过创建if(1 == 1)的恒等条件和分支指令,设置分支指令当为true时entry跳转到originalBB, 否则跳转到alteredBB。同时在alteredBB的末尾创建直接跳转指令跳转到originalBB

此时的流程图如下

分割originalBB生成originalBBpart2

获取originalBB的terminator终端指令(末尾指令),并将其与originalBB进行分割生成originalBBpart2,最后清除originalBBoriginalBBpart2的关系。

恒等条件连接originalBB和originalBBpart2

最后在originalBB的末尾再次创建一个恒等条件和分支指令,当为true时跳转到originalBBpart2,否则跳转到alteredBB

最后混淆后完整的流程图如下

BogusControlFlow::doF

doF函数通过创建两个初始化为0的全局变量x和y,然后将所有的FCMP_TRUE分支指令替换为恒等式y<10 || x*x(x-1)%2 ==0(不透明谓词)。这样ida就无法进行优化因为x,y为初始化为0的全局变量,所以保存在.bss段中,而全局变量是一个变量,可读可写,ida不能确定在程序运行中其是否会变化,所以不能用其初始值直接计算出表达式的结果。

fla控制流平坦化

ollvm控制流平坦化提供了三个编译选项:激活控制流平坦化-fla,激活基本块分割-split, 分割循环次数-split_num

原理是通过将程序的结构修改为switch case的形式并且不改变原有程序的逻辑。

pass的runOnFunction函数进行一些简单的判断之后先调用flatten函数。

保存所有的基本块

保存基本块的时候不保存包含异常处理指令的基本块,判断如果基本块数量小于等于1就返回false。

分割第一个基本块

将第一基本块从基本块列表中脱离。判断第一个基本块终端指令terminator是否为条件跳转指令,如果是条件跳转指令就调用splitBasicBlock将跳转指令与第一个基本块分离生成first

此时的流程图如下

构建switch case结构

  • 首先其创建一个switchVar状态变量,并创建store指令赋予他一个随机的值,
  • 创建loopEntry, loopEnd, switchDefault基本块。
  • 移动第一个基本块到loopEntry前,创建直接跳转指令switchDefault jmp loopEndloopEnd jmp loopEntry
  • loopEntry 中创建load switchVar指令和switch指令,默认分支为switchDefault
  • 清除第一个基本块和其后继块的关系(话说之前不是去过了?),创建直接跳转指令使第一个基本块jmp到loopEntry

switch case的结构如下,第一个基本块就是序言,loopEntry就是主分发器,loopEnd就是预处理器

将所有基本块加入case中

将所有的基本块移动到loopEnd的前面,然后将所有的基本块都加入到case中并设置一个随机的case值。

构建更新switchVar状态变量的指令

所有的基本块都加入到case中后需要恢复程序的逻辑,将原本通过条件判断跳转到目标基本块的逻辑修改为通过修改switchVar状态变量,借助构造的switch case结构跳转到目标基本块中。构造更新switchVar状态变量的指令的时候需要考虑三种情况。

  • 当前基本块的终端指令terminator的后继为0时,表示其是ret块程序不做处理。
  • 当前基本块的终端指令terminator为非条件跳转指令时,通过获取其目标块的case值,构建store指令对switchVar状态变量进行赋值。
  • 当前基本块的终端指令terminator为条件跳转指令时,通过获取其两个目标块的case值,构建select指令返回对应的case值,然后在对switchVar状态变量赋值

然后在所有的基本块最后创建直接跳转指令,所有的基本块都跳转到loopEnd

fixStack修复phi和变量

因为平坦化之后原程序基本块的前后关系完全打乱由switch case结构控制,所以基本块中的phi节点和一些变量的使用就会发生错误,需要修改为内存存取指令。

sub指令替换

ollvm的指令替换提供了两种编译选项:激活指令替换-sub,循环次数-sub_loop

pass的runOnFunction函数进行一些简单的判断之后调用substitute函数,

此函数有个三层循环,外层是-sub_loop循环次数控制,第2层会遍历所有基本块,最内层会遍历一个基本块的所有指令,对特定的指令从与之对应的函数数组中调用一个随机的指令变换函数。

例如add指令的一个变换函数为Substitution::addNeg,其将a + b变换为a - (-b)。因为指令变换往往要使用较多的指令替换原指令,所以会产生指令膨胀,如果设置了-sub_loop循环次数,那么循环次数越多指令膨胀越严重。

以上均为个人研究观点,仅供参考
参考链接:
https://sq.sf.163.com/blog/article/175307579596922880
https://jev0n.com/2022/07/08/ollvm-1.html

posted @ 2023-02-23 15:33  怎么可以吃突突  阅读(650)  评论(0编辑  收藏  举报