一
框架分析
如何反debug的原理不再赘述,我们主要针对PASS的具体实现进行分析。该PASS旨在增加编译后程序的抵抗调试能力,先梳理它的整体实现逻辑有个第一印象,后续我们会展开详解。它通过两个主要方式实现反调试功能:
代码中尝试从指定的路径(由adbextirpath
选项提供)加载预编译的反调试IR文件。如果文件加载成功,它会通过Linker::linkModules
函数被链接到当前的模块中。这个预编译的IR可能包含了一系列用于反调试的函数(ADBCallBack
和InitADB
)和结构,例如:
◆检测调试器的代码。
◆修改自身执行路径以防止调试器正常跟踪。
◆插桩代码以检测在调试环境中可能发生的异常行为。
对于Darwin操作系统的AArch64架构,如果没有找到ADBCallBack
和InitADB
函数,pass会尝试直接注入内联汇编代码。采用了一种基于概率的方法,通过cryptoutils->get_range(2)
随机选择一种内联汇编代码注入:
◆生成的内联汇编代码可能使用系统调用尝试触发反调试行为,例如,通过ptrace
调用来检测是否被调试。
◆使用InlineAsm::get
创建内联汇编对象,然后将其插入到函数的最后一条指令之前,通常是函数的返回指令之前。
二
代码分析
我们先对配置进行简单的解读,代码开始位置定义了两个静态全局命令行选项:
(1)PreCompiledIRPath
命令行选项:
static cl::opt<std::string> PreCompiledIRPath(
"adbextirpath",
cl::desc("External Path Pointing To Pre-compiled AntiDebugging IR"),
cl::value_desc("filename"), cl::init(""));
cl::opt<std::string> 定义了一个类型为 std::string 的命令行选项。
"adbextirpath" 是命令行选项的名称,在命令行中指定该选项时使用的标志。
cl::desc 提供了该选项的描述,告诉用户这个选项是用来指定预编译反调试IR文件的外部路径。
cl::value_desc 是命令行参数的描述,告诉用户这个参数应该是一个文件名。
cl::init("") 初始化了这个选项的默认值,这里是一个空字符串,表示默认不指定任何路径。
(2)ProbRate
命令行选项:
static cl::opt<uint32_t> 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> 定义了一个类型为 uint32_t (无符号32位整数)的命令行选项。
"adb_prob" 是该命令行选项的名称。
cl::desc 这个参数设定了一个百分比,用于决定每个函数被反调试混淆的概率。
cl::value_desc 用于描述该命令行选项所期望的值类型,在这个例子中,用户应该提供一个“概率率”。
cl::init(40) 表示这个选项的默认值是40,即如果用户没有在命令行中指定该选项,它的值将自动设为40%。
cl::Optional 表示这个命令行选项是可选的,用户可以选择是否提供这个选项。
总的来说,允许用户在命令行中通过-adbextirpath
选项指定预编译反调试IR文件的路径,以及用-adb_prob
选项指定每个函数被混淆的概率。
接下来我们详细地解析initialize
函数的代码,并梳理它的整体逻辑。
首先判断了PreCompiledIRPath
是否为空。如果是,就尝试构建一个默认的路径。它假定有一个名为"Hikari"的文件夹在用户的home_directory
目录下,然后根据当前模块的目标架构和操作系统类型来构建文件名称。
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();
}
}
在这个部分,首先使用一个ifstream
对象f
来检查文件是否存在。如果存在,就尝试链接预编译的IR文件。如果文件不存在或不可读,就输出一条错误信息。
std::ifstream f(PreCompiledIRPath);
if (f.good()) {
errs() << "Linking PreCompiled AntiDebugging IR From:" << PreCompiledIRPath << "\n";
SMDiagnostic SMD;
std::unique_ptr<Module> 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";
}
ADBCallBack
和InitADB
函数的属性:如果找到了ADBCallBack
函数,就断言它不是一个声明(即它已经被定义了),然后改变它的可见性、链接属性以及函数属性,保证它在优化和链接期间的行为。
// ... (前面的链接代码)
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处理)
在成功链接预编译IR之后,设置了initialized
标志为true
,并且将模块的triple
信息存储起来。
this->initialized = true;
this->triple = Triple(M.getTargetTriple());
最终,initialize
方法在完成它的任务后返回true
。通过这种方式,如果在程序编译时包含了这个LLVM Pass,它会为每个模块提供一个初始化和链接预编译IR的过程,从而植入防调试代码。如果初始化失败,它将输出错误,并且可能停止Pass的进一步执行。
runOnModule
函数比较简单,整体逻辑也很清晰,通过使用一个设定的概率值来决定是否对模块中的各个函数应用反调试混淆。它首先确保用户输入的概率值在合理范围内(0到100),然后遍历模块的所有函数,并通过toObfuscate
函数和概率判断来决定是否对非特定函数(即非ADBCallBack
和InitADB
)应用混淆。如果是,则进行相应的混淆处理,并在处理过程中初始化必要的数据结构。
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;
}
该函数为整个PASS的核心函数,我们也将针对该函数进行详细解析。
F
的入口基本块EntryBlock
获取了函数的第一个基本块,通常用于插入初始化代码或其他前置逻辑。
BasicBlock *EntryBlock = &(F.getEntryBlock());
ADBCallBack
和InitADB
函数的引用尝试从当前函数所在的模块(F.getParent()
)中获取名为ADBCallBack
和InitADB
的函数。
Function *ADBCallBack = F.getParent()->getFunction("ADBCallBack");
Function *ADBInit = F.getParent()->getFunction("InitADB");
ADBCallBack
和InitADB
函数的处理如果找到这两个函数的处理,则在入口基本块中创建对InitADB
的调用。
如果ADBCallBack
或InitADB
没有找到,则输出错误消息,并且如果函数F
的返回类型不是void
,则返回false
。
if (ADBCallBack && ADBInit) {
CallInst::Create(ADBInit, "",
cast<Instruction>(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;
如果目标系统是 Darwin(例如 macOS 或 iOS)且架构是 AArch64(ARM64),则继续执行,初始化一个空的字符串,用于后续构建内联汇编代码。
if (triple.isOSDarwin() && triple.isAArch64()) {
errs() << "Injecting Inline Assembly AntiDebugging For:"
<< F.getParent()->getTargetTriple() << "\n";
std::string antidebugasm = "";
antidebugasm
通过一个随机函数get_range(2)
来选择不同的代码路径。
switch (cryptoutils->get_range(2)) {
antidebugasm
使用循环和随机选择的方法,确保每组指令都至少使用一次,然后拼接到antidebugasm
字符串。
case 0: {
std::string s[] = {"mov x0, #31\n", "mov w0, #31\n", "mov x1, #0\n",
"mov w1, #0\n", "mov x2, #0\n", "mov w2, #0\n",
"mov x3, #0\n", "mov w3, #0\n", "mov x16, #26\n",
"mov w16, #26\n"}; // svc ptrace
bool c[5] = {false, false, false, false, false};
while (c[0] != true || c[1] != true || c[2] != true || c[3] != true ||
c[4] != true) {
// ...
}
InlineAsm
对象IA
并在函数终结指令前插入创建一个内联汇编对象,其中包含了字符串antidebugasm
中的汇编代码。
InlineAsm *IA = InlineAsm::get(FunctionType::get(Type::getVoidTy(EntryBlock->getContext()), false), antidebugasm, "", true, false);
遍历函数中的所有基本块,并在每个基本块的终止指令前插入内联汇编调用,并在内部进行了版本适配。
Instruction *I = nullptr;
for (BasicBlock &BB : F)
I = BB.getTerminator();
CallInst::Create(IA, std::nullopt, "", I);
#if LLVM_VERSION_MAJOR >= 16
CallInst::Create(IA, std::nullopt, "", I);
#else
CallInst::Create(IA, None, "", I);
#endif
如果不是预期的操作系统和架构,输出一个错误消息。
} else {
errs() << "Unsupported Inline Assembly AntiDebugging Target: " << F.getParent()->getTargetTriple() << "\n";
}
通过上述代码,大致流程主要是先进行ADBCallBack
和InitADB
函数的获取以及调用,之后针对Darwin系统ARM64架构进行了内联汇编的插入,通过汇编实现svc ptrace的调用,在过程中采用了随机数填充等安全手段。
在上面的分析中我们可知,代码逻辑通过PreCompiledIRPath
参数设置了包含ADBCallBack
和InitADB
函数的IR文件,在此文件中进行了一下反调试的逻辑。所以接下来我们针对该文件进行分析。该IR文件Hikari原作者已经提供,地址为:https://github.com/HikariObfuscator/Resources,文件结构如下:
PrecompiledAntiDebugging-aarch64-ios.bc
PrecompiledAntiDebugging-thumb-ios.bc
PrecompiledAntiDebugging-x86_64-macosx.bc
SymbolConfig.json
我们仅针对PrecompiledAntiDebugging-aarch64-ios.bc
文件进行分析,.bc
文件是LLVM bitcode文件格式,它包含了LLVM的中间表示的编译后的二进制形式。要查看.bc
文件的内容,需要将其转换成文本形式的LLVM IR。使用LLVM工具链中的llvm-dis
工具来完成这个转换。转换后的文件通常具有.ll
扩展名,这是一个可读的LLVM IR文件。
llvm-dis <input.bc> -o <output.ll>
读者可以自行去转换一下,由于代码量较大,在此处就不提供对应代码,我们接下来针对该IR文件进行分析。
代码的开头定义了多个结构体,其中包括%struct.kinfo_proc
、%struct.extern_proc
、%union.anon
、%struct.itimerval
、%struct.timeval
、%struct.eproc
、%struct._pcred
、%struct._ucred
、%struct.vmspace
和%struct.ios_execp_info
。
◆@.str
是字符串 "ptrace" 的全局声明:@.str = private unnamed_addr constant [7 x i8] c"ptrace\00", align 1
◆@mach_task_self_
是一个外部全局变量声明:@mach_task_self_ = external global i32, align 4
ADBCallBack
函数比较简单,调用abort()
函数终止程序,然后执行一个无法到达的指令(unreachable
)。
define void @ADBCallBack() #0 {
call void @abort() #4
unreachable
}
这个函数包含了多个系统调用和检查,主要逻辑如下:
◆使用sysctl
查询进程信息:%18 = call i32 @sysctl(ptr %16, i32 4, ptr %17, ptr %3, ptr null, i64 0)
◆检查进程的某些状态(通过执行位运算and
和比较指令icmp
):%22 = and i32 %21, 2048
与%23 = icmp ne i32 %22, 0
◆如果检测到调试状态,调用ADBCallBack
函数:call void @ADBCallBack()
◆尝试动态加载和卸载库,可能是尝试检测是否有调试器干预动态链接过程:dlopen
和dlsym
调用:%26 = call ptr @dlopen(ptr null, i32 10)
◆使用syscall
进行系统调用,进行更底层的检查:syscall
调用:%34 = call i32 (i32, ...) @syscall(i32 26, i32 31, i32 0, i32 0)
◆动态分配内存,调用task_get_exception_ports
来检查异常端口,这可能用于确定是否有调试器附加:%52 = call i32 @task_get_exception_ports(i32 %37, i32 7166, ptr %40, ptr %42, ptr %45, ptr %48, ptr %51)
◆检查isatty
和ioctl
是否表现异常,这些通常用于检查程序是否在终端上运行,以及终端的状态。:%81 = call i32 @isatty(i32 1)
与%85 = call i32 (i32, i64, ...) @ioctl(i32 1, i64 1074295912)
函数声明部分包含了多个系统调用,例如:
◆declare void @abort() #1
◆declare i32 @getpid() #2
◆declare ptr @malloc(i64) #3
◆declare i32 @task_get_exception_ports(i32, i32, ptr, ptr, ptr, ptr, ptr) #2
◆declare i32 @isatty(i32) #2
◆declare i32 @ioctl(i32, i64, ...) #2
函数属性在代码末尾通过 attributes 关键字定义:
attributes #0 = { noinline nounwind optnone ssp uwtable ... }
attributes #1 = { noreturn "correctly-rounded-divide-sqrt-fp-math"="false" ...}
attributes #2 = ...
模块的编译器标志和识别信息在代码末尾给出:
!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}
以上 IR 代码设计用来检测和防止调试。一旦它检测到某些条件符合调试器运行或者与正常运行程序的预期不符,它会通过调用ADBCallBack
来终止程序。我们做一下代码的总体分析:
① 结构体定义:代码以多个结构体的定义开始,这些结构体可能用于与 iOS 操作系统的交互和内存数据的组织。
② 全局声明:@.str
是一个私有的、未命名的地址常量,用于存储字符串 "ptrace"。@mach_task_self_
是一个外部全局变量,它可能表示当前任务的标识。
③ 函数 ADBCallBack:这个函数非常简单,它调用abort()
函数终止程序,然后执行一个无法到达的指令(unreachable
),这通常是反调试逻辑的一部分。
④ 函数 InitADB:这个函数是反调试逻辑的核心。它进行了一系列的系统调用和检查:
⑤ 系统调用和声明:代码中声明了一系列系统函数,如getpid
、sysctl
、dlopen
、dlsym
、dlclose
、syscall
、malloc
、task_get_exception_ports
、isatty
和ioctl
。这些函数用于执行各种系统级别的操作,很多与防止调试有关。
⑥ 属性:这些定义了函数的编译器优化属性,如不内联(noinline
)、不抛出异常(nounwind
)等。
⑦ 模块标志和标识:声明了一些编译器相关的元数据,比如wchar_size
和 PIC(位置无关代码)等级。
三
总结
这篇文章我们通过详细的代码分析以及IR文件解读了解了基于LLVM PASS的AntiDebug是如何实现的,最后我们总结一下相较于源代码实现AntiDebug采用PASS的形式两者之间有什么不同。
在项目中直接实现AntiDebug通常意味着在源代码层面增加检测调试器的逻辑,而基于LLVM Pass实现AntiDebug则是在编译器优化阶段插入这类逻辑。两者的优势可以从以下几个方面进行比较:
1.隐蔽性:
2.可移植性:
3.灵活性和复用性:
4.维护性:
5.性能:
6.混淆程度:
总而言之,基于LLVM Pass实现AntiDebug可以提供更好的隐蔽性、可移植性、灵活性、维护性,同时可能带来性能和混淆程度方面的优势。然而,这种方法需要对LLVM框架有深入的了解,并且可能需要面对更复杂的构建和调试过程。
看雪ID:ElainaDaemon
https://bbs.kanxue.com/user-home-945395.htm
# 往期推荐
2、在Windows平台使用VS2022的MSVC编译LLVM16
3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱
球分享
球点赞
球在看