首页
社区
课程
招聘
5
[原创]Hikari源码分析 - AntiDebug
发表于: 2024-1-9 12:01 16444

[原创]Hikari源码分析 - AntiDebug

2024-1-9 12:01
16444

该篇文章是Hikari源码分析的第二篇,第一篇在这里(https://bbs.kanxue.com/thread-280139.htm ), 该篇主要针对AntiDebug功能的实现进行分析,源码位置: https://github.com/61bcdefg/Hikari-LLVM15-Core/blob/main/AntiDebugging.cpp, 话不多说,我们开始。

框架分析

如何反debug的原理不再赘述,我们主要针对PASS的具体实现进行分析。该PASS旨在增加编译后程序的抵抗调试能力,先梳理它的整体实现逻辑有个第一印象,后续我们会展开详解。它通过两个主要方式实现反调试功能:

1. 链接预编译的反调试IR代码

代码中尝试从指定的路径(由adbextirpath选项提供)加载预编译的反调试IR文件。如果文件加载成功,它会通过Linker::linkModules函数被链接到当前的模块中。这个预编译的IR可能包含了一系列用于反调试的函数(ADBCallBackInitADB)和结构,例如:

  • 检测调试器的代码。
  • 修改自身执行路径以防止调试器正常跟踪。
  • 插桩代码以检测在调试环境中可能发生的异常行为。

2. 特定于平台的内联汇编注入

对于Darwin操作系统的AArch64架构,如果没有找到ADBCallBackInitADB函数,pass会尝试直接注入内联汇编代码。采用了一种基于概率的方法,通过cryptoutils->get_range(2)随机选择一种内联汇编代码注入:

  • 生成的内联汇编代码可能使用系统调用尝试触发反调试行为,例如,通过ptrace调用来检测是否被调试。
  • 使用InlineAsm::get创建内联汇编对象,然后将其插入到函数的最后一条指令之前,通常是函数的返回指令之前。

代码分析

0. config

我们先对配置进行简单的解读,代码开始位置定义了两个静态全局命令行选项:

  1. PreCompiledIRPath命令行选项

    1
    2
    3
    4
    static cl::opt PreCompiledIRPath(
        "adbextirpath",
        cl::desc("External Path Pointing To Pre-compiled AntiDebugging IR"),
        cl::value_desc("filename"), cl::init(""));
    • cl::opt 定义了一个类型为 std::string 的命令行选项。
    • "adbextirpath" 是命令行选项的名称,在命令行中指定该选项时使用的标志。
    • cl::desc 提供了该选项的描述,告诉用户这个选项是用来指定预编译反调试IR文件的外部路径。
    • cl::value_desc 是命令行参数的描述,告诉用户这个参数应该是一个文件名。
    • cl::init("") 初始化了这个选项的默认值,这里是一个空字符串,表示默认不指定任何路径。
  2. ProbRate命令行选项

    1
    2
    3
    4
    5
    static cl::opt ProbRate(
        "adb_prob",
        cl::desc("Choose the probability [%] For Each Function To Be "
                 "Obfuscated By AntiDebugging"),
        cl::value_desc("Probability Rate"), cl::init(40), cl::Optional);
    • cl::opt 定义了一个类型为 uint32_t (无符号32位整数)的命令行选项。
    • "adb_prob" 是该命令行选项的名称。
    • cl::desc 这个参数设定了一个百分比,用于决定每个函数被反调试混淆的概率。
    • cl::value_desc 用于描述该命令行选项所期望的值类型,在这个例子中,用户应该提供一个“概率率”。
    • cl::init(40) 表示这个选项的默认值是40,即如果用户没有在命令行中指定该选项,它的值将自动设为40%。
    • cl::Optional 表示这个命令行选项是可选的,用户可以选择是否提供这个选项。

总的来说,允许用户在命令行中通过-adbextirpath选项指定预编译反调试IR文件的路径,以及用-adb_prob选项指定每个函数被混淆的概率。

1. initialize

接下来我们详细地解析initialize函数的代码,并梳理它的整体逻辑。

1. 检查预编译IR路径:

首先判断了PreCompiledIRPath是否为空。如果是,就尝试构建一个默认的路径。它假定有一个名为"Hikari"的文件夹在用户的home_directory目录下,然后根据当前模块的目标架构和操作系统类型来构建文件名称。

1
2
3
4
5
6
7
8
9
10
11
12
if (PreCompiledIRPath == "") {
  SmallString<32> Path;
  if (sys::path::home_directory(Path)) {
    sys::path::append(Path, "Hikari");
    Triple tri(M.getTargetTriple());
    sys::path::append(Path, "PrecompiledAntiDebugging-" +
                              Triple::getArchTypeName(tri.getArch()) +
                              "-" + Triple::getOSTypeName(tri.getOS()) +
                              ".bc");
    PreCompiledIRPath = Path.c_str();
  }
}

2. 链接预编译的IR:

在这个部分,首先使用一个ifstream对象f来检查文件是否存在。如果存在,就尝试链接预编译的IR文件。如果文件不存在或不可读,就输出一条错误信息。

1
2
3
4
5
6
7
8
9
10
11
std::ifstream f(PreCompiledIRPath);
if (f.good()) {
  errs() << "Linking PreCompiled AntiDebugging IR From:" << PreCompiledIRPath << "\n";
  SMDiagnostic SMD;
  std::unique_ptr ADBM(
      parseIRFile(StringRef(PreCompiledIRPath), SMD, M.getContext()));
  Linker::linkModules(M, std::move(ADBM), Linker::Flags::LinkOnlyNeeded);
  // ... (省略了一部分代码)
} else {
  errs() << "Failed To Link PreCompiled AntiDebugging IR From:" << PreCompiledIRPath << "\n";
}

3. 修改ADBCallBackInitADB函数的属性:

如果找到了ADBCallBack函数,就断言它不是一个声明(即它已经被定义了),然后改变它的可见性、链接属性以及函数属性,保证它在优化和链接期间的行为。

1
2
3
4
5
6
7
8
9
10
11
// ... (前面的链接代码)
Function *ADBCallBack = M.getFunction("ADBCallBack");
if (ADBCallBack) {
  assert(!ADBCallBack->isDeclaration() && "AntiDebuggingCallback is not concrete!");
  ADBCallBack->setVisibility(GlobalValue::VisibilityTypes::HiddenVisibility);
  ADBCallBack->setLinkage(GlobalValue::LinkageTypes::PrivateLinkage);
  ADBCallBack->removeFnAttr(Attribute::AttrKind::NoInline);
  ADBCallBack->removeFnAttr(Attribute::AttrKind::OptimizeNone);
  ADBCallBack->addFnAttr(Attribute::AttrKind::AlwaysInline);
}
// ... (类似地对InitADB处理)

4. 设置初始化标志和目标三元组信息:

在成功链接预编译IR之后,设置了initialized标志为true,并且将模块的triple信息存储起来。

1
2
this->initialized = true;
this->triple = Triple(M.getTargetTriple());

最终,initialize方法在完成它的任务后返回true。通过这种方式,如果在程序编译时包含了这个LLVM Pass,它会为每个模块提供一个初始化和链接预编译IR的过程,从而植入防调试代码。如果初始化失败,它将输出错误,并且可能停止Pass的进一步执行。

2. runOnModule&runOnFunction

2.1 runOnModule

runOnModule函数比较简单,整体逻辑也很清晰,通过使用一个设定的概率值来决定是否对模块中的各个函数应用反调试混淆。它首先确保用户输入的概率值在合理范围内(0到100),然后遍历模块的所有函数,并通过toObfuscate函数和概率判断来决定是否对非特定函数(即非ADBCallBackInitADB)应用混淆。如果是,则进行相应的混淆处理,并在处理过程中初始化必要的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool runOnModule(Module &M) override {
    if (ProbRate > 100) {
      errs() << "AntiDebugging application function percentage "
                "-adb_prob=x must be 0 < x <= 100";
      return false;
    }
    for (Function &F : M) {
      if (toObfuscate(flag, &F, "adb") && F.getName() != "ADBCallBack" &&
          F.getName() != "InitADB") {
        errs() << "Running AntiDebugging On " << F.getName() << "\n";
        if (!this->initialized)
          initialize(M);
        if (cryptoutils->get_range(100) <= ProbRate)
          runOnFunction(F);
      }
    }
    return true;
  }

2.2 runOnFunction

该函数为整个PASS的核心函数,我们也将针对该函数进行详细解析。

1. 获取函数F的入口基本块EntryBlock

获取了函数的第一个基本块,通常用于插入初始化代码或其他前置逻辑。

1
BasicBlock *EntryBlock = &(F.getEntryBlock());

2. 尝试获取ADBCallBackInitADB函数的引用

尝试从当前函数所在的模块(F.getParent())中获取名为ADBCallBackInitADB的函数。

1
2
Function *ADBCallBack = F.getParent()->getFunction("ADBCallBack");
Function *ADBInit = F.getParent()->getFunction("InitADB");

3. ADBCallBackInitADB函数的处理

如果找到这两个函数的处理,则在入口基本块中创建对InitADB的调用。

如果ADBCallBackInitADB没有找到,则输出错误消息,并且如果函数F的返回类型不是void,则返回false

1
2
3
4
5
6
7
8
9
10
if (ADBCallBack && ADBInit) {
    CallInst::Create(ADBInit, "",
                     cast(EntryBlock->getFirstInsertionPt()));
  } else {
    errs() << "The ADBCallBack and ADBInit functions were not found\n";
    if (!F.getReturnType()
             ->isVoidTy()) // We insert InlineAsm in the Terminator, which
                           // causes register contamination if the return type
                           // is not Void.
      return false;

4. 检查目标操作系统和架构并构建内联汇编代码字符串

如果目标系统是 Darwin(例如 macOS 或 iOS)且架构是 AArch64(ARM64),则继续执行,初始化一个空的字符串,用于后续构建内联汇编代码。

1
2
3
4
if (triple.isOSDarwin() && triple.isAArch64()) {
        errs() << "Injecting Inline Assembly AntiDebugging For:"
               << F.getParent()->getTargetTriple() << "\n";
        std::string antidebugasm = "";

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 5
支持
分享
赞赏记录
参与人
雪币
留言
时间
PLEBFE
为你点赞~
2024-5-7 01:05
victorkong
为你点赞~
2024-4-2 16:21
slcn
为你点赞~
2024-2-26 08:16
令狐双
为你点赞~
2024-1-10 11:00
huangjw
为你点赞~
2024-1-9 14:17
最新回复 (8)
雪    币: 1253
活跃值: (1830)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
tql 
2024-1-10 10:29
0
雪    币: 1701
活跃值: (215907)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
tql
2024-1-10 11:19
0
雪    币: 424
活跃值: (4810)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
4
tql
2024-1-21 18:54
0
雪    币: 3984
活跃值: (31431)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
tql
2024-1-22 09:53
1
雪    币: 194
活跃值: (2271)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
6
tql
2024-1-22 13:34
0
雪    币: 757
活跃值: (1832)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
写完源码没了。。。
2024-1-30 14:42
0
雪    币: 1364
活跃值: (2109)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
8
寻梦之璐 写完源码没了。。。[em_85]
源码的地址多了一个逗号,手动删一下
2024-1-30 15:13
0
雪    币: 757
活跃值: (1832)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
ElainaDaemon 源码的地址多了一个逗号,手动删一下[em_85]
好的,谢谢,我还以为写完源码就被删了
2024-2-26 10:17
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册