LLVM PASS安全漏洞分析与利用:从编译器插件到新型Pwn挑战

发布时间:2026/8/23 7:56:14
LLVM PASS安全漏洞分析与利用:从编译器插件到新型Pwn挑战 1. 从一道“奇怪”的题目说起为什么是LLVM PASS如果你和我一样是从传统的二进制安全、堆栈溢出这些领域摸爬滚打过来的第一次看到“LLVM PASS类pwn题”这个说法大概率会愣一下。Pwn题不都是给一个可执行文件让你找漏洞、写利用、拿shell吗怎么和LLVM、和编译器扯上关系了这玩意儿听起来更像是搞程序分析、代码混淆或者学术研究的人玩的。我第一次接触这类题目时也是这种感觉。题目给的不是一个./vuln二进制文件而是一个.cpp源文件或者一个.bc的LLVM字节码文件要求你写一个LLVM PASS去分析它甚至去“攻击”它。当时我的第一反应是这到底是要我pwn掉这个程序还是让我去写一个编译器插件后来折腾明白了才发现这类题目巧妙地将程序分析、编译器技术和漏洞利用结合在了一起开辟了一个非常有趣且富有挑战性的新领域。它考察的不仅仅是传统的漏洞挖掘和利用能力更是对程序底层表示、编译器工作流程乃至程序自动化分析的深刻理解。简单来说LLVM PASS类pwn题的核心是题目会给出一个目标程序通常是C/C源码或LLVM IR并提供一个或要求你编写一个运行在LLVM编译器框架下的PASS。这个PASS本意是用来分析或转换程序的但其中存在漏洞。你的目标就是利用这个PASS本身的漏洞去实现非预期的操作比如泄露敏感信息、执行任意代码或者篡改PASS的分析逻辑来“骗过”它。举个例子一个PASS可能被设计用来检测程序中的内存错误如Use-After-Free。攻击者可以通过精心构造的输入程序触发PASS自身处理逻辑中的漏洞例如一个缓冲区溢出从而控制PASS的执行流让这个本该“抓坏人”的警察反而被坏人控制了。这就像《三体》里的“思想钢印”打歪了或者安全软件自己被植入了木马讽刺意味和技术挑战性都拉满了。所以这类题目的入口门槛确实比传统pwn要高一些。你需要至少了解LLVM是什么IR长什么样PASS又是怎么运作的但别担心我们一步一步来。理解了这个范式你会发现它就像戴上了一副“编译器视角”的眼镜能看到程序更本质的一面对于理解软件安全、代码混淆、漏洞模式都大有裨益。2. 构建认知基石快速理解LLVM与PASS在真正动手“开pwn”之前我们必须花点时间打好地基。这部分内容可能有些枯燥但它是理解后续所有操作的关键。我会尽量用类比和实际例子来解释避免陷入复杂的学术定义。2.1 LLVM不只是编译器更是一个现代化基础设施你可以把传统的GCC编译器想象成一个巨大的、一体化的黑盒子。你把C代码塞进去它经过词法分析、语法分析、优化、代码生成等一系列固定流程最后吐出一个可执行文件。你想定制其中的某个环节非常困难几乎要动它的筋骨。而LLVM则采用了完全不同的模块化设计。它的核心是一个与具体编程语言和硬件架构都无关的中间表示也就是LLVM IR。你可以把LLVM IR想象成一种“通用汇编语言”或者“程序的标准电路图”。无论你的源程序是C、C、Rust还是其他语言前端编译器都会把它们翻译成这种统一的LLVM IR。然后一系列独立的、可插拔的模块也就是PASS会对这份“电路图”进行分析和优化。最后后端编译器再根据目标平台x86, ARM等将优化后的IR“电路图”翻译成具体的机器码。这种设计带来了巨大的灵活性语言无关为Swift、Rust等新语言开发编译器只需要写一个生成LLVM IR的前端即可后端优化和代码生成直接用现成的。优化灵活优化过程变成了对IR进行一系列变换的PASS可以像搭积木一样组合不同的优化策略。易于开发你想写一个程序分析工具不必从零开始解析C语法树直接写一个处理LLVM IR的PASS就行了LLVM已经帮你做好了所有繁重的基础工作。在安全领域LLVM的这种特性被广泛应用代码插桩写一个PASS在每一个内存分配和释放操作前后插入检查代码就成了一个内存调试器如AddressSanitizer的核心原理。控制流平坦化写一个PASS打乱程序原有的控制流结构用于代码混淆增加逆向难度。漏洞模式检测写一个PASS自动在IR中寻找可能产生缓冲区溢出、格式化字符串等漏洞的代码模式。而我们的“LLVM PASS类pwn题”正是抓住了“PASS本身也是程序”这一点。如果开发者在编写这些功能强大的PASS时自己引入了安全漏洞那么这个分析工具本身就变成了一个可被攻击的对象。2.2 PASS编译流水线上的“工人”或“质检员”在LLVM的编译流水线上PASS就是一个一个独立工作的“工人”或“质检员”。每个PASS都有明确的职责有的负责检查IR是否符合规范分析PASS有的负责对IR进行修改和优化转换PASS。一个PASS的生命周期大致如下获取单元PASS会接收到一个编译单元通常是一个Module包含全局变量、函数等或者更细粒度的Function函数、BasicBlock基本块、Instruction指令。遍历与分析PASS会遍历这些单元读取其中的信息。例如一个检测未初始化变量的PASS会遍历所有指令查看变量在定义前是否被使用。执行操作根据分析结果PASS可能会报告信息如发现漏洞也可能会修改IR如删除死代码、插入新的指令。传递结果PASS处理完后编译流程会继续将处理后的IR交给下一个PASS。从编程角度看编写一个PASS通常意味着继承LLVM提供的某个PASS基类如ModulePass,FunctionPass并重写其关键方法如runOnModule,runOnFunction。在这个方法里你就可以访问到IR的所有细节。2.3 LLVM IR程序的“骨骼清奇”的中间形态LLVM IR是理解一切的关键。它看起来像是一种低级的、带类型的汇编语言但比机器码更规整和富有信息。我们来看一个简单的C代码和对应的IR建立直观感受C代码:int add(int a, int b) { return a b; }LLVM IR (近似):define i32 add(i32 %a, i32 %b) { entry: %sum add i32 %a, %b ret i32 %sum }解读一下define i32 add(i32 %a, i32 %b)定义了一个名为add的函数返回类型是i3232位整数接受两个i32类型的参数%a和%b。entry:这是一个基本块的标签。基本块是一系列顺序执行、只有一个入口和一个出口的指令序列是控制流的基本单位。%sum add i32 %a, %b这是一条指令。将%a和%b相加结果存入新的寄存器%sum。注意IR使用的是静态单赋值形式每个变量寄存器只被赋值一次这极大简化了分析。ret i32 %sum返回指令。IR里充满了类似的信息全局变量、函数调用call、分支跳转br、内存加载存储load/store、指针运算getelementptr简称GEP这是重点和难点之一。PASS的工作就是遍历和操作这些指令和数据结构。对于pwn手来说我们关注IR的哪些方面呢数据结构的内存布局LLVM的IR对象如Instruction、Value在内存中是如何存放的它们的虚表指针在哪里这关系到我们能否在PASS中触发类似C对象的内存破坏。PASS对IR的访问接口PASS通过LLVM提供的API来获取IR信息。如果API使用不当比如未检查边界就直接解引用就可能成为漏洞点。IR本身作为“数据”题目给出的目标程序IR是PASS的输入“数据”。我们可以构造特殊的、畸形的IR来触发PASS处理逻辑中的边界条件错误。3. 漏洞在哪里PASS类题目的攻击面分析传统pwn题的漏洞存在于目标二进制程序中而PASS类题目的漏洞则存在于分析工具PASS本身。我们需要切换视角把PASS当作一个独立的、处理特定格式LLVM IR输入的程序来审计。它的攻击面主要集中在以下几个方面3.1 内存安全漏洞古老的敌人新的战场这是最经典、也最直接的漏洞类型。虽然PASS通常用C编写享受着现代语言的便利但程序员的手误和逻辑缺陷依然存在。缓冲区溢出PASS在解析或处理IR中的字符串如函数名、全局变量名、元数据时可能使用固定大小的栈上缓冲区而没有进行边界检查。如果IR中的标识符名称异常的长就可能覆盖栈上的返回地址或函数指针。示例场景一个PASS遍历所有函数并将函数名打印到日志。它使用char name_buffer[256]和sprintf。攻击者可以在IR中定义一个名字长度为300的函数。整数溢出与越界访问PASS在处理IR中数组、结构体等聚合类型时需要计算索引或偏移。如果对来自IR的索引值缺乏校验可能导致计算出的偏移量溢出进而访问到非法内存。示例场景一个PASS要分析一个全局数组的访问模式。它从IR中读取数组索引一个ConstantInt并直接用于访问一个内部的分析数组。攻击者可以构造一个索引值为-1或远超数组大小的常数。释放后重用与双重释放LLVM IR对象Value及其子类由LLVM的内存管理系统管理。PASS中如果错误地手动管理了某些对象的生命周期或者持有了一些本不该持有的指针就可能引发UAF/DF。这类漏洞在复杂的、涉及IR修改的PASS中更可能出现。类型混淆LLVM IR是强类型的。PASS通过dyn_cast或cast进行类型转换。如果开发者过于自信使用了不安全的强制转换cast而IR中的实际类型并非预期就会导致将一种类型的对象当作另一种类型来处理访问错误的虚函数表或成员变量。3.2 逻辑漏洞与设计缺陷更隐蔽的突破口这类漏洞不直接导致内存破坏但能让你“欺骗”PASS使其得出错误结论或执行非预期操作。符号执行或约束求解的绕过一些高级的PASS会进行符号执行或调用约束求解器如Z3来分析路径条件。如果PASS对求解器的结果过于信任或者约束条件设置不完整攻击者可以构造特殊的IR使求解器返回一个符合PASS检查条件、但实际上在真实执行时会导致漏洞的“模型”。类比这就像你通过了一门开卷考试但你的答案是基于一本错误的参考书得出的考官PASS只看你引用了书却没发现书是错的。状态机混乱一些PASS会维护一个内部状态机来跟踪程序的分析状态如变量是否初始化、锁是否持有。攻击者可能通过构造复杂的控制流如不可达的基本块、循环使PASS的状态机进入非预期状态从而导致误报将安全代码判为危险或漏报放过了危险代码。依赖分析错误PASS在分析数据依赖或控制依赖时出错。例如一个检测数据竞争Data Race的PASS可能错误地认为两个内存访问操作是顺序执行的而实际上它们可能并发执行。攻击者可以构造特定的IR指令顺序来诱发这种错误分析。3.3 外部依赖与序列化漏洞配置文件/输入文件解析PASS有时会从外部文件读取配置如要忽略的函数列表、规则库。如果解析逻辑存在漏洞如XML外部实体注入、YAML反序列化攻击面就延伸到了文件系统。PASS间数据传递一些PASS会依赖前一个PASS的分析结果。如果攻击者能影响前一个PASS的输出例如通过漏洞或者当前PASS无条件信任这些输入就可能产生连锁反应。理解这些攻击面后我们在审题和审计PASS代码时就有了明确的寻找方向关注PASS如何处理来自IR的、不受信任的输入关注所有涉及内存操作和类型转换的代码关注PASS的核心分析逻辑是否有被绕过的可能。4. 实战环境搭建与工具链准备工欲善其事必先利其器。玩转LLVM PASS pwn你需要一个既能编译运行PASS又能进行漏洞利用调试的环境。下面是我推荐的一套配置在Ubuntu 20.04/22.04上亲测有效。4.1 基础LLVM开发环境搭建我们不一定要从源码完整编译LLVM那太耗时了但需要安装包含开发文件头文件、库文件的LLVM套件。# 对于 Ubuntu 22.04 (Jammy) wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo add-apt-repository deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main sudo apt update sudo apt install llvm-17 llvm-17-dev clang-17 libclang-17-dev lld-17 # 设置默认版本可选但建议 sudo update-alternatives --install /usr/bin/llvm-config llvm-config /usr/bin/llvm-config-17 100 sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 100安装完成后验证一下llvm-config-17 --version # 应输出 17.x.x clang-17 --version # 应包含 LLVM 17.x.x关键组件说明llvm-17-dev: 包含开发LLVM PASS所需的所有头文件.h和静态库.a。llvm-config-17: 核心工具用于获取LLVM的编译和链接参数如--cxxflags,--ldflags,--libs。clang-17: 我们将用它把C代码编译成LLVM IR.ll或.bc文件。4.2 编写你的第一个PASSHello World让我们创建一个最简单的PASS来感受一下。这个PASS什么也不做只是打印出模块中每个函数的名称。1. 创建项目目录和文件mkdir HelloPass cd HelloPass touch HelloPass.cpp2. 编写HelloPass.cpp#include llvm/Pass.h #include llvm/IR/Function.h #include llvm/Support/raw_ostream.h using namespace llvm; namespace { // 1. 定义我们的PASS类继承自FunctionPass struct HelloPass : public FunctionPass { // 2. 声明一个静态的Pass IDLLVM内部机制需要 static char ID; // 3. 构造函数初始化基类并传递Pass ID HelloPass() : FunctionPass(ID) {} // 4. 重写runOnFunction方法这是PASS的逻辑入口 bool runOnFunction(Function F) override { // 5. 使用llvm::errs()输出类似std::cerr errs() Hello from function: F.getName() !\n; // 6. 返回false表示我们没有修改这个Function return false; } }; } // 7. 初始化Pass ID char HelloPass::ID 0; // 8. 注册PASS让opt工具能够识别和加载它 static RegisterPassHelloPass X(hello, Hello World Pass, false, false);3. 编译PASS为共享库我们需要将C代码编译成一个动态库.so文件这样LLVM的opt工具才能加载它。# 使用llvm-config获取正确的编译和链接参数 clang-17 -shared -fPIC llvm-config-17 --cxxflags HelloPass.cpp -o HelloPass.so llvm-config-17 --ldflags llvm-config-17 --libs core-shared -fPIC: 生成位置无关代码的动态库。llvm-config-17 --cxxflags: 获取必要的编译器标志如包含路径、宏定义。llvm-config-17 --ldflags --libs core: 获取链接器标志和核心LLVM库。core库包含了我们需要的Pass、Function、IR等基础类。4. 准备一个测试程序创建一个简单的C文件test.c:#include stdio.h void foo() { printf(In foo\n); } int main() { foo(); return 0; }5. 将C代码编译为LLVM IR文本格式clang-17 -S -emit-llvm test.c -o test.ll现在你可以用文本编辑器打开test.ll看看IR长什么样。6. 使用opt工具加载并运行我们的PASSopt-17 -load ./HelloPass.so -hello test.ll /dev/null-load ./HelloPass.so: 加载我们编译好的PASS共享库。-hello: 这是我们注册PASS时指定的命令行参数名RegisterPassHelloPass X(hello, ...)。 test.ll: 将IR文件作为输入。 /dev/null: 将输出IR重定向到空设备因为我们只关心PASS打印的信息。如果一切顺利你会在终端看到类似输出Hello from function: foo! Hello from function: main!恭喜你已经成功创建并运行了第一个LLVM PASS。这个流程是后续所有PASS开发、分析和攻击的基础。4.3 调试与分析工具链漏洞利用离不开调试。除了经典的GDB我们还需要一些LLVM专属工具。GDB/Pwndbg调试PASS本身。因为PASS是作为一个共享库被opt加载的所以我们需要调试opt进程。gdb --args opt-17 -load ./VulnerablePass.so -vuln-pass input.ll在GDB中你可以在PASS的代码里设置断点。注意PASS的符号可能没有被完全加载有时需要手动通过函数地址下断点。LLVM IR 查看与操作工具opt-17: 核心工具用于加载PASS并运行在IR上。-S参数可以输出可读的IR文本。llvm-dis-17: 将LLVM位码.bc二进制格式反汇编为可读的IR文本.ll。llvm-as-17: 将IR文本.ll汇编为位码.bc。clang -emit-llvm -S -c: 将C/C源码编译为IR文本。llvm-extract-17: 从模块中提取指定的函数或全局变量。llvm-link-17: 链接多个LLVM模块。脚本辅助Python的pwntools在构造复杂IR数据时非常有用。你可以用Python生成包含特定模式如超长字符串、特殊数值的IR文件作为PASS的输入。5. 从理论到实战一个模拟漏洞的完整利用过程现在让我们假设一个存在漏洞的PASS并走一遍从分析到利用的完整流程。这是理解此类题目精髓的关键。5.1 漏洞PASS代码审计假设我们有一个名为SimpleCheckerPass的PASS它的目的是检查函数中是否使用了“不安全的”函数如gets。代码如下已简化并植入漏洞// VulnerablePass.cpp #include llvm/Pass.h #include llvm/IR/Function.h #include llvm/IR/Instructions.h #include llvm/IR/InstVisitor.h #include llvm/Support/raw_ostream.h #include cstring // 漏洞来源 using namespace llvm; namespace { struct SimpleCheckerPass : public FunctionPass { static char ID; SimpleCheckerPass() : FunctionPass(ID) {} bool runOnFunction(Function F) override { // 漏洞点使用固定大小的栈缓冲区存储函数名 char funcNameBuffer[64]; const char* unsafeFuncs[] {gets, strcpy, sprintf}; const int numUnsafeFuncs 3; for (BasicBlock BB : F) { for (Instruction I : BB) { // 检查是否是调用指令 if (CallInst *CI dyn_castCallInst(I)) { // 获取被调用函数 Function *Callee CI-getCalledFunction(); if (Callee Callee-hasName()) { // 危险操作将函数名复制到小缓冲区未检查长度 strcpy(funcNameBuffer, Callee-getName().data()); // 检查是否为不安全函数 for (int i 0; i numUnsafeFuncs; i) { if (strcmp(funcNameBuffer, unsafeFuncs[i]) 0) { errs() [!] Found unsafe function call: funcNameBuffer in F.getName() \n; } } } } } } return false; } }; } char SimpleCheckerPass::ID 0; static RegisterPassSimpleCheckerPass X(simple-checker, A simple unsafe function checker (with vuln), false, false);漏洞分析 这个PASS的意图是好的但第20行strcpy(funcNameBuffer, Callee-getName().data());是典型的栈缓冲区溢出漏洞。Callee-getName()返回一个StringRef其.data()方法返回的C风格字符串长度未知。如果IR中某个被调用函数的名称长度超过63个字符funcNameBuffer大小为64需留一个给空字符strcpy就会覆盖栈上funcNameBuffer之后的数据包括可能的返回地址。5.2 构造恶意输入IR我们的目标构造一个LLVM IR文件其中包含一个名称超长的函数当PASS尝试检查对这个函数的调用时触发缓冲区溢出。步骤1编写一个C程序声明一个名字很长的函数。直接用C写超长函数名可能不方便我们可以先写一个简单的然后用工具修改IR。// victim.c void AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA() { // 假设有70个A // 函数体不重要 } int main() { AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA(); return 0; }步骤2编译成LLVM IR。clang-17 -S -emit-llvm victim.c -o victim.ll步骤3修改IR文件将函数名加长到足以覆盖返回地址。我们需要计算偏移。在64位系统上funcNameBuffer之后可能还有栈帧对齐、保存的寄存器等。我们可以用GDB调试来确定精确的偏移但作为示例我们假设需要覆盖到返回地址需要覆盖funcNameBuffer之后72个字节。那么我们需要一个长度至少为72 8(覆盖返回地址本身)的函数名不strcpy会一直复制直到遇到空字节所以我们需要在函数名中嵌入我们想要覆盖的地址。 更实际的方法是我们让函数名长度刚好覆盖到返回地址的位置然后在函数名字符串的末尾部分直接放置我们想要跳转的地址小端序。但由于函数名是字符串不能包含\x00空字节那会终止复制所以我们需要精心构造。一个更简单的攻击思路仅用于演示概念我们让函数名非常长比如200个字符先触发崩溃确认漏洞存在。然后通过调试精确控制溢出的内容。我们可以用Python脚本直接生成一个包含超长函数名的LLVM IR文件# gen_evil_ir.py import sys long_name FUNC_ A*200 # 200个A远超缓冲区大小 ir_template f ; ModuleID evil.c source_filename evil.c define void {long_name}() #0 {{ ret void }} define i32 main() #0 {{ call void {long_name}() ret i32 0 }} with open(evil.ll, w) as f: f.write(ir_template)运行python3 gen_evil_ir.py生成evil.ll。5.3 动态调试与利用开发1. 编译漏洞PASSclang-17 -shared -fPIC -g llvm-config-17 --cxxflags VulnerablePass.cpp -o VulnerablePass.so llvm-config-17 --ldflags llvm-config-17 --libs core注意加上了-g参数以便调试。2. 使用GDB调试观察崩溃gdb --args opt-17 -load ./VulnerablePass.so -simple-checker evil.ll在GDB中运行很可能会看到段错误Segmentation fault。使用bt查看回溯崩溃点应该在strcpy内部或之后。使用x/20gx $rsp等命令查看栈内存确认我们的长字符串是否覆盖了关键数据。3. 精确计算偏移并构造ROP链假设目标环境有NX无ASLR在真实CTF题目中环境通常更复杂可能有ASLR、NX、Canary等。你需要泄露地址可能需要利用PASS的其他功能或漏洞先泄露一些地址绕过ASLR。寻找gadget在opt二进制文件或它链接的LLVM库中寻找可用的ROP gadget。构造利用链将精心构造的函数名作为数据写入IR使得strcpy时栈上的返回地址被覆盖为ROP链的起始地址。由于函数名中不能有\x00你需要确保地址字节都不为0或者利用strcpy的特性它遇到\x00才停止进行分段写入但这在栈溢出中比较困难通常需要其他原语。4. 获得代码执行或达成目标PASS类题目的最终目标不一定是拿到shell。可能是泄露信息让PASS在错误处理或打印信息时输出一些本不该输出的内存内容比如通过格式化字符串漏洞如果PASS用了printf之类。修改分析结果通过破坏PASS的内部数据结构使其对目标程序的分析得出错误结论例如将一个真实的漏洞标记为安全。执行任意命令如果成功劫持控制流可以执行system等函数。5.4 一个更现实的“逻辑漏洞”利用示例假设一个PASS用于计算程序中循环的迭代次数上限用于界分析。它简单地认为for (i 0; i N; i)中的N如果是一个常数那么循环次数就是N。但N可能来自一个未初始化的变量或者一个攻击者可控的输入。PASS没有进行深入的数据流分析就报告“循环最多执行N次是安全的”。攻击者可以通过构造一个在静态分析时N为小常数、但动态运行时N非常大的循环来触发一个真正的运行时DoS或缓冲区溢出。这里攻击者“欺骗”了PASS使其产生了漏报。6. CTF实战技巧与常见题型解析在CTF比赛中LLVM PASS类题目通常有以下几种形式每种都有不同的解题思路6.1 题型一PASS本身有漏洞要求利用它这是最经典的题型。题目提供一个有漏洞的PASS.cpp或.so和一个目标程序.c或.ll。要求你编写或修改目标程序当用该PASS分析时能触发漏洞并实现利用如读取flag文件。解题步骤审计PASS代码这是最关键的一步。仔细阅读PASS源码寻找章节3中提到的各类漏洞。重点关注所有处理来自IR的字符串、整数的地方。所有的数组访问、指针解引用。所有的类型转换castvsdyn_cast。PASS内部自定义的数据结构及其操作。理解漏洞触发条件漏洞需要什么样的IR输入才能触发是一个特定名称的函数一个特定值的常量一个特殊结构的循环或分支构造恶意IR根据漏洞条件编写或修改LLVM IR。你可能需要使用clang编译一个C模板再手动修改.ll文件。直接用Python脚本生成.ll文件这对于构造复杂或畸形的结构非常方便。使用llvm-as将文本IR转为二进制.bc有时PASS可能直接读.bc。动态调试使用gdb调试opt观察PASS运行过程确认漏洞触发点并计算偏移、寻找gadget等。完成利用编写最终的exp生成能实现任意代码执行或信息泄露的IR文件。6.2 题型二编写PASS去攻击/分析目标程序题目给出一个目标程序通常是混淆过的、或有特殊保护的要求你编写一个LLVM PASS来“破解”它。例如程序将flag加密后与一个常量比较你的PASS需要在编译时或IR分析阶段推导出flag。解题步骤分析目标程序用clang -S -emit-llvm将其转换为IR阅读IR理解其逻辑。混淆可能体现在控制流平坦化、虚假分支、不透明谓词等。设计PASS策略你的PASS需要做什么符号执行在IR层面进行简单的符号执行记录约束并求解。常量传播写一个PASS进行积极的常量传播和折叠消除混淆。模式匹配识别出关键的比较、解密逻辑并直接提取或计算出结果。IR简化移除虚假分支和无用代码还原程序本来面目。实现PASS使用LLVM API遍历IR实现你的分析逻辑。这可能需要对LLVM IR的指令、值、使用者-定义链有深入理解。运行与验证用你的PASS处理目标IR观察输出或修改后的IR提取flag。6.3 题型三PASS与目标程序交互漏洞这类题目综合了前两者。目标程序本身可能包含漏洞但需要通过一个特定的PASS去“激活”或“触发”它或者PASS在分析目标程序时其行为会被目标程序的某些特性所影响从而暴露出PASS自身的漏洞。解题思路需要同时分析目标程序和PASS代码找到它们之间微妙的交互点。例如目标程序可能包含一个巨大的全局数组PASS在遍历该数组时其索引计算可能发生整数溢出。6.4 实用工具与命令速查查看IR结构opt -S -view-cfg foo.ll(需要Graphviz) 可以生成控制流图。统计IR信息opt -stats -analyze foo.ll可以输出一些统计信息。运行多个PASSopt -load pass1.so -pass1 -load pass2.so -pass2 foo.ll。调试PASS在PASS代码中插入errs() Debug: some_value \n;是最简单的打印调试法。处理崩溃如果opt加载你的PASS后崩溃首先检查PASS编译链接的LLVM版本是否与opt版本一致这是最常见的问题。7. 进阶深入LLVM内部与漏洞挖掘思维当你掌握了基础后可以尝试向更深处探索这能让你在遇到新颖题目时游刃有余。7.1 理解LLVM核心数据结构的内存布局要挖掘深层次的漏洞尤其是UAF、类型混淆等需要了解LLVM对象在内存中是如何表示的。LLVM大量使用了继承和虚函数。Value所有值的基类。有一个Use链表记录哪些指令使用了这个值。User继承自Value表示使用了其他值的类如指令。Instruction继承自User表示一条IR指令。BasicBlock继承自Value包含指令列表。Function继承自Value包含基本块列表。每个对象通常有一个指向其虚函数表的指针vptr。如果通过溢出或类型混淆修改了vptr就可能控制程序流。在调试器中你可以打印这些对象的地址观察其内存布局。7.2 关注LLVM的“未定义行为”处理LLVM IR本身包含一些“未定义行为”UB的概念。LLVM优化器在遇到UB时可以做出任何假设。有些PASS可能会利用这些假设进行激进的优化。思考能否构造IR使得PASS基于UB做出错误假设从而导致PASS自身的逻辑错误或安全漏洞这属于非常高级的利用思路。7.3 从代码审计到漏洞模式养成对LLVM API的“危险嗅觉”看到getOperand(i)想索引i是否可能越界看到castSpecificType(someValue)想someValue真的总是SpecificType吗用dyn_cast是不是更安全看到getName().data()或getString()想这个字符串的长度检查了吗复制时用了安全的函数吗看到对ConstantInt的getZExtValue()想这个值会不会太大导致截断或符号问题看到PASS维护自己的容器如std::map,std::vector来存储IR对象信息想IR被其他PASS修改后比如被优化掉这个容器里的指针会不会悬空7.4 与其他领域的结合LLVM PASS pwn的知识可以迁移到其他领域Clang静态分析器其检查器Checker也是类似的插件机制也可能存在漏洞。源码级插桩工具基于Clang/LLVM的源码插桩工具如自定义的Sanitizer其插桩逻辑如果有误可能被利用来绕过检测。代码混淆与反混淆理解PASS如何变换IR是理解控制流平坦化、虚假分支等混淆技术的基础也是进行反混淆的前提。LLVM PASS类pwn题就像一座连接编译器技术与二进制安全的桥梁。它要求你既要有底层的内存攻击思维又要有上层程序分析与设计的视野。入门的过程可能会有陡峭的学习曲线但一旦跨越你对程序的理解、对漏洞的认知都会达到一个新的层次。从分析一个简单的栈溢出PASS开始逐步尝试更复杂的逻辑漏洞、数据流分析漏洞你会发现自己阅读代码、设计攻击链的能力在不知不觉中大幅提升。最重要的是保持动手实践多读代码多调试多构造有趣的IR去“测试”那些看似坚固的分析工具乐趣和知识就在这个过程中不断涌现。