LLVM PASS PWN(一)
CISCN和强网就出了LLVM PASS PWN,笔者没有接触过就直接跳过了,但是随着pwn的难度越来越高LLVM PASS PWN在笔者看来出题的次数会越来越多,笔者写下这一系列文章来记录学习LLVM PASS PWN,如有错误欢迎指正
LLVM PASS PWN(一)
前置知识简述
为什么要编译程序
机器语言是用1和0组成的代码,但机器是识别不了1和0的,更具体的是如何识别的呢?对机器电路进行设计之后,机器能识别高电平还是低电平,刚好与2进制很相似,想输入0就给机器输入低电平,想输入1,就给机器输入高电平,所以就看到了1和0的表示形式
机器语言它是计算机唯一能识别和执行的语言,但它的直观性差,可读性差,比如一串11110000111100001111
机器可以快速识别是什么但是我们很难理解,再比如我们想要在屏幕上输出hello world那我们该如何用二进制来表示呢,所以汇编语言就诞生了
汇编语言用助记符来表示机器指令中的操作码和操作数的指令系统,如a = 1,我们不需要去用二进制来理解,我们完全可以利用mov a, 1进行理解,那有没有更简单的方法呢,比如现在要输出hello wrold,还是需要十几行的汇编代码的,所以高级语言就诞生了
高级语言是一种更接近人类的自然语言和数学语言的语言,比如想要a = 1,很直观就是a = 1,在很大程度上减少编程人员的编写量
但是问题来了,机器只懂0和1那怎么才能让高级语言被机器识别,所以就有了编译,将高级语言(源语言)翻译成汇编语言或机器语言(目标语言),编译的根本目的就是把源代码变成目标代码
编译的过程是什么
编译过程主要可以划分为前端与后端,笔者用一张图简述一下
前端把源代码翻译成IR,后端把IR编译成目标平台的机器码,这里笔者在查阅资料的时候发现有些会将生成中间代码放入前端,而有些资料会将生成中间代码放入后端
在词法分析中编译器读入源代码,经过词法分析器识别出Token,比如词法分析器中识别出的Token可以是int, return, {, }
等
在语法分析中会把上面的Token串给转换成一个抽象语法树AST,AST树反映了程序的语法结构
在语义分析中需要做的任务是理解语义,语句要做什么,如for是需要去实现循环,if是判断等
在前端完成之后,会生成中间代码,统一优化中间代码,再去将中间代码生成目标代码
前置知识这里笔者简述了一下,具体的可以移步编译原理
LLVM
LLVM IR & LLVM Pass
gcc
这个最经典的编译器提供的是一整套服务,前端和后端耦合在了一起,导致了如果一个新的编程语言出现可能需要设计一个新的IR以及实现这个IR的后端,如果出现了一个新的平台就要实现一个从自己的IR到新平台的后端,针对此类问题就出现了LLVM
不同的前后端使用统一的中间代码,这样一个新的编程语言出现只需要实现一个新的前端,如果出现了一个新的平台只需要实现一个新的后端
LLVM IR有三种表示形式
- 可读IR,类似汇编代码,可以给人看的,后缀
.ll
- 不可读二进制IR,后缀
.bc
- 保存在内存中,内存格式
LLVM Pass 是一个框架设计,是LLVM系统里重要的组成部分,因为LLVM Pass负责LLVM编译器绝大部分的工作,一系列的Pass组合,构建了编译器的转换和优化部分,抽象成结构化的编译器代码。
在实现上,LLVM的核心库中会给你一些 Pass类 去继承。你需要实现它的一些方法。 最后使用LLVM的编译器会把它翻译得到的IR传入Pass里,给你遍历和修改。
LLVM Pass的用处是插桩,机器无关的代码优化,静态分析,代码混淆等
LLVM 工具
以下内容来自LLVM Pass入门导引
llvm-as
:把LLVM IR从人类能看懂的文本格式汇编成二进制格式。注意:此处得到的不是目标平台的机器码。llvm-dis
:llvm-as
的逆过程,即反汇编。 不过这里的反汇编的对象是LLVM IR的二进制格式,而不是机器码。opt
:优化LLVM IR。输出新的LLVM IR。llc
:把LLVM IR编译成汇编码。需要用as
进一步得到机器码。lli
:解释执行LLVM IR。
Clang
Clang 是 LLVM 的前端,可以用来编译 C,C++,ObjectiveC 等语言。Clang 的功能包括:词法分析、语法分析、语义分析、生成中间中间代码LLVM Intermediate Representation (LLVM IR)。
LLVM & Clang环境安装 & 工具测试
ubuntu20.04下安装LLVM + Clang如下
sudo apt install clang-12
sudo apt install clang-8
sudo apt install llvm-12
sudo apt install llvm-8
llvm-12安装之后可以使用opt-12,今年的ciscn的LLVM PASS PWN就是opt-12,一般题目都会给出opt的版本。ubuntu20.04应该自带opt-10如果没有的话,sudo apt install clang-10 && sudo apt install llvm-10
上面的做题环境都安装完成之后,先写一个c文件,利用Clang将c文件编译成.ll, .bc
等格式看一下是否是如上所说,c文件如下
1 |
|
首先是.c->.ll
,clang-12 -emit-llvm -S test.c -o test.ll
,test.ll(生成的IR文本文件)如下
1 | ; ModuleID = 'test.c' |
上面的IR很直观,之前提到LLVM PASS的一个用处是优化IR代码,会将上面的可以优化的进行优化
其次是.c->.bc
,clang-12 -emit-llvm -c test.c -o test.bc
,bc是不可读二进制
然后是.ll -> .bc
,llvm-as test.ll -o test.bc
,结果和上面的一样
接着是.bc - > .ll
,llvm-dis test.bc -o test.ll
,同上
最后还有一个.bc -> .s
, llc test.bc -o test.s
,将字节码的二进制格式文件转换为本地的汇编文件
1 | .text |
编写第一个LLVM Pass
通过前面的知识之后,现在可以尝试编写“hello world”的pass,下面是官方的示例
1 |
|
先声明pass本身,然后声明了一个Hello
类,它是FunctionPass的子类。稍后将详细描述不同的内置pass子类,但是现在知道FunctionPass一次对一个函数进行操作。
然后声明了LLVM用于标识pass的pass标识符。 这允许LLVM避免使用昂贵的C ++运行时信息,如下
1 | static char ID; |
然后声明了一个runOnFunction方法,它覆盖了从FunctionPass继承的抽象虚方法。 这是我们应该做的事情,所以我们只用每个函数的名称打印出我们的消息。代码如下
1 | bool runOnFunction(Function &F) override { |
接着初始化passID。 LLVM使用ID的地址来标识pass,因此初始化值并不重要。代码如下
1 | char Hello::ID = 0; |
最后,我们注册我们的类Hello,给它一个命令行参数“hello”,并命名为“Hello World Pass”。 最后两个参数描述了它的行为:如果传递遍历CFG而不修改它,那么第三个参数设置为true; 如果pass是分析pass,例如支配树pass,则提供true作为第四个参数。代码如下
1 | static RegisterPass<Hello> X("hello", "Hello World Pass", |
如果我们想将通道注册为现有管道的一个步骤,则提供了一些扩展点,例如PassManagerBuilder::EP_EarlyAsPossible
在任何优化之前应用我们的通道,或者PassManagerBuilder::EP_FullLinkTimeOptimizationLast
在链接时间优化之后应用它。代码如下
1 | static llvm::RegisterStandardPasses Y( |
现在需要将这个Pass编译成模块,使用如下命令即可
1 | clang-12 `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared Hello.cpp -o LLVMHello.so `llvm-config --ldflags` |
现在应该会看到LLVMHello.so这个文件,通过官方文档可知需要使用以下命令
1 | opt -load LLVMHello.so -hello test.ll |
这里的 -hello由Hello.cpp中的static RegisterPass<Hello> X
参数决定
但是笔者这里报了一个错Error opening 'LLVMHello.so': LLVMHello.so: cannot open shared object file: No such file or directory
,这是因为linux无法在默认地址找到LLVMHello.so,解决很简单sudo cp LLVMHello.so /lib
成功输出test.c所有函数名称
对第一个LLVM Pass逆向分析
刚刚生成了LLVMHello.so这个pass文件,比赛题和上面也一样,会重写FunctionPass
类中的runOnFunction
函数,所以我们对上面的示例程序进行逆向分析,看一下虚表位置这样方便比赛的时候确定每个函数的位置
跟进RegisterPass
发现调用了callDefaultCtor进行对象创建,跟进它
给Hello对象分配了0x20个空间,跟进Hello
看到虚表了,直接跟进
runOnFunction
函数位于虚表中的最后一个位置,因为runOnFunction函数被我们重写了,所以它指向的是我们自定义的那个函数,比赛题的漏洞基本就是这个,所以在做LLVM Pass pwn的时候定位函数的位置可以从虚表入手
总结
收获很大,从编译过程到LLVM,加固了计算机底层的一些知识,知道了LLVM PASS PWN该怎么入手,以前看到LLVM PASS PWN的时候都不知道怎么运行(XD),这里第一篇就结束了,后面笔者会继续更新
Reference
https://zhuanlan.zhihu.com/p/130702001
https://zhuanlan.zhihu.com/p/122522485