Introduction

操作系统背后蕴含的技术和架构比我们最初看到的要多得多。在这个房间里,我们将观察 Windows 操作系统和常见的内部组件。

学习目标

  • 了解并与 Windows 进程及其底层技术交互。

  • 了解核心文件格式及其使用方式。

  • 与 Windows 内部交互并了解 Windows 内核的运行方式。

由于 Windows 机器构成了企业基础设施的大多数,红队需要了解 Windows 内部结构及其可能被(滥用)使用的方式。红队可以在制作攻击性工具或漏洞时(滥用)使用 Windows 来帮助规避和利用。

在开始这个房间之前,请熟悉 Windows 的基本使用和功能。还建议但不要求具备 C++ 和 PowerShell 的基本编程知识。

Processes

进程维护并代表程序的执行;应用程序可以包含一个或多个进程。进程有许多组件,这些组件被分解成多个部分进行存储和交互。Microsoft 文档 分解了这些其他组件,“每个进程都提供执行程序所需的资源。进程具有虚拟地址空间、可执行代码、系统对象的打开句柄、安全上下文、唯一进程标识符、环境变量、优先级类、最小和最大工作集大小以及至少一个执行线程。”这些信息可能看起来令人生畏,但这个房间旨在使这个概念变得不那么复杂。

如前所述,进程是从应用程序的执行中创建的。进程是 Windows 运行的核心,Windows 的大多数功能都可以作为应用程序包含,并具有相应的进程。以下是启动进程的几个默认应用程序示例。

  • MsMpEng (Microsoft Defender)
  • wininit (键盘和鼠标)
  • lsass (凭证存储)

攻击者可以针对进程来逃避检测并将恶意软件隐藏为合法进程。以下是攻击者可能针对进程使用的一些潜在攻击媒介

进程有许多组成部分;它们可以分解为关键特征,我们可以使用这些特征在高层次上描述进程。下表描述了进程的每个关键组成部分及其用途。

进程组件 目的
私有虚拟地址空间 分配给进程的虚拟内存地址。
可执行程序 定义存储在虚拟地址空间中的代码和数据。
打开句柄 定义进程可访问的系统资源的句柄。
安全上下文 访问令牌定义用户、安全组、权限和其他安全信息。
进程 ID 进程的唯一数字标识符。
线程 计划执行的进程部分。

我们还可以从较低层次解释进程,因为它驻留在虚拟地址空间中。下表和图表描述了进程在内存中的样子。

组件 目的
代码 进程要执行的代码。
全局变量 存储的变量。
进程堆 定义存储数据的堆。
进程资源 定义进程的更多资源。
环境块 用于定义进程信息的数据结构。

img

当我们深入研究和滥用底层技术时,这些信息非常有用,但它们仍然非常抽象。我们可以通过在 Windows 任务管理器 中观察它们来使该过程变得有形。任务管理器可以报告有关进程的许多组件和信息。下面的表格简要列出了基本流程详细信息。

值/组件 目的 示例
名称 定义进程的名称,通常从应用程序继承 conhost.exe
PID 用于标识进程的唯一数值 7408
状态 确定进程的运行方式(运行、暂停等) 正在运行
用户名 启动进程的用户。可以表示进程的权限 SYSTEM

这些是您作为最终用户最常与之交互或作为攻击者操纵的内容。

有多个实用程序可用于使观察进程更容易;包括 Process Hacker 2Process ExplorerProcmon

进程是大多数 Windows 内部组件的核心。以下任务将扩展有关进程及其在 Windows 中的使用方式的信息。

Threads

线程是进程使用的可执行单元,根据设备因素进行调度。

设备因素可能因 CPU 和内存规格、优先级和逻辑因素等而异。

我们可以简化线程的定义:“控制进程的执行”。

由于线程控制执行,因此这是一个常见的目标组件。线程滥用可以单独使用来帮助代码执行,也可以更广泛地用于与其他 API 调用链接作为其他技术的一部分。

线程与其父进程共享相同的详细信息和资源,例如代码、全局变量等。线程还具有其独特的值和数据,如下表所示。

组件 用途
堆栈 与线程相关且特定于线程的所有数据(异常、过程调用等)
线程本地存储 用于将存储分配给唯一数据环境的指针
堆栈参数 分配给每个线程的唯一值
上下文结构 保存由内核维护的机器寄存器值

线程可能看起来像是基本而简单的组件,但它们的功能对进程至关重要。

Virtual Memory

虚拟内存是 Windows 内部工作和交互的关键组件。虚拟内存允许其他内部组件与内存交互,就像它是物理内存一样,而不会出现应用程序之间发生冲突的风险。模式和冲突的概念在任务 8 中进一步解释。

虚拟内存为每个进程提供了一个 私有虚拟地址空间。内存管理器用于将虚拟地址转换为物理地址。通过拥有私有虚拟地址空间而不直接写入物理内存,进程造成损坏的风险较小。

内存管理器还将使用 页面传输 来处理内存。应用程序使用的虚拟内存可能多于分配的物理内存;内存管理器会将虚拟内存传输或分页到磁盘以解决此问题。您可以在下图中直观地看到这个概念。

img

img

在 32 位 x86 系统上,理论上的最大虚拟地址空间为 4 GB。

此地址空间被分成两半,下半部分 (0x00000000 - 0x7FFFFFFF) 分配给如上所述的进程。上半部分 (0x80000000 - 0xFFFFFFFF) 分配给操作系统内存利用率。管理员可以通过设置 (increaseUserVA) 或 AWE (Address Windowing Extensions) 为需要更大地址空间的应用程序更改此分配布局。

在 64 位现代系统上,理论上的最大虚拟地址空间为 256 TB。

32 位系统的确切地址布局比率分配给 64 位系统。

大多数需要设置或 AWE 的问题都可以通过增加理论最大值来解决。

您可以在右侧直观地看到两个地址空间分配布局。

虽然这个概念并不直接转化为 Windows 内部结构或概念,但理解它至关重要。 如果理解正确,可以利用它来帮助滥用 Windows 内部结构。

Dynamic Link Libraries

[Microsoft 文档](https://docs.microsoft.com/en-us/troubleshoot/windows-client/deployment/dynamic-link-library#:~:text=A DLL is a library,common dialog box related functions.) 将 DLL 描述为“包含可由多个程序同时使用的代码和数据的库”。

DLL 是 Windows 中应用程序执行背后的核心功能之一。根据 [Windows 文档](https://docs.microsoft.com/en-us/troubleshoot/windows-client/deployment/dynamic-link-library#:~:text=A DLL is a library,common dialog box related functions.),“使用 DLL 有助于促进代码模块化、代码重用、高效内存使用和减少磁盘空间。因此,操作系统和程序加载速度更快、运行速度更快,占用的计算机磁盘空间更少。”

当 DLL 作为程序中的函数加载时,DLL 被指定为依赖项。由于程序依赖于 DLL,因此攻击者可以针对 DLL 而不是应用程序来控制执行或功能的某些方面。

DLL 的创建与任何其他项目/应用程序没有区别;它们只需要稍微修改语法即可工作。以下是来自 Visual C++ Win32 动态链接库项目 的 DLL 示例。

#include "stdafx.h"
#define EXPORTING_DLL
#include "sampleDLL.h"
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
)
{
return TRUE;
}

void HelloWorld()
{
MessageBox( NULL, TEXT("Hello World"), TEXT("In a DLL"), MB_OK);
}

下面是 DLL 的头文件;它将定义导入和导出哪些函数。我们将在本任务的下一部分讨论头文件的重要性(或缺失)。

#ifndef INDLL_H
#define INDLL_H
#ifdef EXPORTING_DLL
extern __declspec(dllexport) void HelloWorld();
#else
extern __declspec(dllimport) void HelloWorld();
#endif

#endif

DLL 已创建,但问题仍未解决,即它们如何在应用程序中使用?

可以使用 加载时动态链接运行时动态链接 将 DLL 加载到程序中。

使用 加载时动态链接 加载时,应用程序会显式调用 DLL 函数。您只能通过提供标头 (.h) 和导入库 (.lib) 文件来实现这种类型的链接。以下是从应用程序调用导出的 DLL 函数的示例。

#include "stdafx.h"
#include "sampleDLL.h"
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
HelloWorld();
return 0;
}

使用运行时动态链接加载时,将使用单独的函数(“LoadLibrary”或“LoadLibraryEx”)在运行时加载 DLL。加载后,您需要使用“GetProcAddress”来识别要调用的导出 DLL 函数。以下是在应用程序中加载和导入 DLL 函数的示例。

...
typedef VOID (*DLLPROC) (LPTSTR);
...
HINSTANCE hinstDLL;
DLLPROC HelloWorld;
BOOL fFreeDLL;

hinstDLL = LoadLibrary("sampleDLL.dll");
if (hinstDLL != NULL)
{
HelloWorld = (DLLPROC) GetProcAddress(hinstDLL, "HelloWorld");
if (HelloWorld != NULL)
(HelloWorld);
fFreeDLL = FreeLibrary(hinstDLL);
}
...

在恶意代码中,威胁行为者通常会更多地使用运行时动态链接,而不是加载时动态链接。这是因为恶意程序可能需要在内存区域之间传输文件,而传输单个 DLL 比使用其他文件要求进行导入更易于管理。

Portable Executable Format

可执行文件和应用程序是 Windows 内部在更高级别上运作的很大一部分。PE(可执行文件和可执行文件)格式定义了有关可执行文件和存储数据的信息。PE 格式还定义了数据组件的存储结构。

PE(可执行文件和可执行文件)格式是可执行文件和目标文件的总体结构。PE(可执行文件和可执行文件)和 COFF(普通文件和可执行文件)文件构成了 PE 格式。

PE 数据最常见于可执行文件的十六进制转储中。下面我们将 calc.exe 的十六进制转储分解为 PE 数据的各个部分。

PE 数据的结构分为七个部分,

DOS Header 定义文件类型

MZ DOS Header 将文件格式定义为 .exe。您可以在下面的十六进制转储部分中看到 DOS 标头。

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ..........ÿÿ..
00000010 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ¸.......@.......
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 E8 00 00 00 ............è...
00000040 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 ..º..´.Í!¸.LÍ!Th

DOS Stub 是一个默认在文件开头运行的程序,它会打印兼容性消息。这不会影响大多数用户使用文件的任何功能。

DOS Stub 会打印消息“此程序无法在 DOS 模式下运行”。DOS Stub 可以在下面的十六进制转储部分中看到。

00000040  0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68  ..º..´.Í!¸.LÍ!Th
00000050 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F is program canno
00000060 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 t be run in DOS
00000070 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 mode....$.......

PE 文件头 提供二进制文件的 PE 头信息。定义文件的格式,包含签名和图像文件头以及其他信息头。

PE 文件头是输出最不可读的部分。您可以从下面十六进制转储部分中的“PE”存根中识别 PE 文件头的开头。

000000E0  00 00 00 00 00 00 00 00 50 45 00 00 64 86 06 00  ........PE..d†..
000000F0 10 C4 40 03 00 00 00 00 00 00 00 00 F0 00 22 00 .Ä@.........ð.".
00000100 0B 02 0E 14 00 0C 00 00 00 62 00 00 00 00 00 00 .........b......
00000110 70 18 00 00 00 10 00 00 00 00 00 40 01 00 00 00 p..........@....
00000120 00 10 00 00 00 02 00 00 0A 00 00 00 0A 00 00 00 ................
00000130 0A 00 00 00 00 00 00 00 00 B0 00 00 00 04 00 00 .........°......
00000140 63 41 01 00 02 00 60 C1 00 00 08 00 00 00 00 00 cA....`Á........
00000150 00 20 00 00 00 00 00 00 00 00 10 00 00 00 00 00 . ..............
00000160 00 10 00 00 00 00 00 00 00 00 00 00 10 00 00 00 ................
00000170 00 00 00 00 00 00 00 00 94 27 00 00 A0 00 00 00 ........”'.. ...
00000180 00 50 00 00 10 47 00 00 00 40 00 00 F0 00 00 00 .P...G...@..ð...
00000190 00 00 00 00 00 00 00 00 00 A0 00 00 2C 00 00 00 ......... ..,...
000001A0 20 23 00 00 54 00 00 00 00 00 00 00 00 00 00 00 #..T...........
000001B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001C0 10 20 00 00 18 01 00 00 00 00 00 00 00 00 00 00 . ..............
000001D0 28 21 00 00 40 01 00 00 00 00 00 00 00 00 00 00 (!..@...........
000001E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Image Optional Header 的名称具有欺骗性,它是 PE File Header 的重要组成部分

Data Dictionaries 是图像可选头的一部分。它们指向图像数据目录结构。

Section Table 将定义图像中可用的部分和信息。如前所述,部分存储文件的内容,例如代码、导入和数据。您可以从下面的十六进制转储部分中的表中识别每个部分的定义。

000001F0  2E 74 65 78 74 00 00 00 D0 0B 00 00 00 10 00 00  .text...Ð.......
00000200 00 0C 00 00 00 04 00 00 00 00 00 00 00 00 00 00 ................
00000210 00 00 00 00 20 00 00 60 2E 72 64 61 74 61 00 00 .... ..`.rdata..
00000220 76 0C 00 00 00 20 00 00 00 0E 00 00 00 10 00 00 v.... ..........
00000230 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 40 ............@..@
00000240 2E 64 61 74 61 00 00 00 B8 06 00 00 00 30 00 00 .data...¸....0..
00000250 00 02 00 00 00 1E 00 00 00 00 00 00 00 00 00 00 ................
00000260 00 00 00 00 40 00 00 C0 2E 70 64 61 74 61 00 00 ....@..À.pdata..
00000270 F0 00 00 00 00 40 00 00 00 02 00 00 00 20 00 00 ð....@....... ..
00000280 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 40 ............@..@
00000290 2E 72 73 72 63 00 00 00 10 47 00 00 00 50 00 00 .rsrc....G...P..
000002A0 00 48 00 00 00 22 00 00 00 00 00 00 00 00 00 00 .H..."..........
000002B0 00 00 00 00 40 00 00 40 2E 72 65 6C 6F 63 00 00 ....@..@.reloc..
000002C0 2C 00 00 00 00 A0 00 00 00 02 00 00 00 6A 00 00 ,.... .......j..
000002D0 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 42 ............@..B

现在,文件头已经定义了文件的格式和功能,文件部分可以定义文件的内容和数据。

部分 目的
.text 包含可执行代码和入口点
.data 包含初始化数据(字符串、变量等)
.rdata 或 .idata 包含导入(Windows API)和 DLL。
.reloc 包含重定位信息
.rsrc 包含应用程序资源(图像等)
.debug 包含调试信息

Interacting with Windows Internals

与 Windows 内部组件交互似乎令人望而生畏,但它已被大大简化。与 Windows 内部组件交互的最易访问和研究最多的选项是通过 Windows API 调用进行交互。Windows API 提供与 Windows 操作系统交互的本机功能。该 API 包含 Win32 API 和不太常见的 Win64 API。

我们将仅简要概述如何使用此房间中与 Windows 内部组件相关的一些特定 API 调用。查看 Windows API 房间 了解有关 Windows API 的更多信息。

大多数 Windows 内部组件都需要与物理硬件和内存交互。

Windows 内核将控制所有程序和进程并桥接所有软件和硬件交互。这一点尤其重要,因为许多 Windows 内部组件需要以某种形式与内存交互。

默认情况下,应用程序通常无法与内核交互或修改物理硬件,并且需要接口。通过使用处理器模式和访问级别可以解决此问题。

Windows 处理器具有 用户内核 模式。处理器将根据访问和请求的模式在这些模式之间切换。

用户模式和内核模式之间的切换通常由系统和 API 调用来实现。在文档中,这一点有时被称为“切换点”。

用户模式 内核模式
无直接硬件访问 直接硬件访问
在私有虚拟地址空间中创建进程 在单个共享虚拟地址空间中运行
访问“拥有的内存位置” 访问整个物理内存

img

在用户模式或“用户空间”下启动的应用程序将保持该模式,直到进行系统调用或通过 API 进行交互。进行系统调用时,应用程序将切换模式。右图是描述此过程的流程图。

在查看语言如何与 Win32 API 交互时,此过程可能会进一步扭曲;应用程序将先通过语言运行时,然后再通过 API。最常见的示例是 C# 在与 Win32 API 交互并进行系统调用之前通过 CLR 执行。

我们将在本地进程中注入一个消息框,以演示与内存交互的概念验证。

将消息框写入内存的步骤概述如下,

  1. 为消息框分配本地进程内存。
  2. 将消息框写入/复制到分配的内存。
  3. 从本地进程内存执行消息框。

在第一步中,我们可以使用“OpenProcess”获取指定进程的句柄。

HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // Defines access rights
FALSE, // Target handle will not be inhereted
DWORD(atoi(argv[1])) // Local process supplied by command-line arguments
);

第二步,我们可以使用“VirtualAllocEx”来分配一个带有有效载荷缓冲区的内存区域。

remoteBuffer = VirtualAllocEx(
hProcess, // Opened target process
NULL,
sizeof payload, // Region size of memory allocation
(MEM_RESERVE | MEM_COMMIT), // Reserves and commits pages
PAGE_EXECUTE_READWRITE // Enables execution and read/write access to the commited pages
);

在第三步,我们可以使用“WriteProcessMemory”将有效负载写入分配的内存区域。

WriteProcessMemory(
hProcess, // Opened target process
remoteBuffer, // Allocated memory region
payload, // Data to write
sizeof payload, // byte size of data
NULL
);

在第四步,我们可以使用“CreateRemoteThread”从内存中执行我们的有效载荷。

remoteThread = CreateRemoteThread(
hProcess, // Opened target process
NULL,
0, // Default size of the stack
(LPTHREAD_START_ROUTINE)remoteBuffer, // Pointer to the starting address of the thread
NULL,
0, // Ran immediately after creation
NULL
);

Conclusion

Windows 内部组件是 Windows 操作系统工作方式的核心。如果不损害操作系统在基本层面上的运行方式,这些内部组件就无法改变。正因为如此,Windows 内部组件是攻击者的有利目标。

正如整个房间所提到的,攻击者可以轻易滥用 Windows 内部组件的功能来达到邪恶的目的。有关此问题的更多信息,请查看滥用 Windows 内部组件房间。

本房间中涵盖的许多概念甚至可以转化为 Unix 对应部分。虽然攻击的细节及其执行方式可能会发生变化,但许多核心概念保持不变。

总体而言,Windows 内部组件将继续存在;红队和蓝队都需要了解它们的功能,包括如何使用它们以及为什么使用它们。