LLVM pass 实现 C++虚表保护

本文写于2019.12.21,LLVM9.0版本编译相关

超哥的程序分析与测试的大作业要求,基于LLVM实现一个具体的分析优化任务,于是选择了跟之前大作业相关的一个课题,C++的虚表保护。因为现在的编译器默认会把虚表编译到一个只读的程序段中,所以当虚表指针指向的内存是可写时,程序一定是被攻击了。基于这个思路想到利用LLVM在程序源码层面进行插桩,即在虚函数调用处检测虚表指针(对象中的第一个元素),如果指向的内存可写即退出。所以要完成的两件事情:

  • 找到虚函数调用处(插桩处)
  • 调用地址检测函数(要插的函数)

因为是第一次接触LLVM这种东西,所以开始根本不知道要实现的代码写在哪,也不知道要用到什么命令,什么工具才能完成以上的工作,又基本查不到特别相关的中文资料,很是崩溃。最终要感谢我的室友徐老师,让我大概明白,怎么完成以上工作。

  • 即如何找到插桩处,插什么函数:这部分逻辑写在LLVM的pass中,在LLVM源码文件夹中编写
  • 插的函数本身的逻辑:与LLVM工程无关,单独编写、编译成动态库,让目标程序在运行时加载即可

LLVM与Clang

LLVM的LOGO是一只龙,官网:https://llvm.org/,一堆英文字,密密麻麻,不知道说的啥。经过一段时间的了解,参考LLVM中文网,用人话说就是:LLVM是一个编译器框架,以C++写成。

image

可见,LLVM把编译过程分成了前端后端。这样当新出现一种语言时,只需要编写对应的前端,即可让其运行在各种指令集上。当出现一种新的指令集,只需要编写相应的后端,即可支持所有的前端语言。其工作原理,是把所有的前端语言处理成一种中间语言,即LLVM的IR(Intermediate Representation,译为中间表示),也可以理解为一种汇编。所以印证了那句名言:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决

Clang(读音:可浪/C浪,全称:a C language family frontend for LLVM)即为LLVM框架编译的C/C++语言的前端软件。所以也可以把Clang直接理解为一个编译器,对标gcc和g++:

image

而且其实在苹果电脑上使用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对于源码的处理方式有如下几种流程:

image

用法类似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:

  1. 首先早在http://releases.llvm.org/9.0.0/llvm-9.0.0.src.tar.xz下载LLVM工程源码
  2. 进入到源码目录新建一个build目录
  3. 执行cmake ../
  4. 执行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简介

其他参考:

虚函数调用时的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。

基本流程

  • 在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
  1. 首先用clang++的-emit-llvm和-c把c++源码编译成llvm不可读的字节码
  2. 然后用llvm编译好的opt工具,-load选项后接刚才编译好的LLVMHello.so,-hello选项(源自Hello.cpp的RegisterPass),调用Hello(Hello.cpp的struct Hello)这个FunctionPass,小于号后接输入的需要被pass优化的字节码文件(test.bc),大于号后接优化好的字节码文件(test_new.bc),由于这个简答的示例没有对IR进行什么插桩,所以后面结果也可以直接送给/dev/null
  3. 从输出可以看到,的确打印了所有的函数名,main函数,两个虚函数,以及一个类的构造函数
  4. 继续用clang++编译优化好的test_new.bc,输出二进制文件
  5. 这个二进制文件即使已经优化好的二进制了,不过我们这里其实并未对其进行任何优化,因为我们的pass仅仅是打印了函数名

所以我们回头在看一遍Hello.cpp,大概明白了其编写逻辑:

  1. 声明一个结构体Hello继承自FunctionPass
  2. 重写FunctionPass类的runOnFunction函数
  3. 函数体中的代码即可在编译时检测到每一个函数时执行
  4. 结构体Hello通过RegisterPass进行注册,X是啥不知道
  5. X函数的第一个参数是,之后opt工具要调用这个pass中结构体Hello的逻辑,需要的参数
  6. 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)

回忆总结

到目前为止我们学到:

  1. 如何安装编译LLVM
  2. 如何用clang编译代码
  3. IR理解

以及如何开发使用pass:

  1. 在LLVM源码目录中进行一系列操作(新建文件夹,编写cpp,编写配置文件)
  2. 重新make LLVM的源码,生成pass对应的动态链接库
  3. 利用opt工具挂载该库对clang编译好bytecode进行优化
  4. clang编译优化好的bytecode即可

那PASS到底怎么写呢?仅仅是一个Hello函数名显然没啥用。找到两篇中文的文章,比较入门的:

那比较复杂的功能比较多的,能实现我们想要的功能的PASS怎么写?应该用什么样的API?怎么用?这个问题我不知道怎么回答,因为我并没有找到官方关于pass高级开发的详细介绍,或者说我找的姿势不对,或者说我看不懂。也许这些API的用法在A First Function

我真的不知道这个东西应该怎么学,比较简单粗暴的办法是参考别人(github)上的相关代码,看看人家是怎么写的,我是参考我室友的。

C++虚表保护

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实现:

  • 找到虚函数调用点
  • 插桩检查函数

代码实现

这里说明一点,插桩函数时,仅仅是把函数的符号插进去,所以要插的函数的函数实现是需要编译一个动态链接库,在插桩完代码最后编译时链接上这个动态库,运行时让加载器找到这个动态库才算完成本次工作,完成后代码如下,非常简单:

image

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