在写OLLVM的时候,经常会遇到各种坑点,这里就专门开一篇来记录一些自己遇到的、比较难处理的坑。本文动态更新。
复制基本块相关
在Obf的时候经常要对块进行split和clone,这个过程的坑特别多
unmatched subprogram between llvm.dbg.value and !dbg
在使用Debug模式编译程序,并对程序的一些基本块进行复制时,会出现此问题,报错来自于LLVM的Verify机制(verify.cpp)。解决这个报错之前首先先要介绍一下 @llvm.dbg.value 是什么
void @llvm.dbg.value(metadata, i64, metadata, metadata)
这个函数当 local variable 被 set new value 时会被调用,其目的是用于源码级调试时变量数据的同步。
参数 1:被 metadata 包装的 new value
参数 2:local value 中的 offset
参数 3:local variable( DILocalVariable类型 表示源码中的局部变量
参数 4:complex expression
DILocalVariable字段:
name: 源码中这个变量的名字
arg:如果不是 0,则表示他是一个子程序 subprogram 的参数,他会在 DISubprogram 的 variables 字段中
line:行号
type.scope.file…
DISubprogram:表示源码中的方法,他会被使用 !dbg attach 在方法的 define 后面
报错原因:LLVM Verify 机制中,要求 llvm.dbg.value 的第三个参数(源码局部变量)和后面的 !dbg 的 subprogram 一致。然而,clone-remap 后两个 subprogram 内容一样,但是编号不一样。
解决方法:
- clone 的时候不去 clone tail call @llvm.dbg.value。即,clone函数内判断指令如果是 call @llvm.dbg.value 则remove。但这种方法会引入一个新的问题,即,原始块和复制块指令数量不一样,在处理 attach loc 的时候会出差错。
- remap 的时候不去修改 orig 中tail call @llvm.dbg.value信息。即,clone函数内判断指令如果是 call @llvm.dbg.value 则skip,不去ReMap其中的变量。也是笔者目前采用的方法。
下面给出修复问题的 createCloneBasicBlock 函数
|
|
寄存器降级:CatchPadInst not the first non-PHI instruction in the block.
这个问题发生在,Windows下,代码中存在C++异常(IR层面存在Invoke和catchpad)时,对其进行寄存器降级时。
问题复现代码
|
|
原始IR
entry:
%retval = alloca i32, align 4
%a = alloca i32, align 4
%b = alloca i32, align 4
%c = alloca i32, align 4
store i32 0, ptr %retval, align 4
call void @llvm.dbg.declare(metadata ptr %a, metadata !1217, metadata !DIExpression()), !dbg !1218
store i32 0, ptr %a, align 4, !dbg !1218
%call = call i32 (ptr, ...) @scanf(ptr noundef @"??_C@_02DPKJAMEF@?$CFd?$AA@", ptr noundef %a) #6, !dbg !1219
call void @llvm.dbg.declare(metadata ptr %b, metadata !1220, metadata !DIExpression()), !dbg !1221
%0 = load i32, ptr %a, align 4, !dbg !1222
%call1 = invoke noundef i32 @"?callF@@YAHP6AHH@ZH@Z"(ptr noundef @"?f@@YAHH@Z", i32 noundef %0)
to label %invoke.cont unwind label %catch.dispatch, !dbg !1222
catch.dispatch: ; preds = %entry
%1 = catchswitch within none [label %catch] unwind to caller, !dbg !1224
catch: ; preds = %catch.dispatch
%2 = catchpad within %1 [ptr @"??_R0H@8", i32 0, ptr %c], !dbg !1224
call void @llvm.dbg.declare(metadata ptr %c, metadata !1225, metadata !DIExpression()), !dbg !1226
%3 = load i32, ptr %c, align 4, !dbg !1227
store i32 %3, ptr %b, align 4, !dbg !1227
%call2 = call i32 (ptr, ...) @printf(ptr noundef @"??_C@_0BB@LLPJFGLF@catch?5exception?6?$AA@") #6 [ "funclet"(token %2) ], !dbg !1229
catchret from %2 to label %catchret.dest, !dbg !1230
对其寄存器降级后就变成了这样
entry:
%retval.reg2mem = alloca ptr, align 8
%a.reg2mem = alloca ptr, align 8
%b.reg2mem = alloca ptr, align 8
%c.reg2mem = alloca ptr, align 8
%.reg2mem = alloca i32, align 4
%rand.ptr = alloca i32, align 4
%0 = load i32, ptr %rand.ptr, align 4
%1 = mul i32 %0, 3
...............
catch.dispatch: ; preds = %endBB
%18 = catchswitch within none [label %catch] unwind to caller, !dbg !1224
catch: ; preds = %catch.dispatch
%c.reload9 = load ptr, ptr %c.reg2mem, align 8
%19 = catchpad within %18 [ptr @"??_R0H@8", i32 0, ptr %c.reload9], !dbg !1224
...............
可以看到,原始IR的catch块中,开头第一条指令是catchpad,其第三个参数是寄存器 %c,这里实际上对应的是C代码 catch (int c),c变量是声明在entry块中,alloc其为IR寄存器,对应IR %c = alloca i32, align 4 。当复制entry时,会对entry块中用到的逃逸变量进行寄存器降级,对应 createCloneBasicBlock 函数
|
|
即,因为catch的这个c变量,在entry块中声明为一个寄存器,对entry块复制的时候,将函数内的该变量降级成了一个变量,并在所有用到该寄存器的指令前插入了一条Load指令,也就是降级后IR的catch块的第一条指令 %c.reload9 = load ptr, ptr %c.reg2mem, align 8,其将被降级的变量load到寄存器以给catchpad指令使用。但是LLVM要求catch块的第一条非PHI指令必须为catchpad,所以出错。
解决方法:观察了catchpad的使用,发现在正常情况下LLVM会事先的将需要用到的catch变量提前load到一个寄存器里去,所以,解决方案是重写寄存器降级函数,如果发现catchpad指令,则将该Load指令移动到entry块上,以避免直接插入到catchpad指令前。
给出重写后的DemoteRegToStack函数
|
|
指令相关
ConstantFP
在构造不透明谓词时,需要使用Constant类构造Value以生成CMPInst来实现不透明谓词跳转。BCF使用ConstantFP生成跳转条件
|
|
笔者的混淆采用外部插件化,即编译生成LLVM-Plugin,编译时clang加载插件对IR变形进行混淆。但是插件化后会导致ConstantFP二次初始化。会导致执行上述代码时卡住(非卡死,像死循环了一样),实际调试发现是抛出了异常并卡在了异常处理中。
此问题非必现,在一些情况下,如外部Plugin先初始化了ConstantFP并内部不再使用到它就不会出错,但通常来说一些大型的项目代码肯定是会出问题的。
解决方案:
- 不使用外部plugin加载,将这个pass写到内部
- 不使用ConstantFP,换用ConstantInt等来构造不透明谓词。实际上在优化的过程中,BCF这样的写法最终也会被优化成整数,从二进制层面来说没有区别,也就是没有使用ConstantFP的必要。笔者采用的是换用ConstantInt解决问题。
Register Alive
在一些情况下,我们会在MIR层通过computeRegisterLiveness计算寄存器活性,对死寄存器进行重复的无效操作来实现垃圾指令的效果。但是踩到了一个abi的坑,来看一下下面这个demo
|
|
通过这样调用
|
|
调用nake_func后,rbx不再等于0xDEADBEEF了。调式发现,computeRegisterLiveness计算得到rbx寄存器活性为DEAD,即认为这是一个死寄存器,便对该寄存器做了一些无效的写操作破坏了原来的值。我们查ABI的定义,会发现RBX寄存器是一个由被调用者保存的非易失寄存器,即我们不可以在混淆函数中破坏它。
对于这个问题,两个比较好的解决方法,主要依赖于 TRI->getCalleeSavedRegs 获取到ABI规定的需要保留的寄存器列表 以及 TFL->determineCalleeSaves 获取到函数序言已保存的寄存器列表:
- 可以在函数序言生成时添加判断,如果是需要混淆的函数,强制让函数序言保存ABI规定的需要保留的寄存器,同时关闭该函数的编译器优化(MF设置O0属性)
|
|
- 判断寄存器如果是ABI规定的需要保留的非易失寄存器并且该寄存器以及其更高级寄存器均没有被函数序言保存,则不使用这个寄存器做混淆