最近开发参考了几个项目,仔细阅读源码的过程中,看到一些不大理解的地方。本来准备先写点基础知识的,看到hikari的作者在自己的 blog 里列举了其他人的 bug,作为后辈我也列举一下前人的 bug 好了。 本文包括一个ollvm的 bug,多个hikari的bug。(不代表 Armariris 没 bug,因为写的太挫了,没忍心看。。。) 2020年01月20日,又发现一个ollvm的 bug,然后发现hikari 作者在 2019.1.1已将其修复,点赞。
2019.4.23:Hikari 作者更新 wiki 后,已修改本文部分措辞,单纯交流技术和设计。 一、ollvm在 Flatten 时对 IndirectBranch 缺少处理的bug 原因:ollvm 在 Flatten 时,根据每个 BasicBlock 的 Terminator 的后继个数来进行处理,当后继为2时,就认为是 BranchInst 了,但这时不一定是 BranchInst。
代码位置:
1 2 3 4 5 6 7 8 9 10 if (i->getTerminator ()->getNumSuccessors () == 2 ) { BranchInst *br = cast<BranchInst>(i->getTerminator ()); SelectInst *sel =SelectInst::Create (br->getCondition (), numCaseTrue, numCaseFalse, "" , i->getTerminator ()); }
测试用的 case:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ; Function Attrs: noinline nounwind optnone ssp uwtable define i32 @main() #0 { entry: %retval = alloca i32, align 4 %a = alloca i32, align 4 store i32 0, i32* %retval, align 4 store i32 0, i32* %a, align 4 %0 = load i32, i32* %a, align 4 %conv = sitofp i32 %0 to double %cmp = fcmp oeq double %conv, 0.000000e+00 %1 = select i1 %cmp, i8* blockaddress(@main, %if.then), i8* blockaddress(@main, %if.else) indirectbr i8* %1, [label %if.then, label %if.else] if.then: ; preds = %entry %call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0)) br label %if.end if.else: ; preds = %entry %call2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.1, i32 0, i32 0)) br label %if.end if.end: ; preds = %if.else, %if.then %call3 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([12 x i8], [12 x i8]* @.str.2, i32 0, i32 0)) %2 = load i32, i32* %retval, align 4 ret i32 %2 }
1 2 3 4 5 6 7 8 9 #include <stdio.h> int main () { int a = 0 ; if (a == 0.0 ) printf ("0!n" ); else printf ("1!n" ); printf ("HelloWorld!" ); }
crashLog:
1 2 3 4 7 libxxxxxx.so 0x000000010863ac27 llvm::cast_retty<llvm::BranchInst, llvm::TerminatorInst*>::ret_type llvm::cast<llvm::BranchInst, llvm::TerminatorInst>(llvm::TerminatorInst*) + 103 8 libxxxxxx.so 0x00000001086560c1 llvm::Flattening::flatten(llvm::Function&) + 7521 9 libxxxxxx.so 0x00000001086542d9 llvm::Flattening::runOnFunction(llvm::Function&) + 121 10 clang-7 0x00000001041bb171 llvm::FPPassManager::runOnFunction(llvm::Function&) + 1009
PS:直接拿这个.c编译的话是不行的,因为正常情况下是不会出现 IndirectBr 的,和其他 Pass 配合起来就会出现 IndirectBrInst,从而造成Crash。
例如随手写的这个 Pass 进行配合,就会造成Crash:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 for (BasicBlock &BB: F) { TerminatorInst *Term = BB.getTerminator(); if (Term->getNumSuccessors() == 2 && isa<BranchInst>(Term)) { BranchInst *br = cast<BranchInst>(Term); if (br->isConditional()) { IRBuilder<> IRB(Term); // True 分支是第0个后继 BasicBlock *TrueBlock = br->getSuccessor(0); // False 分支是第0个后继 BasicBlock *FalseBlock = br->getSuccessor(1); Value *Addr = IRB.CreateSelect(br->getCondition(), BlockAddress::get(TrueBlock), BlockAddress::get(FalseBlock)); IndirectBrInst *Indirect = IRB.CreateIndirectBr(Addr, 2); Indirect->addDestination(TrueBlock); Indirect->addDestination(FalseBlock); br->eraseFromParent(); } } }
其实这里还会被其他的 Terminator 打挂,例如C++那边的那几个处理异常的 Terminator,也是可能把 Flatten 打挂的,只是我没触发到罢了。
二、Hikari字符串加密时的逻辑bug 原因:Hikari 对rwdata的字符串进行加密时,如果有多处引用,会复制多份 copy,从而导致该字符串被修改后没有写回。当时好像看第一遍时候就觉得有问题,测试时候果然发现有问题。
效果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ➜ /tmp cat test.c #include<stdio.h> char hello[] = "HelloWorld!n"; void repeat(){ printf(hello); } int main(){ hello[0] = 'A'; printf(hello); repeat(); return 0; } ➜ /tmp clang -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include -Xclang -load -Xclang /Users/leadroyal/CLion_code/llvm-pass-tutorial/cmake-build-debug/Hikari/libHikari.so -w test.c -o test.bin Running StringEncryption On repeat Running StringEncryption On main ➜ /tmp ./test.bin AelloWorld! HelloWorld!
按理说输出的应该是 2 次 "AlloWorld!",但因为作者设计的问题,第二次输出错误了。
在main和repeat函数里,都对hello字符串进行了引用,所以存放了2份密文和2份 key ,直接被当成了2个独立的数据进行处理。当时还记得错误的位置来着,现在给忘了。
作者对这个地方的理解是,多处copy 是为了防止一处字符串被破,导致所有同名字符串均被破解;但引入的rwdata多处备份的问题,也是需要开发者重视的。
三、Hikari使用dlopen时不同平台flag 错误的 bug 原因:Hikari 的 FunctionCallObfuscate 引用了dlfcn.h 里定义好的常量,万万没想到,各个平台对这个常量的定义不一样。
代码位置
1 2 3 4 5 #include <dlfcn.h> vector<Value *> dlopenargs; dlopenargs.push_back (Constant::getNullValue (Int8PtrTy)); dlopenargs.push_back (ConstantInt::get (Int32Ty, RTLD_NOW RTLD_GLOBAL)); Value *Handle = IRB.CreateCall (dlopen_decl, ArrayRef<Value *>(dlopenargs));
其实乍一眼,看不出这个有什么问题,确实使用了这两个 flag,问题出在,各个平台的这个文件可能不一致。
例如 Android 上的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 sdk/ndk-bundle/sysroot/usr/include/dlfcn.h #define RTLD_LOCAL 0 #define RTLD_LAZY 0x00001 #define RTLD_NOW 0x00002 #define RTLD_NOLOAD 0x00004 #define RTLD_GLOBAL 0x00100 #define RTLD_NODELETE 0x01000 #if !defined(__LP64__) // LP32 is broken for historical reasons. #undef RTLD_NOW #define RTLD_NOW 0x00000 #undef RTLD_GLOBAL #define RTLD_GLOBAL 0x00002 #endif
例如 Mac 上的:
1 2 3 4 5 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/dlfcn.h #define RTLD_LAZY0x1 #define RTLD_NOW0x2 #define RTLD_LOCAL0x4 #define RTLD_GLOBAL0x8
其他的没统计,反正我在安卓上踩坑了,在小伙伴的提醒下,打开 IDA才发现是这里的问题。
这里应该根据 TargetTriple 的实际情况去配置数值,不能直接用 Mac 上自带的这个。
四、向各位前辈大佬致敬 ======2019.3.17更新========= 五、Hikari 字符串加密的并发 bug 当时是注意到来着,但和使用情景确认过,调用是单线程的,而且调用间隔比较长,所以就没有刻意去解决这个问题。从设计角度上来讲,可以很容易看出这个 bug。
写一下 hikari 的伪代码,例如一段 HelloWorld 的代码。
1 2 3 4 5 6 7 8 bool isDecrypt = 0 ;int func () { if (!isDecrypt){ xor_decrypt (data); } isDecrypt = 1 ; printf (data); }
显然,当多个线程同时进入 func 时,在第一个线程解密到一半的时候,第二个线程发现 isDecrypt 仍然是0,于是也开始解密。预期情况下,数据被解密一遍后是明文,被解密两遍后是乱码,所以并发肯定会出问题的。
在没有锁的情况下,无论如何写,总会出现错误的,无论什么时候去解密和设置标记,没有完美的方案。
我推荐的代码应该是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 bool isDecrypt = 0 ;pthread_mutex_t lock;__attribute__ ((constructor (101 ))) void before () { pthread_mutex_init (&lock, 0 ); } int func () { if (!isDecrypt){ pthread_mutex_lock (&lock); if (!isDecrypt){ xor_decrypt (data); isDecrypt = 1 ; } pthread_mutex_unlock (&lock); } printf (data); }
当然,该方案也不是完美的方案,将就着用还是可以的,Hikari 的作者在官方wiki里有给出方案,并且指出了该方案的不足,求同存异。
六、Hikari 字符串加密的未处理 bug 直接上样例吧,出现在当前 module 使用了其他 module 里的字符串的情况下,会出现非预期。
1 2 3 4 5 6 7 8 9 10 11 12 // main.c #include <stdio.h> extern char c[10]; int main() { printf("c=%sn", c); } // data.c #include <stdio.h> char c[10] = {'1', '2', '3', '4', '5'};
例如这个字符数组,是两个 module 里的(虽然没有人这么写),执行后,在main.c里会寻找不到 c 这个符号,因为在data里已经将 c 给删掉了,连接时就会报错。
处理方案:额,暂时没想到。。。
七、Hikari字符串加密乱插指令的 bug 设计角度上,Hikari 为每个函数都加了一个flag,在第一次进入该函数时候运行解密代码。问题就出在这里,例如写了一个很简单的函数。
void func(){;}
我什么都不写,按理说里面没有字符串,没有必要插指令,但 hikari 还是插进去了。。。。。佛了。。。。
对 c 代码还好,cpp 代码体积会扩大很多,毕竟 cpp 的函数实在是太多了。
处理方案:在处理函数时,如果函数里没有用到常量字符串,就跳过该函数。
2020年01月20日更新 八、hikari早期的bug 在看混淆出来的样本时,发现似乎平坦化不生效,单独拿出来跑确实如此,之前为什么没有注意到这个bug,我也不是很清楚。。。
主要代码逻辑是:在控制流平坦化时,会看看函数的每个BB 末尾,是不是合法的末尾,如果是 cpp 之类的不好处理就不处理了。
ollvm的实现
1 2 3 4 5 6 7 8 9 10 // Save all original BB for (Function::iterator i = f->begin(); i != f->end(); ++i) { BasicBlock *tmp = &*i; origBB.push_back(tmp); BasicBlock *bb = &*i; if (isa<InvokeInst>(bb->getTerminator())) { return false; } }
hikari早期的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // Save all original BB for (Function::iterator i = f->begin(); i != f->end(); ++i) { BasicBlock *tmp = &*i; if (tmp->isEHPad() tmp->isLandingPad()) { errs()<<f->getName()<<" Contains Exception Handing Instructions and is unsupported for flattening in the open-source version of Hikari.n"; return false; } origBB.push_back(tmp); BasicBlock *bb = &*i; if (!isa<BranchInst>(bb->getTerminator())) { return false; } }
ollvm 已经去世了,就不谈了; hikari 的这个 bug,在最后那个return false上。BB的末尾有两种,一种是跳转、一种是返回,如果有 BB 的最后一句是返回指令的话,那就直接不处理这个函数了。但是每个函数总要return的,就导致很多函数不进行控制流平坦化,Pass 直接失效了。
作者在 2019 年 1 月 1 日修复了这个 bug,commit 在 https://github.com/HikariObfuscator/Hikari/commit/9df5a45bb9cb37ce8a93be1c0b00dac436792a9c 修复了这个bug。