目录
- 混淆在静态规避中的作用
- 混淆在分析对抗中的作用
- 保护和剥离可识别信息
概述:混淆的起源与目的 (Overview: Origin and Purpose of Obfuscation)
代码混淆技术在众多与软件相关的领域中被广泛应用,其主要目的是保护应用程序中可能包含的知识产权 (IP) 和其他专有或敏感信息,防止其被轻易理解、复制或修改。
一个著名的例子是流行的游戏Minecraft (我的世界),它使用混淆器ProGuard来混淆和最小化其 Java 类文件。为了支持庞大的模组 (Mod) 社区,Minecraft 还会发布包含有限信息的混淆映射文件,这些文件作为旧版本未混淆类名与新版本混淆后类名之间的“翻译器”。
这仅仅是混淆技术在公开领域广泛应用的一个缩影。为了系统地记录和组织各种混淆方法,学术界也进行了深入研究。例如,研究论文《分层混淆:用于分层安全软件混淆技术的分类法》(Layered Obfuscation: A Taxonomy for Layered Security Software Obfuscation Techniques)将混淆方法按照类似于 OSI 模型的层次结构进行组织,但应用于应用程序的数据流和代码结构。该分类法将混淆技术划分为不同的层次和子层,每个子层又包含多种能够实现该子层整体目标的具体方法。
利用这种分类法,我们可以首先确定混淆的目标(例如,是隐藏数据、打乱代码布局还是复杂化控制流),然后从相应的层次和子层中选择一种或多种适合需求的方法。例如,如果我们希望混淆代码的结构,但又不想修改现有代码的逻辑,根据分类法,我们可以考虑在“代码元素层 (Code Element Layer)” -> “混淆布局 (Obfuscating Layout)” 子层中选择“注入无用代码 (Junk Codes)”的方法。
混淆在静态规避中的作用 (Role of Obfuscation in Static Evasion)
对于攻击者而言,面临的两个重要安全边界是杀毒软件引擎 (Antivirus Engines)和终端检测与响应 (EDR) 解决方案。这两个平台都会利用一个广泛的已知恶意软件签名数据库(这些签名被称为静态签名),以及考虑应用程序行为模式的启发式签名。
为了规避这些基于签名的检测,攻击者可以利用各种逻辑和语法层面的规则来实现代码混淆。这通常通过滥用在合法应用程序中用于隐藏重要可识别信息(如敏感字符串、API 调用、数据结构等)的数据混淆实践来完成。
前述的《分层混淆分类法》白皮书在“代码元素层 (Code Element Layer)”的“数据混淆 (Obfuscating Data)”子层中很好地总结了这些实践。以下是该分类法在该子层中涵盖的一些方法:
| 混淆方法 (Obfuscation Method) | 目的 (Purpose) |
| 数组转换 (Array Transformation) | 通过拆分、合并、折叠(多维变一维)和平坦化(结构体数组变多组基本类型数组)等方式转换数组的存储和访问方式。 |
| 数据编码 (Data Encoding) | 使用数学函数(如算术运算)、密码算法(如XOR、Base64)或其他编码方案来转换原始数据。 |
| 数据过程化 (Data Procedurization) | 将静态数据替换为过程调用,即数据的值通过执行一个函数来动态计算或获取,而不是直接存储。 |
| 数据拆分/合并 (Data Splitting/Merging) | 将一个变量的信息分散存储到多个新变量中,或者将多个变量的信息合并到一个复杂结构中。 |
对象连接 (Object Concatenation)
连接 (Concatenation)是一种常见的编程概念,它指的是将两个或多个独立的对象(最常见的是字符串)组合成一个单一的对象。一个预定义的操作符或函数通常用于定义连接发生的位置和方式。
不同编程语言中用于连接的操作符或方法可能有所不同:
| 语言 (Language) | 连接运算符/方法 (Concatenation Operator/Method) |
| Python | + |
| PowerShell | +,,(数组连接),$(字符串内变量展开), 或无操作符 (字符串并列) |
| C# | +,String.Join,String.Concat |
| C | strcat(库函数) |
| C++ | +(对于std::string),append(成员函数) |
根据《分层混淆分类法》,连接操作可以被视为“代码元素层”下“数据拆分/合并”方法的一种应用(将一个完整的字符串“拆分”成多个部分,在运行时再“合并”)。
对攻击者的意义:
连接操作为修改静态签名或操纵应用程序的其他可识别特征提供了多种途径。在恶意软件中最常见的应用之一是破坏目标静态签名。攻击者可以预先使用连接来拆分程序中所有可疑的字符串或字节序列,从而在功能上保持不变,但在静态扫描时,这些被拆分的片段不再匹配已知的恶意签名。这在自动化混淆器中尤为常见,可以一次性处理多个签名点,而无需逐个手动修改。
示例:使用连接规避 Yara 规则
假设有一个静态 Yara 规则如下:
代码段
rule ExampleRule { strings: $text_string = "AmsiScanBuffer" // 查找文本字符串 $hex_string = { B8 57 00 07 80 C3 } // 查找十六进制字节序列 condition: $text_string or $hex_string // 满足任一条件则报警 // 原文为 $my_text_string or $my_hex_string,但定义时未使用 my_ 前缀,此处修正 }当编译后的二进制文件被此 Yara 规则扫描时,如果其中直接包含字符串 “AmsiScanBuffer” 或字节序列{ B8 57 00 07 80 C3 },则会触发警报。
攻击者可以使用连接操作来修改代码,例如:
原始代码:
C#
IntPtr ASBPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");使用连接混淆后的代码:
C#
IntPtr ASBPtr = GetProcAddress(TargetDLL, "Amsi" + "Scan" + "Buffer");
在第二个代码块中,字符串 “AmsiScanBuffer” 在编译时可能仍然会被编译器优化为一个完整的字符串,但如果在解释型语言或某些特定编译场景下,它在源代码层面被拆分。更有效的做法是在运行时动态构建这个字符串。如果字符串在静态文件中以拼接形式存在(且未被编译器完全优化掉拼接),或者其组成部分在不同位置定义,Yara 的简单字符串匹配$text_string = "AmsiScanBuffer"将无法直接匹配到这个拼接后的完整字符串,从而可能规避检测。对于十六进制序列,也可以通过插入无用指令或改变指令顺序(如果逻辑允许)等方式进行类似的分割或变形。
使用非解释字符进行混淆:
除了连接,攻击者还可以使用在特定语言或上下文中不被解释(或被忽略)的字符来破坏或混淆静态签名。这些字符可以独立使用,也可以与连接操作结合使用。
| 字符/技巧 (Character/Technique) | 目的 (Purpose) | 示例 (Example - PowerShell) |
| 中断 (Breaks) | 将单个字符串拆分为多个子字符串,然后通过连接组合它们。 | ('co'+'ffe'+'e')-> “coffee” |
| 重排 (Reorders) | 重新排序字符串的组成部分(通常与格式化字符串结合)。 | ('{1}{0}'-f'ffee','co')-> “coffee” |
| 空白字符 (Whitespace) | 包含在某些语言中不影响代码逻辑执行的空白字符,以分割字符串或标识符。 | .( 'Ne' +'w-Ob' + 'ject')-> (执行) New-Object |
| 抑音符/反引号 (Ticks) | 在某些语言(如 PowerShell)中,反引号可以用于转义或被用作无意义的字符插入到标识符或字符串片段中。 | downLoAdString-> "downLoAdString" (字符串内容) 或Get-Command` (命令) |
| 随机大小写 (Random Case) | 利用某些语言或比较操作对大小写不敏感的特性,随机改变标识符或字符串的大小写。 | dOwnLoAdsTRing(如果比较时不区分大小写,则等同于 “downloadstring”) |
混淆在分析对抗中的作用 (Deceptive Functions of Obfuscation in Anti-Analysis)
即使经过基础的数据和字符串混淆后,恶意代码可能能够通过基于签名的软件检测,但它仍然容易受到经验丰富的人工分析。分析师和逆向工程师通过深入理解恶意应用程序的功能和逻辑,最终仍可能成功阻止其运行或提取其核心目的。
为了对抗人工分析和逆向工程,攻击者可以利用更高级的逻辑和数学知识来创建结构更复杂、理解难度更大的代码。
《分层混淆分类法》白皮书在“代码元素层”的其他子层中(如“混淆布局 (Obfuscating Layout)”和“混淆控制流 (Obfuscating Control Flow)”)也总结了这些高级实践。以下是这些子层中涵盖的一些方法:
| 混淆方法 (Obfuscation Method) | 目的 (Purpose) |
| 无用代码 (Junk Code) | 添加在功能上无效的垃圾指令或代码块(也称为代码桩,Dead Code),以增加代码体积和分析复杂度。 |
| 分离相关代码 (Separation of Related Code) | 将逻辑上相关的代码或指令片段分散到程序的不同位置,通过跳转或间接调用连接,增加阅读和理解程序的难度。 |
| 剥离冗余符号 (Stripping Redundant Symbols) | 移除二进制文件中的符号信息,如调试信息、函数名、变量名等(如果编译器默认生成的话),使反汇编和调试更加困难。 |
| 无意义的标识符 (Meaningless Identifiers) | 将代码中有意义的变量名、函数名、类名等替换为无意义的、随机生成的或统一的名称(如 a, b, c 或 var1, var2)。 |
| 隐式控制 (Implicit Controls) | 将显式的控制流指令(如直接的条件跳转、循环)转换为功能相同但形式更隐晦的指令序列(例如,使用算术运算结果来决定跳转目标)。 |
| 基于分派的控制 (Dispatcher-based Controls) | 使用一个中心分派器 (Dispatcher) 来决定程序在运行时的下一个执行块,而不是直接的顺序执行或简单跳转,使得控制流难以静态追踪。 |
| 概率控制流 (Probabilistic Control Flows) | 引入多个具有相同语义(功能)但语法结构不同的控制流路径,程序在运行时可能随机选择其中一条执行。 |
| 虚假控制流 (Bogus Control Flows) | 故意在程序中添加一些永远不会被执行(或其执行结果无意义)的复杂控制流路径,以迷惑分析工具和人工分析。 |
代码流和逻辑 (Code Flow and Logic)
程序的控制流 (Control Flow)是其执行的关键组成部分,它定义了程序指令执行的顺序和逻辑路径。逻辑 (Logic)是决定应用程序控制流的最重要因素,通常通过各种逻辑语句来实现,例如条件判断 (if/else) 或循环 (for/while)。
传统上,程序代码会从上到下顺序执行。当遇到逻辑语句时,程序会根据条件的真假来决定接下来的执行路径。
常见的逻辑语句:
| 逻辑语句 (Logic Statement) | 目的 (Purpose) |
if/else | 仅当某个条件满足时执行if块中的代码,否则执行else块中的代码(如果存在)。 |
try/catch(或try/except) | 尝试执行try块中的代码,如果在此过程中发生特定类型的错误(异常),则执行catch(或except) 块中的错误处理代码。 |
switch/case | 类似于if/else if/else结构,根据一个表达式的值,从多个case中选择一个匹配的执行路径,通常以break结束或包含一个default处理。 |
for/while循环 | for循环通常用于在已知迭代次数或遍历集合时重复执行代码块。while循环则在某个条件保持为真时持续执行代码块。 |
对攻击者的意义:
分析师会通过追踪程序的控制流来尝试理解其功能。然而,程序的逻辑和控制流几乎可以被攻击者毫不费力地操纵,并使其变得任意混乱和难以理解。攻击者在处理控制流时的目标是:在有效混淆分析师的同时,引入足够晦涩和随机的逻辑,但又不能复杂到引起启发式检测引擎的怀疑或被平台直接标记为恶意(例如,过于复杂的控制流图本身可能是一个恶意特征)。
任意控制流模式 (Arbitrary Control Flow Patterns)
为了构建任意复杂的控制流模式,攻击者可以利用数学、逻辑运算和/或其他复杂算法,向恶意函数中注入不同的、难以预测的控制流路径。
不透明谓词 (Opaque Predicates)是一种常用于实现此目的的技术。谓词通常指一个函数或表达式,它根据输入返回布尔值(真或假),用于决策。不透明谓词特指这样一种谓词:其返回值对于混淆者(攻击者)来说是已知的(例如,它总是返回真,或总是返回假,或在特定条件下返回已知值),但对于分析工具或人工分析者来说,其结果却难以通过静态分析推断出来。
研究论文《不透明谓词:混淆二进制代码中的攻击与防御》(Opaque Predicates: Attacking and Defending Obfuscated Binary Code)对此有详细论述。不透明谓词可以与无用代码等其他混淆方法无缝结合,将逆向工程尝试转变为一项艰巨的工作。在《分层混淆分类法》中,不透明谓词属于“虚假控制流”和“概率控制流”方法的范畴。它们可用于向程序中任意添加看似有意义的逻辑分支,或重构现有函数的控制流,使其更难理解。
Collatz 猜想 (Collatz Conjecture)(又称 3n+1 问题) 是一个可以用作不透明谓词的常见数学问题。该猜想指出:对于任何正整数,如果它是奇数,则将其乘以 3 再加 1;如果它是偶数,则将其除以 2。重复这个过程,最终该数都会变成 1。由于对于已知的正整数输入,我们(几乎可以肯定地)知道它最终会收敛到 1,这意味着它可以被用作一个不透明谓词(例如,构造一个循环,其退出条件依赖于 Collatz 序列达到 1)。
Python 中的 Collatz 猜想示例 (用于构造不透明谓词):
Python
# x 是一个正整数输入 # original_x = x # 保存原始x,如果谓词需要基于原始输入 # is_always_true_predicate_result = False # 假设我们需要一个总是为真的谓词 # temp_x = x # if temp_x > 0: # Collatz 通常用于正整数 # while(temp_x != 1 and temp_x != 0 and temp_x != -1 and temp_x != -5 and temp_x != -17): # 防止某些已知循环或错误输入 # if (temp_x % 2 == 1): # 奇数 # temp_x = temp_x * 3 + 1 # else: # 偶数 # temp_x = temp_x / 2 # # 为防止无限循环 (尽管猜想认为不会对正整数发生),可以加一个迭代计数器 # if temp_x == 1: # is_always_true_predicate_result = True # 对于正整数,我们预期它会到达1 # if is_always_true_predicate_result: # print("This part of code will (almost) always execute if x was a positive integer.") # else: # print("This part is a bogus/dead code path for positive integers.") # x = 0 # 原文示例的 x 初始值为0,这不会进入 Collatz 序列的主体 # while(x > 1): # 如果 x 初始为0或1,此循环不执行 # if(x%2==1): # x=x*3+1 # else: # x=x/2 # if(x==1): # print("hello!")在上述代码片段中,如果x是一个大于1的正整数,while(x > 1)循环会根据 Collatz 规则进行迭代,并且我们预期x最终会变成 1。可以利用这个特性构造一个不透明谓词,例如(Collatz(input) == 1),这个表达式对于所有正整数输入(根据猜想)都为真。
保护和剥离可识别信息 (Protecting and Stripping Identifiable Information)
可识别信息(如变量名、函数名、字符串常量、调试信息等)是分析师剖析和理解恶意程序功能的最关键线索之一。通过限制或消除这些可识别信息,攻击者可以显著增加分析难度,使得分析师难以重建程序的原始功能和意图。
从高层次来看,应考虑混淆或移除以下三种类型的可识别数据:
- 对象名 (Object Names)
- 代码结构 (Code Structure)(已在前述控制流和布局混淆中部分涉及)
- 文件/编译属性 (File/Compilation Attributes)
1. 对象名 (Object Names)
对象名(包括变量名、函数名、类名等)能够提供关于程序功能的最直接的洞察,有时甚至能直接揭示某个函数或变量的确切目的。即使没有明确的命名,分析师仍可能从对象的行为和使用上下文中推断其目的,但如果缺乏有意义的名称,这个过程将变得更加困难和耗时。
重要性差异 (解释型 vs. 编译型语言):
- 解释型语言 (如 Python, PowerShell): 由于源代码或脚本通常更易于访问,所有对象名(变量、函数等)都相对重要,对其进行混淆可以有效增加理解难度。
- 编译型语言 (如 C, C++, C#): 编译后,大部分局部变量名和许多内部函数名在标准发行版 (Release) 构建中会被优化掉或不再直接可见。此时,更有意义进行混淆的是那些仍然会出现在最终二进制文件字符串表中的对象名(例如,用作日志输出的字符串、API 函数名字符串参数、全局字符串常量等),以及导出的函数名(如果是 DLL)。任何执行 I/O 操作或与其他组件交互的功能,都可能使其使用的对象名(作为字符串参数)出现在二进制文件中。
分类法关联: 《分层混淆分类法》中的“代码元素层” -> “无意义的标识符 (Meaningless Identifiers)”方法总结了这类实践。
示例 1: 编译型语言 (C++) - 移除有意义的标识符和字符串
观察一个用 C++ 编写的简单进程注入程序,在其混淆前后通过 strings.exe 工具分析其泄露的字符串信息。
原始 C++ 代码 (包含
iostream输出和有意义的变量名):C++
#include "windows.h" #include <iostream> // 用于输出调试信息 #include <string> // 用于 std::string using namespace std; int main(int argc, char* argv[]) { unsigned char shellcode[] = { /* ... shellcode bytes ... */ }; // 变量名 "shellcode" HANDLE processHandle; HANDLE remoteThread; PVOID remoteBuffer; string leaked = "This was leaked in the strings"; // 字符串常量 processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1]))); cout << "Handle obtained for " << processHandle << endl; // 输出信息 remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE); cout << "Buffer Created" << endl; WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL); cout << "Process written with buffer " << remoteBuffer << endl; remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL); CloseHandle(processHandle); cout << "Closing handle " << processHandle << endl; cout << leaked << endl; // 输出字符串常量 return 0; }使用 strings.exe 分析编译后的原始程序:
输出会包含大量字符串,包括:
iostream相关的错误信息和内部字符串 (如 “invalid argument”, “string too long”, “bad allocation”)。- 程序中定义的字符串常量,如 “This was leaked in the strings”。
- 用于
cout输出的提示信息,如 “Handle obtained for”, “Buffer Created” 等。 - 甚至某些情况下,如果编译器优化不当或代码结构特殊,变量名(如 “shellcode”, “leaked”)的某些痕迹也可能以某种形式存在(尽管在标准Release编译中,局部变量名通常不会直接出现在字符串表中,但全局变量名或某些符号信息可能存在)。原文提到 “shellcode 字节数组也被泄露了”,这可能是指其内容(如果未正确混淆)或变量名本身(在调试版本或特定符号表中)。
移除
iostream并替换有意义标识符后的 C++ 代码:C++
#include "windows.h" // 不再使用 iostream 和 string int main(int argc, char* argv[]) { unsigned char awoler[] = { /* ... shellcode bytes ... */ }; // 无意义变量名 HANDLE awerfu; // 无意义变量名 HANDLE rwfhbf; // 无意义变量名 PVOID iauwef; // 无意义变量名 awerfu = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1]))); iauwef = VirtualAllocEx(awerfu, NULL, sizeof awoler, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE); WriteProcessMemory(awerfu, iauwef, awoler, sizeof awoler, NULL); rwfhbf = CreateRemoteThread(awerfu, NULL, 0, (LPTHREAD_START_ROUTINE)iauwef, NULL, 0, NULL); CloseHandle(awerfu); return 0; }结果: 移除了所有
cout输出后,相关的提示字符串将不再存在于编译后的文件中。将变量名替换为无意义的名称 (如awoler,awerfu),虽然在编译后的 Release 版本中这些局部变量名本身通常不会直接存储为字符串,但这样做可以防止在调试信息、符号文件或源代码泄露时暴露其用途。核心目标是最小化二进制文件中任何可直接识别的、与程序功能相关的字符串信息。
示例 2: 解释型语言 (PowerShell) - BRC4 套件中 Badger PowerShell 加载器片段
以下片段展示了 PowerShell 脚本中如何使用无意义的变量名和简单的字节数组运算 (加法) 来混淆字符串(如函数名、模块名)。
PowerShell
# Set-StrictMode -Version 2 # 脚本通常会包含严格模式设置 [Byte[]] $Ait1m = @(0x3d, 0x50, 0x51, 0x57, 0x50, 0x4e, 0x5f, 0x50, 0x4f, 0x2f, 0x50, 0x57, 0x50, 0x52, 0x4c, 0x5f, 0x50) # "RandomAssemblyName" - 21 [Byte[]] $ahv3I = @(0x34, 0x59, 0x38, 0x50, 0x58, 0x5a, 0x5d, 0x64, 0x38, 0x5a, 0x4f, 0x60, 0x57, 0x50) # "InMemoryModule" - 21 # ... (其他大量类似的字节数组定义) ... [Byte[]] $xee2N = @(0x56, 0x50, 0x5d, 0x59, 0x50, 0x57, 0x1e, 0x1d) # "kernel32" - 21 [Byte[]] $AD0Pi = @(0x41, 0x54, 0x5d, 0x5f, 0x60, 0x4c, 0x57, 0x2c, 0x57, 0x57, 0x5a, 0x4e) # "VirtualAlloc" - 21 # 解混淆函数 (每个字节加上一个固定的值,这里是21的逆运算,即减去21,但原文示例是+21) function Get-Robf ($b3tz) { $aisN = [System.Byte[]]::new($b3tz.Count) for ($x = 0; $x -lt $aisN.Count; $x++) { $aisN[$x] = ($b3tz[$x] - 21) # 假设原始字节是 (char_code - 21) # 原文是 ($b3tz[$x] + 21),如果字节数组是这样生成的,那么这里应该是 -21 } return [System.Text.Encoding]::ASCII.GetString($aisN) } # ... (后续代码使用 Get-Robf 解混淆字节数组来获取函数名和模块名) ... # 例如: $vbuf = ([System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer( # (Get-PA (Get-Robf $xee2N) (Get-Robf $AD0Pi)), # 调用 Get-PA("kernel32", "VirtualAlloc") # ... # )).Invoke(...)讨论:
这种方法通过将所有关键字符串(如 “kernel32”, “VirtualAlloc”)存储为经过简单算术运算(例如,每个字符的ASCII码减去一个固定值)后的字节数组,并在运行时通过一个解混淆函数 (如 Get-Robf) 来还原它们。变量名本身也使用随机生成的无意义名称 (如 $Ait1m, $ahv3I)。
有些 cmdlets 和函数保持其原始状态, 这样做是为了在混淆程度和行为可疑度之间取得平衡。如果恶意软件开发者对所有 cmdlet 和函数都进行深度混淆,那么脚本在解释型语言(如PowerShell)中执行时,其代码的熵 (Entropy)(随机性或复杂度)会显著增加,这本身就可能触发某些 EDR 或行为分析引擎的警报(高熵通常是可疑活动的指标)。此外,过度混淆的脚本在日志中也可能显得异常,更容易引起管理员的注意。因此,攻击者可能会选择性地混淆最关键的部分,而保留一些常用 cmdlet 的原始形式。
2. 代码结构 (Code Structure)
在处理恶意代码的各个方面时,代码结构本身如果处理不当,也可能成为被分析和检测的弱点。即使在编译型语言中,不良的或特征明显的代码结构也可能导致签名匹配或简化反汇编分析。
无用代码和代码重排 (Junk Code and Code Reordering):
正如《分层混淆分类法》中所述,这些技术常被用作增加解释型程序(如PowerShell, VBScript)复杂性的附加手段。由于解释型程序的源代码更易于访问,分析师可以相对容易地理解其逻辑。通过插入无用代码(不影响程序主要功能的冗余指令或代码块)和打乱代码执行顺序(例如,将功能块拆分并用跳转指令连接),可以人为地增加分析难度,迫使分析师花费更多时间来梳理程序的真实意图。
相关代码的分离 (Separation of Related Code):
这种技术会影响解释型和编译型语言的分析。它指的是将逻辑上紧密相关的代码片段(例如,一个完整功能的不同组成部分,或一系列连续的API调用)分散到程序的不同位置。启发式签名引擎有时会根据一组连续出现的、具有特定上下文的功能或API调用序列来判断程序是否恶意。通过随机化这些相关代码的出现顺序或物理位置,攻击者可能使引擎误以为这些是独立、安全的操作,从而绕过基于序列或上下文的启发式检测。
3. 文件与编译属性 (File and Compilation Attributes)
编译二进制文件的一些看似次要的方面,如编译选项和生成的符号信息,也可能为分析师提供便利,从而成为攻击者需要注意的混淆点。
调试版本与符号文件 (Debug Builds and Symbol Files):
如果程序是作为调试版本 (Debug build) 编译的,编译器通常会包含大量的符号文件 (Symbol files)(如 .pdb 文件)。符号信息对于调试二进制映像非常有帮助,它可以包含:
- 全局和局部变量的名称及类型。
- 函数的名称、参数和入口点。
- 源代码行号与机器指令的对应关系。 如果攻击者不慎以调试模式编译并分发了恶意软件,这些信息将极大地帮助分析师理解代码逻辑。
移除符号 (Stripping Symbols):
攻击者必须意识到这些潜在的信息泄露风险,并确保采用正确的编译实践。
- 编译器选项: 在像 Visual Studio 这样的集成开发环境中,将编译目标从 “Debug” 更改为 “Release” 通常会自动移除大部分调试符号,并进行代码优化。使用更轻量级的编译器(如 mingw-gcc for C/C++)时,也需要注意相关的编译和链接选项。
- 后期处理工具: 如果需要从一个已经预编译的二进制镜像中移除符号信息,可以使用命令行工具,例如 Unix/Linux 系统中的
strip命令(对于ELF等格式),或适用于Windows PE文件的特定工具。
分类法关联: 《分层混淆分类法》中的“代码元素层” -> “移除冗余符号 (Stripping Redundant Symbols)”方法总结了这类实践。