llvm学习(六):说说发现的其他几个项目的 Bug

最近开发参考了几个项目,仔细阅读源码的过程中,看到一些不大理解的地方。本来准备先写点基础知识的,看到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 it's a conditional jump
if (i->getTerminator()->getNumSuccessors() == 2) {
/*.......*/
// FIXME: 这里不一定能转换成功。。。可能是IndirectInst
// Create a SelectInst
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);
////////这里不该写destroy这句话,我从网上复制粘贴代码没大注意这里,pthread_mutex_destroy(&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。