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节点的前驱块)。基本块分割后变成了两个子块分别是entry
和originalBB
,并且entry
通过直接跳转指令跳转到originalBB
。
此时的流程图如下
复制originalBB生成alteredBB#
调用createAlteredBasicBlock函数,此函数先通过 llvm::CloneBasicBlock
将originalBB
基本块copy一份为alteredBB
,因为其这个克隆并不是完全的克隆,需要对operands
,phi
和metadata
数据进行修复。
接着createAlteredBasicBlock函数会向alteredBB
虚假块中添加随机的垃圾指令
createAlteredBasicBlock函数调用完之后,清除alteredBB
和entry
两个基本块与后继块的关系,现在一个基本块就变为了三个游离的子块 entry
,originalBB
和alteredBB
。
此时的流程图如下
恒等条件连接entry和originalBB#
通过创建if(1 == 1)
的恒等条件和分支指令,设置分支指令当为true时entry
跳转到originalBB
, 否则跳转到alteredBB
。同时在alteredBB
的末尾创建直接跳转指令跳转到originalBB
。
此时的流程图如下
分割originalBB生成originalBBpart2#
获取originalBB
的terminator终端指令(末尾指令),并将其与originalBB
进行分割生成originalBBpart2
,最后清除originalBB
与originalBBpart2
的关系。
恒等条件连接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 loopEnd
和loopEnd 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
2021-02-23 代码段段间跳转流程