Written by Nicole Fishbein - 13 March 2024
翻译:梦幻的彼岸
原文地址:https://intezer.com/blog/incident-response/intro-to-malware-net-executable-file/
欢迎深入了解 .NET 恶意软件逆向工程世界。作为一名安全研究员或分析师,您可能知道,.NET 框架 以其快速、强大的应用程序开发能力而闻名,但它也是一把双刃剑。.NET框架对合法开发者具有吸引力的特性,也使其成为恶意软件作者的最爱。
那么,为什么要投入时间和精力来破解 .NET 恶意软件呢?简而言之,网络威胁环境中充斥着使用 .NET 框架构建的恶意软件,要应对这些威胁,就必须深入了解其底层代码结构,并具备分析其操作复杂性的能力。掌握了相关知识和逆向工程技能,您就能解读恶意软件的机制,发现其攻击载体,并加强对这些威胁的防御。
本博客旨在揭开.NET逆向工程过程的神秘面纱,使其更加平易近人、易于理解。我们的旅程将使您掌握分析 .NET 文件的基本知识,并深入了解其有害功能和行为。首先,我们将深入探讨 .NET 可执行文件的格式,让您对这些文件的结构有一个扎实的了解。随后,我们将广泛介绍可用于逆向工程 .NET 恶意软件的工具和技术,确保您为应对这些威胁做好充分准备。
微软公司开发的 .NET 软件开发框架和生态系统于 2002 年首次发布。它旨在提供一个可控的编程环境,让软件可以在基于 Windows 的操作系统上开发、安装和执行。随着.NET Core 的推出,.NET 5 及以后的版本也接替了.NET Core ,.NET 已经扩展到为 Linux 和 macOS 提供跨平台支持。与传统编程语言不同,.NET 不是一种语言,而是一个框架,支持多种托管语言,包括 C#、VB.NET 和 F#。
.NET框架包括一个称为框架类库(FCL)的大型类库,它为开发人员提供了从数据访问、加密到XML解析等一系列全面的即用、经过测试和优化的功能。这种广泛的支持和托管执行环境使.NET有别于需要更多人工干预此类任务的语言和框架。将这些功能结合在一起,就能创建一个高效的环境,适用于从网络、桌面到移动的各种应用。
与 C/C++ 相比,.NET 框架具有用户友好的开发流程、丰富的功能集以及与 Windows 的平滑集成,因此恶意软件开发者可能更喜欢使用.NET 框架。然而,dnSpy 等工具使基于 .NET 的恶意软件的逆向工程变得更加简单,这促使这些创建者采用混淆方法来增加分析难度。此外,.NET 能够使恶意软件改变其行为或隐藏起来,这也增加了检测和逆向工程的难度。虽然 C/C++ 允许对系统资源进行更精细的控制,并能开发出更隐蔽、更高效的恶意软件,但它需要对系统的内部运作有更深入的了解。因此,.NET 对那些希望在开发速度和简便性之间寻求平衡的人来说更有吸引力。
.NET威胁环境在不断演变,攻击者经常利用适应性强、被广泛采用的.NET框架来制作和部署各种复杂的威胁。这种框架是许多网络威胁的基础,如臭名昭著的勒索软件Locky 和Killnet 。包括RedLine Stealer 在内的凭证窃取程序和CryptoClippy 等银行木马也是基于 .NET 的威胁。此外,用.NET编写的破坏性擦除程序也在不断涌现,DoubleZero 和最近发现的Hatef Wiper 就说明了这一趋势。
此外,.NET 还有助于创建远程访问木马(RAT)。这方面的例子包括QuasarRAT 和NanoCore ,它们因功能丰富、易于修改和混淆而受到地下圈子的称赞。此外,.NET 也是创建恶意软件加载器的常用工具,这些加载器可以隐蔽地安装和执行其他类型的恶意软件。
编译 - 管理代码
C#、F# 或 VB.NET等语言的执行由运行时控制。当合适的语言编译器编译源代码时,输出是中间语言(IL),也称为 MSIL(微软中间语言)、托管代码或通用中间语言(CIL)。
例如,在 .NET 框架中编译 C# 代码时,C# 编译器 (csc.exe) 的输出是一个 .NET 程序集。该程序集可以是独立程序的可执行文件(EXE),也可以是可重用库的动态链接库(DLL)。
这与 C/C++ 等非托管语言的编译不同,在非托管语言中,源代码被直接翻译成机器代码。然后,托管代码 被打包成一个程序集,并附带一个包含必要元数据的清单。
在此过程中,托管代码的优点在于其可移植性和灵活性;同一个程序集可以在.NET支持的任何平台上运行,而无需重新编译。此外,托管代码允许跨语言继承代码访问的安全性。它还具有支持后期绑定的优势,方法调用可以在运行时而不是编译时解决。托管代码和程序集结构提供的这种抽象级别是.NET 框架的多功能性和优势的基石,使开发人员能够创建更安全、更易管理、更能适应变化的 应用程序。
下图演示了 .NET 的编译和执行过程。我们使用 C# 进行演示,但它适用于所有 .NET 语言。
Compilation and execution of .NET.
Source
通用语言运行时(CLR)是 Microsoft .NET 框架的重要组成部分,用于管理 .NET 程序的执行。它本质上是一个执行引擎,提供运行 .NET 应用程序所需的各种服务,无论这些程序是用哪种编程语言编写的。
执行 .NET 二进制程序时,CLR 会设置执行环境,但不会立即将所有托管代码转换为本地机器代码。即时编译器(JIT)会根据需要将托管代码转换为本地代码,并在调用方法时对其进行编译。这确保了在特定硬件上的高效执行。此外,.NET Core 和 .NET 5+ 中的NGEN 和AOT 编译等技术可以在执行前将托管代码预编译为本地代码,从而进一步提高性能。
CLR 的功能不仅限于执行应用程序,它还提供内存管理、 异常处理、垃圾回收、类型安全检查和安全性等关键服务。由 CLR 协调的内存管理抽象了开发人员手动分配和释放内存的需要,从而大大减少了内存泄漏和相关错误。所提供的自动垃圾回收功能可管理对象的生命周期,通过删除应用程序不再使用的对象来回收内存。
此外,CLR 执行严格的类型安全,有助于确保应用程序不会尝试执行不安全或未经验证的操作。CLR 还在 .NET 的安全架构中扮演重要角色,它提供代码访问安全(CAS),可根据分配给应用程序的信任级别控制程序可访问的资源。总之,CLR 创建了一个高级环境,减少了传统编程语言所需的许多低级编程任务,从而加快了开发周期,提高了生产率,并使应用程序更加安全可靠。这使得 CLR 成为 .NET 生态系统不可或缺的组成部分。
.NET框架中的非托管函数是指在通用语言运行时(CLR)的托管环境之外运行的代码。这些函数通常用 C 或 C++ 等语言编写,绕过 CLR 的管理,直接编译成特定于机器的代码。这意味着.NET托管环境固有的自动垃圾回收、类型安全和异常处理等功能并不适用于这些非托管函数。它们主要用于互操作性目的,允许.NET 应用程序使用非.NET 兼容语言编写的遗留代码或外部库。当需要使用现有的非.NET 库或调用只能通过非托管代码访问的系统级 API 时,这种功能是必不可少的。
不过,这也增加了复杂性和责任,因为开发人员必须手动处理内存管理和错误处理,增加了内存泄漏和安全漏洞等问题的可能性。在恶意软件分析中,了解非托管函数至关重要,因为它们可用于执行绕过托管环境中某些保护措施的代码,从而给分析和检测带来独特的挑战。
示例 - 非托管函数
要创建一个使用非托管函数的简单 .NET 程序,我们可以使用 C# 中的平台调用服务 (PInvoke)。PInvoke 允许托管代码从动态链接库(DLL)中调用非托管函数。
下面是一个示例,我们将调用标准 Windows 库 user32.dll 中的 MessageBox 函数:
让我们探究一下上述代码的流程:
.NET 程序 集是 .NET 应用程序的基本构件,是一个或多个代码模块或资源文件的集合。生成的程序集内容包括
在本节中,我们将深入研究 .NET 内部结构。为了演示我们所涉及的概念,我们将使用一个示例 --臭名昭著的旭日 (SolarWinds),以便您能跟上。Hash: 32519b85c0b422e4656de6e6c41878e95fd95026267daab4215ee59c107d6c77
我们将在演示中使用dnSpy 、ILSpy 和PEStudio 。
.NET程序集中的运行时头文件是 CLR 指定使用的可移植可执行文件(PE)格式中的重要元素。它包含了 CLR 正确执行 .NET 程序集所需的元数据和关键细节。该运行时标头是 PE 标头中的第 15 个数据目录条目,称为 "CLR 运行时标头"。
PE 文件中的数据目录就像索引或目录一样,列出重要部分并提供有关其位置和大小的信息。这种结构可以有效地访问 PE 文件的不同部分,如导入和导出表、资源,尤其是 .NET 程序集的 CLR 运行时头文件。
该条目描述了运行时头文件的相对虚拟地址(RVA)及其大小,从而引导 CLR 在加载时使用该头文件来管理 .NET 程序集的执行。
下面的截图显示了第 15 个数据目录条目,其标识为 .NET。
Analysis of a .NET file in PeStudio.
在该部分之后,我们将进入元数据头,它在组织这些数据流方面起着至关重要的作用。它提供了一个目录,列出了每个数据流、其大小以及在元数据部分中的偏移量。当 CLR 或像 dnSpy 或 ILDasm 这样的工具需要访问一段元数据时,它会查阅元数据头来找到相应的流。然后,它会导航到该数据流中的正确位置来读取数据。接下来,让我们来看看 .NET 元数据头内容中的一些关键字段:
在某些.NET程序集查看器中访问可执行文件的元数据可能比较麻烦,因为它们的表示方法不同。下面的截图显示了 dnspy 和 ILspy 的区别。
ILSpy 中的元数据表
The metadata tables in dnspy.
.NET 中的元数据是一组描述程序元素及其特征的二进制信息。其中包括代码中定义的类型(类、接口、枚举等)、成员定义(方法、属性、字段、事件)、对其他类型和成员的引用以及程序集本身的信息。
元数据在 PE 文件中被分为多个表,统称为元数据表,用于存储这些描述性信息。每个表都遵循特定的模式,该模式概述了所含数据的结构和性质。下面是元数据表中的一些主要信息类型:
定义表: 包含当前程序集中定义的代码信息。其中包括
Reference 表: 包含程序集外部但被程序集引用的代码信息。这包括
Manifest Metadata 表: 介绍程序集本身,包括
Module 表: 有关当前模块的信息,如名称和唯一标识该模块的 GUID(全球统一标识符)。
CustomAttribute 表: 包含应用于组件内各种元素的自定义属性的详细信息。
Event 表 和 Property 表: 描述类型中声明的事件和属性。
Param 表: 方法参数信息
StandAloneSig 表: 独立签名可用于封装类型或方法签名。
Constant 表: 存储代码中定义的常量。
这些表对 CLR 的运行至关重要,因为它们提供了执行程序集所需的上下文信息。运行时读取这些表以执行各种任务,如类型实例化、方法调用、安全验证等。
这些元数据表以高度优化的二进制格式编码,运行时可对其进行高效处理。通过反射,还可以以编程方式访问元数据,允许 .NET 应用程序在运行时检查自己的结构或其他程序集的结构。这种自省能力是 .NET 框架的强大功能之一,可以实现一系列动态编程方案。
元数据标记是 CLR 用来引用程序集元数据表中元数据元素的唯一标识符。这些表中的每个条目都会分配一个元数据标记,作为对该特定项目的稳定引用。
元数据标记对于 CLR 与编译代码的交互至关重要,因为它们为运行时提供了一种有效识别和访问元数据的方法。PE 文件中的每个类型、成员、签名或其他元数据描述符都有一个相应的标记。
在 dnSpy 中,每个方法的声明上方都有一个注释,信息包括Token、RID、RVA 和文件偏移量--如下面的截图所示。
让我们来看看这些字段的含义:
元数据标记的结构可以提高 CLR 在运行时解析引用的性能。例如,当 JIT 编译器需要将 IL 编译为本地代码时,它会使用元数据标记来查找方法签名、类型信息等。元数据标记系统还支持 CLR 的动态特性,如反射。它使各种运行时服务(如类型安全、安全检查和跨语言互操作性)得以有效执行。
以下是 SolarWinds 恶意软件的Start函数。
The token and RID field in of the start function in dnspy.
Token值的高位(0x6)对应元数据表编号 6,即方法(MethodDef)表。标记值的低位是 0x5fa (1530),即方法表中的条目编号。
查看start 方法的元数据下部,0x00058D66 是可执行文件开始处的偏移量,偏移量的值(0x1EC15)是 #String 流中的偏移量。
#字符串流中的偏移量,其中将包含方法名称:Start。让我们看看在 dnSpy 的 Hex 编辑器中是如何显示的:
字符串流中的偏移值。DnSpy 会自动检测字符串的值。
要查看字符串流中的数据,我们将执行以下操作:
在新窗口中,转到偏移量(相对于字符串流的起始位置 - RVA),查看我们要查找的字符串 -起始位置。
The offset in the string Start in the Strings stream.
.NET 清单是 .NET 程序集的关键组件,是描述程序集中各元素如何相互关联的元数据中心。无论程序集是静态的还是动态的,它都嵌入在每个程序集中,并包含程序集运行所需的基本数据,包括版本要求、安全标识、作用域定义以及资源和类引用的解析。
.NET 清单的主要功能是提供全面的元数据描述,以方便程序集的识别、版本控制和依赖性管理。它可以确保程序集是自描述的,有助于类型引用的解析以及将这些引用映射到包含其声明和实现的文件。这对于维护版本控制、确保不同程序集和依赖程序集的组件之间的兼容性尤为重要。
清单中包含了对于程序集的身份标识和运行至关重要的各种信息:
Manifest in dnSpy.
在.NET中,方法主体结构可以用两种格式编码,分别称为 "Tiny "格式和 "Fat "格式,每种格式都根据方法的复杂性和要求有不同的用途。.NET编译器会根据编译方法的复杂程度在Tiny和Fat头之间做出选择。
Tiny 头文件是两种格式中较为简单的一种。当一个方法满足特定条件时,就会使用 tiny 头文件:
tiny 头更加紧凑,并针对小型方法进行了优化。
tiny 头是一个单字节长的数据,其中低 2 位设置为 0x2(二进制 10),表示这是一个 tiny 头,其余 6 位表示方法主体的大小(字节)。这种紧凑的格式可以高效地存储小方法体,减少简单方法的元数据资源占比。
The Fat header is used for more complex method bodies that exceed the limitations of the Tiny header:
Fat 头文件较大,由多个字段组成,其中包括一个标志字段,用于指示方法主体的其他特征(如异常处理条款或局部变量初始化)。
有关这些标头结构的更多详细信息,请查看此 资源。
让我们以 DeleteDiscoveryProfileInternal 函数为例:
点击偏移量(或 RVA),我们将在 HEX 视图窗口中看到该方法的标题:
在这里我们可以看到,一旦我们将鼠标悬停在页眉的字节上,DnSpy 就会高亮显示页眉字段。函数的内容--指令紧随头之后,在 dnSpy 中表示为 image_core_ilmethod_fat.instruction[]。为了更好地理解和查看指令值(操作码),我们将在 IDA 中打开恶意软件:
The function in IDA.
左边我们可以看到每个操作码的值,后面是 dnSpy 的前两条指令:
在对.NET可执行文件结构的初步探索中,我们深入研究了.NET框架的复杂性,强调了它在合法开发和恶意软件制作中的双重用途。通过剖析 .NET 编译过程、运行时执行以及元数据和程序集的复杂细节,我们为了解 .NET 应用程序如何运行以及如何被恶意操纵奠定了基础。在深入学习.NET恶意软件逆向工程技术的过程中,这将为您掌握有效分析和应对基于.NET的威胁的技能和知识奠定基础。
标识符类型表
非原文内容,只是收集相关知识点的资料方便阅读理解
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2024-3-15 07:18
被梦幻的彼岸编辑
,原因: