从零开始瞎玩llvm:利用llvm保护程序免受内存修改器的攻击


#1

本文的前置要求: 已阅读 从零开始的LLVM快速入门:自己动手实现基于llvm的字符串加密 这篇文章包含大量的基础概念。

在正向开发,尤其是单机游戏开发中,开发者常常饱受Cheat-Engine 八门神器类的内存修改器攻击。常见的保护方法是手动做大量的加密解密操作,这会导致无比巨大的人力成本和维护成本,本文将教会你Hack LLVM并使用80行代码来在编译层解决这个问题。

考虑以下的代码:

static int flag=0;
int main(int argc, char const *argv[]) {
  while(flag<13){
    printf("Flag is %i not 13.Sleeping for another 5 seconds\n",flag);
    sleep(5);
    flag++;
  }
  printf("You've waited for %i seconds. Quite an effort!\n",flag);
  return 0;
}

逻辑非常简单,从0开始循环判断flag值是不是13,如果是就打印信息并退出,否则睡眠5秒钟后继续循环。

使用clang -S -emit-llvm 可获得如下的LLVM IR:

; ModuleID = 'LLVMConstantEncryptionTest.m'
source_filename = "LLVMConstantEncryptionTest.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.13.0"

@flag = internal global i32 0, align 4
@.str = private unnamed_addr constant [50 x i8] c"Flag is %i not 13.Sleeping for another 5 seconds\0A\00", align 1
@.str.1 = private unnamed_addr constant [48 x i8] c"You've waited for %i seconds. Quite an effort!\0A\00", align 1

; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32, i8**) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  br label %6

; <label>:6:                                      ; preds = %9, %2
  %7 = load i32, i32* @flag, align 4
  %8 = icmp slt i32 %7, 13
  br i1 %8, label %9, label %15

; <label>:9:                                      ; preds = %6
  %10 = load i32, i32* @flag, align 4
  %11 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([50 x i8], [50 x i8]* @.str, i32 0, i32 0), i32 %10)
  %12 = call i32 @"\01_sleep"(i32 5)
  %13 = load i32, i32* @flag, align 4
  %14 = add nsw i32 %13, 1
  store i32 %14, i32* @flag, align 4
  br label %6

; <label>:15:                                     ; preds = %6
  %16 = load i32, i32* @flag, align 4
  %17 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([48 x i8], [48 x i8]* @.str.1, i32 0, i32 0), i32 %16)
  ret i32 0
}

declare i32 @printf(i8*, ...) #1

declare i32 @"\01_sleep"(i32) #1

attributes #0 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.ident = !{!7}

!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!3 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"clang version 6.0.0 (trunk 318965) (llvm/trunk 318964)"}

可以看到IR中包含数个LoadInst和StoreInst用于加载和保存新的flag值,我们的设计思路是在加载后XOR解密出正确的数值,在写入前XOR来写入加密的数值

首先我们创建一个基础的Pass骨架:

/*
    LLVM ConstantEncryption Pass
    Copyright (C) 2017 Zhang(http://mayuyu.io)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "llvm/IR/Constants.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/InstIterator.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Value.h"
#include "llvm/Pass.h"
#include "llvm/Transforms/Obfuscation/Obfuscation.h"// 这是Hikari用的一些全局头文件,自己按照格式创建就好
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
using namespace llvm;
using namespace std;
namespace llvm {
    struct ConstantEncryption : public ModulePass {
        static char ID;
        bool flag;
        ConstantEncryption(bool flag): ModulePass(ID) {
            this->flag=flag;
        }
        ConstantEncryption(): ModulePass(ID) {
            this->flag=true;
        }
        bool runOnModule(Module& M)override {
            return true;
        }
    };
    ModulePass * createConstantEncryptionPass() { return new ConstantEncryption(); }
    ModulePass * createConstantEncryptionPass(bool flag) { return new ConstantEncryption(flag); }
}//namespace llvm
char ConstantEncryption::ID = 0;
INITIALIZE_PASS(ConstantEncryption, "constenc", "Enable ConstantInt GV Encryption.", true,
                true)

注意Pass里的Flag都是用于和我的开源混淆器Hikari对接所设计的接口,您如果自己玩耍并不需要这些。

然后第一步在runOnModule中遍历全局变量列表:

for(auto GV=M.global_begin();GV!=M.global_end();GV++){
              GlobalVariable *GVPtr=&*GV;
}

在这个基本的循环中我们找到了所有的全局变量,对应上述IR中的:

@flag = internal global i32 0, align 4
@.str = private unnamed_addr constant [50 x i8] c"Flag is %i not 13.Sleeping for another 5 seconds\0A\00", align 1
@.str.1 = private unnamed_addr constant [48 x i8] c"You've waited for %i seconds. Quite an effort!\0A\00", align 1

接下来我们需要过滤哪些变量可以加密,哪些不能,由于我们是在编译单个源文件的过程中进行加密,所以我们必须过滤掉可以被其他源文件引用的变量,或者是声明在其他源文件中的变量。 后者通过判断全局变量是否有对应的初始化器(Initializer)来实现,前者通过判断全局变量的链接属性(LinkageType)来实现。
阅读上面的文档:

private
Global values with “private” linkage are only directly accessible by objects in the current module. In particular, linking code into a module with a private global value may cause the private to be renamed as necessary to avoid collisions. Because the symbol is private to the module, all references can be updated. This doesn’t show up in any symbol table in the object file.

这里告诉我们Private(私有)的LinkageType只能被当前源文件引用,这正符合我们上面所分析出的要求。
下面的:

internal
Similar to private, but the value shows as a local symbol (STB_LOCAL in the case of ELF) in the object file. This corresponds to the notion of the ‘static’ keyword in C.

告诉我们internal(内部)和私有非常相似,对应C语言中的static关键字,同样符合我们的要求。所以我们可以通过以下这行代码来过滤出我们需要的全局变量:

if(GVPtr->hasInitializer()&&(GVPtr->hasPrivateLinkage()||GVPtr->hasInternalLinkage())){

}

最后,我们需要确定全局变量的类型是一个整数,我们上文提到了初始化器(Initializer)的概念,阅读GlobalVariable的文档 可知我们可以通过GlobalVariable::getInitializer() 来获取对应的初始化器,通过阅读LLVM Programmers’ Manual 可以了解到LLVM提供三个模版方法来实现运行时类型识别转换,类似C++的RTTI和reinterpret_cast,但性能更快并且强制类型安全。
增加如下代码来判断初始化器的类型:

if(ConstantInt *CI=dyn_cast<ConstantInt>(GVPtr->getInitializer())){

}

常用的为三个模版方法:

  • isa<类>(变量指针) 返回布尔类型,用于做运行时类型审查
  • cast<类>(变量指针)用于将基类转换成派生类,如果不是正确的类型的话会触发一个assert。这个模版方法可以作用于指针和引用上
  • dyn_cast<类>(变量指针) 一个包含了类型检查的类型转换模版方法。如果是正确的类型的话返回新类型的指针,否则返回null。这个模版方法只对指针生效,使用上非常类似C++的dynamic_cast<>

接下来准备Key, LLVM要求类型安全并且并不会像编译器前端一样隐式添加零延伸,因此我们需要根据原来的整数宽度来准备XOR密钥:

IntegerType* IT=cast<IntegerType>(CI->getType());
uint8_t K=cryptoutils->get_uint8_t();
ConstantInt *XORKey=ConstantInt::get(IT,K);

注意这里的cryptoutils->get_uint8_t(); 是Obfuscator-LLVM提供的密码学安全的随机数生成器,您也可以使用其他方式来生成随机的XOR Key。
接下来有了XOR Key,我们先加密原来的全局变量。可以通过ConstantInt::getZExtValue()来获取原来的常量数值ZExt到uint64_t后的数值:

ConstantInt *newGVInit=ConstantInt::get(IT,CI->getZExtValue()^K); // 计算加密后的值并创建新的初始化器
GVPtr->setInitializer(newGVInit); // 将初始化器的值赋值给全局变量

接下来我们通过遍历这个全局变量的声明-使用链来找到所有引用了这个变量的指令并对LoadInst和StoreInst做正确的处理

  • 声明-使用链 即Def-Use Chain,给定一个变量,找到所有引用处
  • 使用-声明链 即Use-Def Chain,给定一个引用,找到所有可能的变量。
  for(User *U : GVPtr->users()){
     if(LoadInst *LI=dyn_cast<LoadInst>(U)){
                   Instruction* XORInst=BinaryOperator::CreateXor(XORKey,XORKey);
                      XORInst->insertAfter(LI);
                      LI->replaceAllUsesWith(XORInst);
                      XORInst->setOperand(0,LI);

                    }else if(StoreInst *SI=dyn_cast<StoreInst>(U)){
                      Instruction* XORInst=BinaryOperator::CreateXor(SI->getValueOperand(),XORKey);
                      XORInst->insertBefore(SI);
                      SI->replaceUsesOfWith(SI->getValueOperand(),XORInst);
                    }
}

在LoadInst的处理块中,我们先创建了一个没有任何卵用的XOR指令占位并插入到原来的Load指令之后,将后续对原始LoadInst的指令替换到我们的占位指令中。因为直接使用正确的左值LI来创建会导致后续的replaceAllUsesWith将我们的XOR指令的左值引用也替换成对自身的引用。最后将我们的占位XOR指令左值指向正确的LoadInst

在对StoreInst的处理块中,我们同样创建了一个新的XOR指令,左值为原来的StoreInst所要保存的数值,右值为一开始我们创建的XOR Key,将这个指令插入到Store指令之前,并将原来的Store指令对未加密数值的引用替换成我们加密之后的结果。

然后就完工啦,完整代码如下。注意有一些小细节我们的示例用代码没有处理,处理这些情况就留做给读者的练习了:

/*
    LLVM ConstantEncryption Pass
    Copyright (C) 2017 Zhang(http://mayuyu.io)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "llvm/IR/Constants.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/InstIterator.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Value.h"
#include "llvm/Pass.h"
#include "llvm/Transforms/Obfuscation/Obfuscation.h"// 这是Hikari用的一些全局头文件,自己按照格式创建就好
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
using namespace llvm;
using namespace std;
namespace llvm {
    struct ConstantEncryption : public ModulePass {
        static char ID;
        bool flag;
        ConstantEncryption(bool flag): ModulePass(ID) {
            this->flag=flag;
        }
        ConstantEncryption(): ModulePass(ID) {
            this->flag=true;
        }
        bool runOnModule(Module& M)override {
            for(auto GV=M.global_begin();GV!=M.global_end();GV++){
              GlobalVariable *GVPtr=&*GV;
              //Filter out GVs that could potentially be referenced outside of current TU
              if(GVPtr->hasInitializer()&&(GVPtr->hasPrivateLinkage()||GVPtr->hasInternalLinkage())){
                if(ConstantInt *CI=dyn_cast<ConstantInt>(GVPtr->getInitializer())){
                  //Prepare Types and Keys
                  IntegerType* IT=cast<IntegerType>(CI->getType());
                  uint8_t K=cryptoutils->get_uint8_t();
                  ConstantInt *XORKey=ConstantInt::get(IT,K);
                  //Encrypt Original GV
                  ConstantInt *newGVInit=ConstantInt::get(IT,CI->getZExtValue()^K);
                  GVPtr->setInitializer(newGVInit);
                  for(User *U : GVPtr->users()){
                    if(LoadInst *LI=dyn_cast<LoadInst>(U)){
                      // This is dummy Instruction so we can use replaceAllUsesWith
                      // without having to hand-craft our own implementation
                      // We will relace LHS later
                      Instruction* XORInst=BinaryOperator::CreateXor(XORKey,XORKey);
                      XORInst->insertAfter(LI);
                      LI->replaceAllUsesWith(XORInst);
                      XORInst->setOperand(0,LI);

                    }else if(StoreInst *SI=dyn_cast<StoreInst>(U)){
                      Instruction* XORInst=BinaryOperator::CreateXor(SI->getValueOperand(),XORKey);
                      XORInst->insertBefore(SI);
                      SI->replaceUsesOfWith(SI->getValueOperand(),XORInst);
                    }
                  }
                }
              }
            }
            return true;
        }
    };
    ModulePass * createConstantEncryptionPass() { return new ConstantEncryption(); }
    ModulePass * createConstantEncryptionPass(bool flag) { return new ConstantEncryption(flag); }
}//namespace llvm
char ConstantEncryption::ID = 0;
INITIALIZE_PASS(ConstantEncryption, "constenc", "Enable ConstantInt GV Encryption.", true,
                true)

使用我们刚刚写的Pass加固开头的程序后的F5


#2

这种文章更适合普罗大众,比之前的纯技术理论文接地气多了,支持!