本文写于2019.12.21,LLVM9.0版本编译相关
超哥的程序分析与测试的大作业要求,基于LLVM实现一个具体的分析优化任务,于是选择了跟之前大作业相关的一个课题,C++的虚表保护。因为现在的编译器默认会把虚表编译到一个只读的程序段中,所以当虚表指针指向的内存是可写时,程序一定是被攻击了。基于这个思路想到利用LLVM在程序源码层面进行插桩,即在虚函数调用处检测虚表指针(对象中的第一个元素),如果指向的内存可写即退出。所以要完成的两件事情:
- 找到虚函数调用处(插桩处)
- 调用地址检测函数(要插的函数)
因为是第一次接触LLVM这种东西,所以开始根本不知道要实现的代码写在哪,也不知道要用到什么命令,什么工具才能完成以上的工作,又基本查不到特别相关的中文资料,很是崩溃。最终要感谢我的室友徐老师,让我大概明白,怎么完成以上工作。
- 即如何找到插桩处,插什么函数:这部分逻辑写在LLVM的pass中,在LLVM源码文件夹中编写
- 插的函数本身的逻辑:与LLVM工程无关,单独编写、编译成动态库,让目标程序在运行时加载即可
LLVM与Clang
LLVM的LOGO是一只龙,官网:https://llvm.org/,一堆英文字,密密麻麻,不知道说的啥。经过一段时间的了解,参考LLVM中文网,用人话说就是:LLVM是一个编译器框架,以C++写成。
可见,LLVM把编译过程分成了前端后端。这样当新出现一种语言时,只需要编写对应的前端,即可让其运行在各种指令集上。当出现一种新的指令集,只需要编写相应的后端,即可支持所有的前端语言。其工作原理,是把所有的前端语言处理成一种中间语言,即LLVM的IR(Intermediate Representation,译为中间表示),也可以理解为一种汇编。所以印证了那句名言:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决
Clang(读音:可浪/C浪,全称:a C language family frontend for LLVM)即为LLVM框架编译的C/C++语言的前端软件。所以也可以把Clang直接理解为一个编译器,对标gcc和g++:
而且其实在苹果电脑上使用gcc或者g++,就是调用的封装好的clang:
➜ ~ gcc --version
Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/usr/include/c++/4.2.1
Apple LLVM version 10.0.1 (clang-1001.0.46.4)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
➜ ~ g++ --version
Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/usr/include/c++/4.2.1
Apple LLVM version 10.0.1 (clang-1001.0.46.4)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
Clang对于源码的处理方式有如下几种流程:
用法类似gcc:
# 编译成可以阅读的IR
$clang hello.c -S -emit-llvm -o hello.ll
# 编译成IR的字节码bytecode
$clang hello.c -c -emit-llvm -o hello.bc
网友例子:
编译可执行文件
./clang test.c -o test
生成LLVM 字节码文件
./clang -O3 -emit-llvm test.c -c -o test.bc
生成LLVM 可视化字节码文件
./clang -O3 -emit-llvm test.c -S -o test.ll
运行可执行文件
./test
运行字节码文件
./lli test.bc
反汇编字节码文件
./llvm-dis < test.bc | less
编译字节码为汇编文件
./llc test.bc -o test.s
LLVM的安装与编译
安装哲学
我以为我是MAC用户自带Clang,就不各种安装编译,看来是想多了。LLVM是一个框架,其中包括clang在内有很多的软件和工具,而MAC就自带一个clang,仅仅一个clang是无法完成pass的编写的。所以还是要把整个框架安好。这里需要注意,编写pass需要在LLVM的源码下进行make,所以肯定是免不了编译源码的。当然一般在linux会选择用包管理工具安装软件,可惜这并不适用于本次任务,因为要编写pass。另外什么时候用apt什么时候用源码编译,知乎有个回答很哲学:
- 只是需要一个程序,不关心版本号:用 apt 装
- 只是需要一个程序,需要的版本 apt 里就有:用 apt 装
- 只是需要一个程序,需要的版本 apt 没有提供,但 PPA 里有:添加对应的 PPA,再用 apt 装
- 只是需要一个程序,需要的版本 apt 没有提供,PPA 里也没有:从源码自行编译安装
- 探索 Linux 世界的秘密:从源码自行编译安装
作者:「邱昊宇」
链接:https://www.zhihu.com/question/60299862/answer/175508859
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
二进制工具包
对于LLVM不建议用apt安装,因为安装的肯定不是最新版。而且如果不需要编写pass,只是需要整个LLVM框架的所有二进制工具,那么也可以直接下载对应操作系统的二进制文件包:http://releases.llvm.org/download.html选择Pre-Built Binaries即可。下载完在该文件夹下的bin目录可以看到LLVM+clang的所有二进制工具,这次我们完成编写pass主要用到clang++和opt这两个二进制工具:
➜ bin ls
bugpoint git-clang-format llvm-cxxmap llvm-rc
c-index-test hmaptool llvm-diff llvm-readelf
clang ld.lld llvm-dis llvm-readobj
clang++ ld64.lld llvm-dlltool llvm-rtdyld
clang-9 llc llvm-dwarfdump llvm-size
clang-apply-replacements lld llvm-dwp llvm-split
clang-change-namespace lld-link llvm-elfabi llvm-stress
clang-check lldb llvm-exegesis llvm-strings
clang-cl lldb-argdumper llvm-extract llvm-strip
clang-cpp lldb-instr llvm-jitlink llvm-symbolizer
clang-doc lldb-mi llvm-lib llvm-tblgen
clang-extdef-mapping lldb-server llvm-link llvm-undname
clang-format lldb-vscode llvm-lipo llvm-xray
clang-import-test lli llvm-lto modularize
clang-include-fixer llvm-addr2line llvm-lto2 obj2yaml
clang-offload-bundler llvm-ar llvm-mc opt
clang-query llvm-as llvm-mca sancov
clang-refactor llvm-bcanalyzer llvm-modextract sanstats
clang-rename llvm-c-test llvm-mt scan-build
clang-reorder-fields llvm-cat llvm-nm scan-view
clang-scan-deps llvm-cfi-verify llvm-objcopy verify-uselistorder
clang-tidy llvm-config llvm-objdump wasm-ld
clangd llvm-cov llvm-opt-report yaml2obj
diagtool llvm-cvtres llvm-pdbutil
dsymutil llvm-cxxdump llvm-profdata
find-all-symbols llvm-cxxfilt llvm-ranlib
源码编译
我是非常害怕编译的,这也是我这门课没退课的原因之一,因为战胜恐惧的最好办法就是面对它
上面的工具是LLVM+Clang,在http://releases.llvm.org/download.html这个下载链接中的源码栏,下载LLVM以及Clang,两个都编译完才有如上的所有工具,仅仅编译LLVM的源码是没有Clang编译器的,这俩东西是分开的,晕死。但是我自己的LLVM编译没问题,Clang编译会报错,不知道为什么。不过对于pass的编写只要能顺利编译LLVM就行,因为编译完的pass的是以动态链接库的形式存在于LLVM工程编译后的目录下,所以我是下载的已经编译好的clang。以下编译LLVM的工作在Ubuntu16.04的虚拟机完成,需要安装cmake:
- 首先早在http://releases.llvm.org/9.0.0/llvm-9.0.0.src.tar.xz下载LLVM工程源码
- 进入到源码目录新建一个build目录
- 执行
cmake ../
- 执行
make
make的过程会非常的慢,半个小时到一个小时左右。按照文档,其中cmake和make命令都可以加各种参数,但是我并不知道都是干啥的。其中make可以加-j选项增加处理器数量,我开始给虚拟机分配了6个核,所以用的-j6,但是make到98%就报错了,重新用make -j6到100报错,然后单纯用make会在78%左右重新编译某些库,过了20分钟编译成功,原因不详,总之编译成功后即可在build目录下的bin目录下看到已经编译好的二进制文件,可见并没有Clang:
➜ bin pwd
/home/xuanxuan/Desktop/llvm-9.0.0.src/build/bin
➜ bin ls
bugpoint llvm-mc
count llvm-mca
dsymutil llvm-microsoft-demangle-fuzzer
FileCheck llvm-modextract
llc llvm-mt
lli llvm-nm
lli-child-target llvm-objcopy
llvm-addr2line llvm-objdump
llvm-ar llvm-opt-fuzzer
llvm-as llvm-opt-report
llvm-bcanalyzer llvm-pdbutil
llvm-cat llvm-PerfectShuffle
llvm-cfi-verify llvm-profdata
llvm-config llvm-ranlib
llvm-cov llvm-rc
llvm-c-test llvm-readelf
llvm-cvtres llvm-readobj
llvm-cxxdump llvm-rtdyld
llvm-cxxfilt llvm-size
llvm-cxxmap llvm-special-case-list-fuzzer
llvm-diff llvm-split
llvm-dis llvm-stress
llvm-dlltool llvm-strings
llvm-dwarfdump llvm-strip
llvm-dwp llvm-symbolizer
llvm-elfabi llvm-tblgen
llvm-exegesis llvm-undname
llvm-extract llvm-xray
llvm-isel-fuzzer llvm-yaml-numeric-parser-fuzzer
llvm-itanium-demangle-fuzzer not
llvm-jitlink obj2yaml
llvm-lib opt
llvm-link sancov
llvm-lipo sanstats
llvm-lit verify-uselistorder
llvm-lto yaml2obj
llvm-lto2 yaml-bench
我们可以在build目录的lib目录下看到已经编译好的pass示例:
➜ lib pwd
/home/xuanxuan/Desktop/llvm-9.0.0.src/build/lib
➜ lib ls
LLVMHello.so
LLVMTestPass.so
...省略一堆...
顺便说一句,pass源码的路径是lib/Transforms
:
➜ Transforms pwd
/home/xuanxuan/Desktop/llvm-9.0.0.src/lib/Transforms
➜ Transforms ls -l
total 16
drwxr-xr-x@ 7 wangyuxuan staff 224 9 19 21:09 AggressiveInstCombine
-rw-r--r--@ 1 wangyuxuan staff 308 12 21 01:26 CMakeLists.txt
drwxr-xr-x@ 12 wangyuxuan staff 384 9 19 21:09 Coroutines
drwxr-xr-x@ 5 wangyuxuan staff 160 9 19 21:09 Hello
drwxr-xr-x@ 41 wangyuxuan staff 1312 9 19 21:09 IPO
drwxr-xr-x@ 20 wangyuxuan staff 640 9 19 21:09 InstCombine
drwxr-xr-x@ 23 wangyuxuan staff 736 9 19 21:09 Instrumentation
-rw-r--r--@ 1 wangyuxuan staff 822 1 19 2019 LLVMBuild.txt
drwxr-xr-x@ 19 wangyuxuan staff 608 9 19 21:09 ObjCARC
drwxr-xr-x@ 77 wangyuxuan staff 2464 9 19 21:09 Scalar
drwxr-xr-x 4 wangyuxuan staff 128 12 21 01:32 TestPass
drwxr-xr-x@ 64 wangyuxuan staff 2048 9 19 21:09 Utils
drwxr-xr-x@ 25 wangyuxuan staff 800 9 19 21:09 Vectorize
每个pass自己一个文件夹,在文件下下编写对应pass代码,然后回到build目录重新make,即可在build目录的lib目录下看到新的pass生成的动态链接库。后续会更细致的说明。另外如果是用github的源码编译应该是会编译完LLVM+Clang的,不会出现我这种情况:
总感觉开发就是绕来绕去的,因为你不知道人家怎么搞的,人家不仅仅是代码的逻辑,还有目录的逻辑,配置文件的逻辑。所以要按照人家的框架开发,你得知道:
- 代码写在哪(什么位置,什么文件名)
- 代码怎么写(框架语法)
- 配置文件在哪
- 配置文件如何修改
- 在哪用什么命令以进行编译或者运行
- 这个命令有没有什么配置文件或者运行选项
- 需不需要环境变量
- 需不需要预装其他工具
- 等等…
想要明白这些,如果没有一个完整的指导书,或者一个人手把手的教你,这真的是太难了。我真的不知道开发如何自学,而且热心网友的指导的文章可能是去年或者更久,方法可能已经失效了。
虽然还是官方文档最靠谱,但是LLVM的官网,一堆英语总是抓不住重点,无法聚焦,不知道该看哪。安装方式又各有不同,也不知道文档这句是说的哪种安装,总之我觉得这个文档真的乱七八糟的。或者说这个东西的功能太多了,我所要完成的工作仅仅是需要使用其中非常零碎的技术,所以寻觅这些技术好似大海里捞针。并且我是刚刚入门的新手,却要完成一个不太初级的工作,根本不知道应该看哪,该找啥。
IR
IR(Intermediate Representation,译为中间表示),理解为LLVM自己的汇编
语法简介
LLVM的优化都在IR层进行处理,之前说了,IR可以理解为LLVM自己的汇编,所以了解IR是必要的,官方文档:LLVM Language Reference Manual,写的比较简洁能看懂的中文介绍:LLVM IR简介
其他参考:
- A Tour to LLVM IR(上)
- A Tour to LLVM IR(下)
- llvm IR 语法小例子
- LLVM语言参考手册(开始至高级结构)
- llvm中间语言IR介绍
- LLVM每日谈之二 LLVM IR
- LLVM每日谈
虚函数调用时的IR
让我们来看看C++虚函数调用时,在IR层面上的代码是个什么样子吧!首先为我们准备一个C++带虚函数调用的源码test.cpp:
#include <stdio.h>
class FileDownload {
public:
virtual void check(){
printf("virtual check\n");
}
virtual void wget(){
printf("virtual wget\n");
}
};
int main(int argc, char* argv[]){
FileDownload f = FileDownload();
FileDownload * T = &f;
T->check();
T->wget();
return 0;
}
然后我们执行:
$ clang++ test.cpp -S -emit-llvm -o test.ll
查看test.ll,内容比较多,让我们关注main函数部分,我们一句一句解释:
# 定义一个返回类型是i32(4字节,对于源码中int),参数是i32和一个i8的二级指针(对应argc,argv)的main函数
define i32 @main(i32, i8**) #0 {
# 分配一个4字节的栈空间,4字节对齐(以下对齐省略),栈地址给%3寄存器,故%3的类型是i32*(星号表示指针)
%3 = alloca i32, align 4
# 同上
%4 = alloca i32, align 4
# 分配一个8字节的栈空间,栈上存的是一个i8类型的二级指针,因为64位下指针要占8个字节,这个指针解引用两次对应的是一个i8类型,故%5的类型是i8***
%5 = alloca i8**, align 8
# 分配一个8字节的占空间,存的是FileDownload这个类的一个实例化对象,因为这个对象就一个虚表指针而没有任何的成员,所以只占8个字节,故%6的类型是%class.FileDownload*
%6 = alloca %class.FileDownload, align 8
# 分配一个8字节的占空间,存的是FileDownload这个对象的一个指针,故%7的类型是%class.FileDownload**
%7 = alloca %class.FileDownload*, align 8
# 把立即数0存到%3这个栈空间上
store i32 0, i32* %3, align 4
# 把%0存到%4这个栈空间上
store i32 %0, i32* %4, align 4
# 把%1存到%5这个栈空间上
store i8** %1, i8*** %5, align 8
# 把%6的类型转成一个i8*的指针并赋值给%8
%8 = bitcast %class.FileDownload* %6 to i8*
# 调用memset把%8(%6)对应的栈空间内存清零
call void @llvm.memset.p0i8.i64(i8* align 8 %8, i8 0, i64 8, i1 false)
# 调用FileDownload构造函数把%8(%6)对应的栈空间初始化(给虚表指针赋值),调用完成后,%6指向的栈空间就会存在一个指向虚表的地址了
call void @_ZN12FileDownloadC1Ev(%class.FileDownload* %6) #5
# 把%6的值(一个栈地址),存到%7所指向的栈空间中
store %class.FileDownload* %6, %class.FileDownload** %7, align 8
# 把%7所对应的栈空间中的值取出来给%9,故%9的类型就算是FileDownload*(load就是解引用)
%9 = load %class.FileDownload*, %class.FileDownload** %7, align 8
# 把%9本身的类型(一个指向class.FileDownload的指针),转成(%class.FileDownload*)***赋值给%10
# (%class.FileDownload*)***后面三个星
# 先解一个引用,表示一个FileDownload的对象
# 再解一个引用,表示虚表
# 再解一个引用,表示第一个虚函数
# 括号里的(%class.FileDownload*)是this指针
%10 = bitcast %class.FileDownload* %9 to void (%class.FileDownload*)***
# 把%10所对应内存取出去给%11,所以%11就是FileDownload对象的内存内容,也就是虚表指针
%11 = load void (%class.FileDownload*)**, void (%class.FileDownload*)*** %10, align 8
# 把%11的偏移加0的地址给%12,故%12的类型和%11类型相同
%12 = getelementptr inbounds void (%class.FileDownload*)*, void (%class.FileDownload*)** %11, i64 0
# 把%12所对应的内存取出来给%13,所以%13就是第一个虚函数的地址
%13 = load void (%class.FileDownload*)*, void (%class.FileDownload*)** %12, align 8
# 调用%13,即第一个虚函数,参数是this
call void %13(%class.FileDownload* %9)
# 同上
%14 = load %class.FileDownload*, %class.FileDownload** %7, align 8
%15 = bitcast %class.FileDownload* %14 to void (%class.FileDownload*)***
%16 = load void (%class.FileDownload*)**, void (%class.FileDownload*)*** %15, align 8
%17 = getelementptr inbounds void (%class.FileDownload*)*, void (%class.FileDownload*)** %16, i64 1
%18 = load void (%class.FileDownload*)*, void (%class.FileDownload*)** %17, align 8
call void %18(%class.FileDownload* %14)
# 返回0
ret i32 0
}
对应的两次虚函数调用的部分,每次6句,指令序列是:load,bitcat,load,getelementptr,load,call。所以我们可以朴素的认为,按照这个指顺序执行的,应该就是一次虚函数调用了(这种方法被超哥上课狂喷,超哥说有更好的方法,然而我并不知道,先就按着这个来学习基本流程吧),所以我们已经可能找到了虚函数调用的特征,然后就是在这插函数了。
pass开发
LLVM是一个编译器框架,基本流程是:把所有的前端语言都转成IR然后进行优化,最后由IR编译到不同的平台上。可见很多的一些优化或者是分析都是在IR层面上做的,并且LLVM框架把优化IR的能力提供给了用户,用户可以基于LLVM自己的API,去在LLVM源码中编写自己的优化代码,最终可以编译成一个动态链接库,并利用相应的工具挂载上这个动态链接库去优化自己的IR,这个自己编写的优化代码就是PASS。
- pass开发官方文档:Writing an LLVM Pass
- pass开发官方demo中文版:快速开始(实现Hello World Pass)
- 历史各种版本的pass编译:LLVM Pass编写完,应该如何编译成 .so 文件?
基本流程
- 在LLVM的源码目录下的lib目录下新建一个文件夹作为自己要写的pass的工程目录Hello
- 进入这个目录编写CMakeLists.txt内容如下:
add_llvm_library( LLVMHello MODULE Hello.cpp PLUGIN_TOOL opt )
- 在这个目录下编写Hello.cpp
- 在父目录下修改CMakeLists.txt,最后一行添加add_subdirectory(Hello),注册自己pass
- 回到build目录下进行make即可生成对应的动态链接库文件,存在于bulid目录的lib目录下
好了,到目前为止我们已经知道了pass的代码应该写在哪了,配偶文件怎么写。那pass本身怎么写?生成的动态链接库又怎么用?马上就知道了:
关系疑问
不过开发工程的示例总是这样,不知道以上一堆的Hello是谁和谁关联的,我猜:
- 文件夹名的Hello,和父目录下的CMakeLists.txt注册的Hello是一个事
- Hello目录下的CMakeLists.txt中的Hello.cpp和本目录下的Hello.cpp是一个事
- Hello目录下的CMakeLists.txt中的LLVMHello决定了最后生成的动态链接库的名字
- Hello.cpp里面还有Hello类,和以上是否需要有关系不知道
Hello示例
在LLVM源码工程中lib/Transforms/Hello,可以看到刚才介绍的两个文件,这里还多了个Hello.exports,我这里是空的:
➜ Hello pwd
/Users/Desktop/llvm-9.0.0.src/lib/Transforms/Hello
➜ Hello ls
CMakeLists.txt Hello.cpp Hello.exports
查看CMakeLists.txt中的内容似乎比刚才介绍的内容要多,不过我们关心add_llvm_library这句,后面跟的名字是LLVMHello,那么应该在编译完之后生成LLVMHello.so,是不是这样呢?我们观察一下build/lib目录:
➜ lib pwd
/Users/Desktop/llvm-9.0.0.src/build/lib
➜ lib ls LLVMHello.so
LLVMHello.so
果然发现了这个文件,他有着什么样功能呢?这个动态库又怎么用呢?让我们来看看Hello.cpp的源码吧,这里去掉了注释:
#include "llvm/ADT/Statistic.h"
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
#define DEBUG_TYPE "hello"
STATISTIC(HelloCounter, "Counts number of functions greeted");
namespace {
// Hello - The first implementation, without getAnalysisUsage.
struct Hello : public FunctionPass {
static char ID; // Pass identification, replacement for typeid
Hello() : FunctionPass(ID) {}
bool runOnFunction(Function &F) override {
++HelloCounter;
errs() << "Hello: ";
errs().write_escaped(F.getName()) << '\n';
return false;
}
};
}
char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass");
虽然看不太懂为啥要这么写,但是估计大部分代码都是框架要求。重点看到了runOnFunction函数体中了两句跟cout长得很像的东西,感觉就是输出函数名:
errs() << "Hello: ";
errs().write_escaped(F.getName()) << '\n';
大概知道功能了,这个玩意怎么用呢?我们随便在自己目录下新建一个文件夹(不要在LLVM的源码下),然后随便写几个带好几个函数的c或者c++源码,这里我以我刚才的虚函数test.cpp为例,然后,重点来了:
这里我没配置环境变量,因为虚拟机的文件共享比较乱套,所以能更清楚看到我用的二进制文件在哪,clang++是下载的二进制预编译版,opt是LLVM目录下自己编译的
➜ ../clang+llvm/bin/clang++ -emit-llvm test.cpp -c -o test.bc
➜ ../llvm-9.0.0.src/build/bin/opt -load ../llvm-9.0.0.src/build/lib/LLVMHello.so -hello < test.bc > test_new.bc
Hello: main
Hello: _ZN12FileDownloadC2Ev
Hello: _ZN12FileDownload5checkEv
Hello: _ZN12FileDownload4wgetEv
➜ ../clang+llvm/bin/clang++ test_new.bc -o new
➜ ./new
virtual check
virtual wget
- 首先用clang++的-emit-llvm和-c把c++源码编译成llvm不可读的字节码
- 然后用llvm编译好的opt工具,-load选项后接刚才编译好的LLVMHello.so,-hello选项(源自Hello.cpp的RegisterPass),调用Hello(Hello.cpp的struct Hello)这个FunctionPass,小于号后接输入的需要被pass优化的字节码文件(test.bc),大于号后接优化好的字节码文件(test_new.bc),由于这个简答的示例没有对IR进行什么插桩,所以后面结果也可以直接送给/dev/null
- 从输出可以看到,的确打印了所有的函数名,main函数,两个虚函数,以及一个类的构造函数
- 继续用clang++编译优化好的test_new.bc,输出二进制文件
- 这个二进制文件即使已经优化好的二进制了,不过我们这里其实并未对其进行任何优化,因为我们的pass仅仅是打印了函数名
所以我们回头在看一遍Hello.cpp,大概明白了其编写逻辑:
- 声明一个结构体Hello继承自FunctionPass
- 重写FunctionPass类的runOnFunction函数
- 函数体中的代码即可在编译时检测到每一个函数时执行
- 结构体Hello通过RegisterPass进行注册,X是啥不知道
- X函数的第一个参数是,之后opt工具要调用这个pass中结构体Hello的逻辑,需要的参数
- X函数的第二个参数是啥?是–help的提示:
➜ hello ../llvm-9.0.0.src/build/bin/opt -load ../llvm-9.0.0.src/build/lib/LLVMHello.so --help | grep hello
--hello - Hello World Pass
--hello2 - Hello World Pass (with getAnalysisUsage implemented)
回忆总结
到目前为止我们学到:
- 如何安装编译LLVM
- 如何用clang编译代码
- IR理解
以及如何开发使用pass:
- 在LLVM源码目录中进行一系列操作(新建文件夹,编写cpp,编写配置文件)
- 重新make LLVM的源码,生成pass对应的动态链接库
- 利用opt工具挂载该库对clang编译好bytecode进行优化
- clang编译优化好的bytecode即可
那PASS到底怎么写呢?仅仅是一个Hello函数名显然没啥用。找到两篇中文的文章,比较入门的:
那比较复杂的功能比较多的,能实现我们想要的功能的PASS怎么写?应该用什么样的API?怎么用?这个问题我不知道怎么回答,因为我并没有找到官方关于pass高级开发的详细介绍,或者说我找的姿势不对,或者说我看不懂。也许这些API的用法在A First Function
我真的不知道这个东西应该怎么学,比较简单粗暴的办法是参考别人(github)上的相关代码,看看人家是怎么写的,我是参考我室友的。
C++虚表保护
基本想法
因为要完成作业,所以想了一种非常简单粗暴办法检查当虚函数调用时虚表指针是否指向了一个可写内存,在linux,可以直接去查看proc伪文件系统中的maps获得此信息:
➜ cat /proc/self/maps
00400000-0040c000 r-xp 00000000 08:01 130586 /bin/cat
0060b000-0060c000 r--p 0000b000 08:01 130586 /bin/cat
0060c000-0060d000 rw-p 0000c000 08:01 130586 /bin/cat
0244d000-0246e000 rw-p 00000000 00:00 0 [heap]
7f15a7858000-7f15a7b30000 r--p 00000000 08:01 400560 /usr/lib/locale/locale-archive
7f15a7b30000-7f15a7cf0000 r-xp 00000000 08:01 658358 /lib/x86_64-linux-gnu/libc-2.23.so
7f15a7cf0000-7f15a7ef0000 ---p 001c0000 08:01 658358 /lib/x86_64-linux-gnu/libc-2.23.so
7f15a7ef0000-7f15a7ef4000 r--p 001c0000 08:01 658358 /lib/x86_64-linux-gnu/libc-2.23.so
7f15a7ef4000-7f15a7ef6000 rw-p 001c4000 08:01 658358 /lib/x86_64-linux-gnu/libc-2.23.so
7f15a7ef6000-7f15a7efa000 rw-p 00000000 00:00 0
7f15a7efa000-7f15a7f20000 r-xp 00000000 08:01 658344 /lib/x86_64-linux-gnu/ld-2.23.so
7f15a80de000-7f15a8103000 rw-p 00000000 00:00 0
7f15a811f000-7f15a8120000 r--p 00025000 08:01 658344 /lib/x86_64-linux-gnu/ld-2.23.so
7f15a8120000-7f15a8121000 rw-p 00026000 08:01 658344 /lib/x86_64-linux-gnu/ld-2.23.so
7f15a8121000-7f15a8122000 rw-p 00000000 00:00 0
7ffd56b43000-7ffd56b64000 rw-p 00000000 00:00 0 [stack]
7ffd56b84000-7ffd56b87000 r--p 00000000 00:00 0 [vvar]
7ffd56b87000-7ffd56b89000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
所以想法就是在IR中找到虚函数调用点,特征刚才已经找到,然后插桩一个检查函数。所以就是如何编写pass实现:
- 找到虚函数调用点
- 插桩检查函数
代码实现
这里说明一点,插桩函数时,仅仅是把函数的符号插进去,所以要插的函数的函数实现是需要编译一个动态链接库,在插桩完代码最后编译时链接上这个动态库,运行时让加载器找到这个动态库才算完成本次工作,完成后代码如下,非常简单:
PASS编写
首先通过FunctionPass重写runOnFunction方法,并且使用Function和BasicBlock的迭代器循环找到每一条执行的语句
#define DEBUG_TYPE "hello"
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/ADT/Statistic.h"
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IRBuilder.h"
using namespace llvm;
namespace {
struct Hello : public FunctionPass {
static char ID; // Pass identification, replacement for typeid
Hello() : FunctionPass(ID) {}
int step = 0;
virtual bool runOnFunction(Function &F) {
Function *tmp = &F;
for (Function::iterator bb = tmp->begin(); bb != tmp->end(); ++bb) {
for (BasicBlock::iterator inst = bb->begin(); inst != bb->end(); ++inst) {
// insert your code 1
}
}
return false;
}
};
}
char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass");
然后利用简单的判断逻辑找到刚才的指令序列:
if (inst->getOpcode() == Instruction::Load) {
if(step==1){step++;}else{step=0;}
}else if(inst->getOpcode() == Instruction::BitCast) {
if(step==0){step++;}else{step=0;}
}else if (inst->getOpcode() == Instruction::GetElementPtr) {
//insert your code 2
}
插入函数
// 打印函数名
errs() <<"在"<<F.getName()<<"函数中"<< "发现虚函数调用点\n";
// 把迭代器当前迭代到的指令转换为GetElementPtr指令对象
GetElementPtrInst* sinst = dyn_cast<GetElementPtrInst>(inst);
// 获得这个对象的操作数,即虚表指针
Value* vtable_addr = sinst->getPointerOperand();
// 打印虚表指针
errs() <<"捕获到操作数getPointerOperand:"<<vtable_addr<<"\n";
// 创建一个IRBuilder,设置插桩点为当前指令的下一条
IRBuilder<> builder(inst->getNextNode());
// 创建一个虚表指针的类型对象,后面做参数类型用
Type *ltype = vtable_addr->getType();
// 创建参数类型向量
std::vector<Type*> paramTypes = {ltype};
// 创建返回类型,因为用的是FunctionPass,所以需要用F.getParent()找到父类Module
Type *retType = Type::getVoidTy(F.getParent()->getContext());
// 用函数参数,函数返回值创建一个函数类型
FunctionType *traceType = FunctionType::get(retType, paramTypes, true);
// 用F.getParent()找到父类Module,用Module的getOrInsertFunction方法创建插桩指令,函数名为trace,参数类型是traceType
// 如果用c++编写外部函数,则函数名要符合c++的规范,就是那种很长的带着一堆标识的,原因是C++的函数重载
FunctionCallee traceFunc = F.getParent()->getOrInsertFunction("trace",traceType);
// 设置实参为要检查的虚表指针
std::vector<Value*> args = {vtable_addr};
// 利用IRBuilder的实例化对象,插桩指令
builder.CreateCall(traceFunc, args);
所以完整代码如下:
#define DEBUG_TYPE "hello"
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/ADT/Statistic.h"
#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IRBuilder.h"
using namespace llvm;
namespace {
struct Hello : public FunctionPass {
static char ID; // Pass identification, replacement for typeid
Hello() : FunctionPass(ID) {}
int step = 0;
virtual bool runOnFunction(Function &F) {
Function *tmp = &F;
for (Function::iterator bb = tmp->begin(); bb != tmp->end(); ++bb) {
for (BasicBlock::iterator inst = bb->begin(); inst != bb->end(); ++inst) {
if (inst->getOpcode() == Instruction::Load) {
if(step==1){step++;}else{step=0;}
}else if(inst->getOpcode() == Instruction::BitCast) {
if(step==0){step++;}else{step=0;}
}else if (inst->getOpcode() == Instruction::GetElementPtr) {
if(step==2){
errs() <<"在"<<F.getName()<<"函数中"<< "发现虚函数调用点\n";
GetElementPtrInst* sinst = dyn_cast<GetElementPtrInst>(inst);
Value* vtable_addr = sinst->getPointerOperand();
errs() <<"捕获到操作数getPointerOperand:"<<vtable_addr<<"\n";
IRBuilder<> builder(inst->getNextNode());
Type *ltype = vtable_addr->getType();
std::vector<Type*> paramTypes = {ltype};
Type *retType = Type::getVoidTy(F.getParent()->getContext());
FunctionType *traceType = FunctionType::get(retType, paramTypes, true);
FunctionCallee traceFunc = F.getParent()->getOrInsertFunction("trace",traceType);
std::vector<Value*> args = {vtable_addr};
builder.CreateCall(traceFunc, args);
}else{step=0;}
}else{step=0;}
}
}
return false;
}
};
}
char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass");
外部函数
C函数实现:Linux下C程序检查内存是否可写
参考网友代码,写了一层一个参数无返回的函数封装writeable.c:
#include <stdio.h>
#include <unistd.h>
int check_mem_wrtieable(unsigned long addr)
{
int len = 8;
pid_t pid ;
char access, maps[32] , buff[1024];
unsigned long start_addr, end_addr, last_addr;
FILE *fmap;
pid = getpid();
sprintf(maps, "/proc/%d/maps", pid);
fmap = fopen(maps, "rb");
if(!fmap){
printf("open %s file failed!/n", maps);
return 0;
}
while(fgets(buff, sizeof(buff)-1, fmap) != NULL) {
sscanf(buff, "%lx-%lx %*c%c", &start_addr, &end_addr, &access);
if((addr >= start_addr) && (addr <= end_addr)){
if('w' != access){
fclose(fmap);
return 0;
}
if((addr + len) < end_addr){
fclose(fmap);
return 1;
}else {
last_addr = end_addr;
len = len - (end_addr - addr);
addr = last_addr;
}
}
}
fclose(fmap);
return 0;
}
void trace(unsigned long addr){
if(check_mem_wrtieable(addr)){
printf("%X\n",addr);
printf("虚表地址可写,有内鬼,终止交易\n");
exit(0);
}else{
printf("%X\n",addr);
printf("虚表地址不可写,正常\n");
}
}
编译方法
1. 在llvm源码目录下的build目录 make,编译好pass的动态链接库,然后回到我们的工程目录
2. ../clang+llvm/bin/clang++ -emit-llvm vtable.cpp -c -o vtable.bc
3. ./llvm-9.0.0.src/build/bin/opt -load ../llvm-9.0.0.src/build/lib/LLVMTestPass.so -hello < vtable.bc > vtable_new.bc
4. ../clang+llvm/bin/clang-9 -fPIC -shared writeable.c -o libtrace.so
5. ../clang+llvm/bin/clang++ vtable_new.bc -L. -ltrace -o out_new
6. sudo cp libtrace.so /usr/lib
7. ./out_new
验证
验证一个虚表被劫持的程序:
#include <stdio.h>
#include <unistd.h>
class FileDownload {
public:
virtual void check(){
printf("virtual check\n");
}
virtual void wget(){
printf("virtual wget\n");
}
};
int main(int argc, char* argv[]){
int state = 0;
FileDownload f = FileDownload();
FileDownload * T = &f;
printf("是否篡改虚表指针?\n");
scanf("%d",&state);
if(state) read(0,&f,8);
T->check();
T->wget();
printf("success\n");
return 0;
}
按照上述操作编译优化后输出out_new,gdb调试发现可写内存
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x00402000 r-xp /mnt/hgfs/桌面/hello/out_new
0x00601000 0x00602000 r--p /mnt/hgfs/桌面/hello/out_new
0x00602000 0x00603000 rw-p /mnt/hgfs/桌面/hello/out_new
0x00603000 0x00635000 rw-p [heap]
利用pwntools更改虚表指针为可写内存:
from pwn import *
context(log_level='debug')
io = process("./vtable")
payload = p64(0x601010)
io.recv()
io.sendline('1')
io.send(payload)
io.interactive()
程序检测到虚表指针可写:
➜ python exp.py
[+] Starting local process './vtable': pid 56095
[DEBUG] Received 0x1c bytes:
00000000 e6 98 af e5 90 a6 e7 af a1 e6 94 b9 e8 99 9a e8 │····│····│····│····│
00000010 a1 a8 e6 8c 87 e9 92 88 ef bc 9f 0a │····│····│····││
0000001c
[DEBUG] Sent 0x2 bytes:
'1\n'
[DEBUG] Sent 0x8 bytes:
00000000 10 10 60 00 00 00 00 00 │··`·│····││
00000008
[*] Switching to interactive mode
[*] Process './vtable' stopped with exit code 0 (pid 56095)
[DEBUG] Received 0x35 bytes:
00000000 36 30 31 30 31 30 0a e8 99 9a e8 a1 a8 e5 9c b0 │6010│10··│····│····│
00000010 e5 9d 80 e5 8f af e5 86 99 ef bc 8c e6 9c 89 e5 │····│····│····│····│
00000020 86 85 e9 ac bc ef bc 8c e7 bb 88 e6 ad a2 e4 ba │····│····│····│····│
00000030 a4 e6 98 93 0a │····│·│
00000035
601010
虚表地址可写,有内鬼,终止交易
[*] Got EOF while reading in interactiv