Windows内部

进程(Processes)

每个进程都提供执行程序所需要的资源

进程具有虚拟空间、可执行代码、系统对象的开放句柄、安全上下文、唯一进程标识符、环境变量、优先级类、最小和最大工作及大小、至少一个执行线程

每个进程都用单个线程(通常称为主线程)启动,但可以从其任何线程创建其他线程

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

从更底层的层面来看,也就是从虚拟地址空间一层面来解释进程

进程在内存中的组件和用途大概如下

组件 用途
代码(Code) 提供进程执行的代码
全局变量(Global Variables) 存储的变量
进程堆(Process Heap) 定义数据存储的堆
进程资源(Process Resources) 定义进程的更多资源
环境块(Environment Block) 用于定义进程信息的数据结构

image-20241229142920993

在任务管理器中的进程信息有这些

组件 作用
Name 定义进程的名称,通常从应用程序处继承而来 conhost.exe
PID 用于标识进程的唯一数值 7408
Status 确定进程的运行方式(正在运行、已挂起等) Running
User name 启动进程的用户。也可表示进程的权限 SYSTEM

线程(Threads)

线程是进程内可计划执行的实体

进程的所有线程共享其虚拟地址空间和系统资源

每个线程都维护异常处理程序、计划优先级、线程本地存储、唯一线程标识符,以及系统将用于保存线程上下文的一组结构,直到计划线程上下文为止

线程上下文包括:线程的计算机的寄存器集、内核堆栈、线程环境块,以及线程进程的地址空间中的用户堆栈

线程还可以有自己的安全上下文,可用于模拟客户端

Windows支持 抢占式多任务处理 ,这将会产生同时执行多个进程中多个线程的效果。在多处理器计算机上,系统可以同时执行与计算机上存在的处理器一样多的线程

用一句话可以概括线程的作用:控制进程的进行

进程的组成大致如下

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

虚拟内存(Virtual Memory)

虚拟内存是Windows内部工作原理的一个重要组成部分

虚拟内存允许其他内部组件与内存交互,就像物理内存一样,但不会有应用程序之间发生冲突的风险

虚拟内存为每个进程都提供了一个专用虚拟地址空间。而内存管理器则用于将虚拟地址转换为物理地址

通过拥有私有虚拟地址空间,而不是直接写入物理内存,能减小进程对内存造成的损害风险

image-20241229155344086

内存管理器也会使用 页(Page) 或者 **交换 (Transfer)**操作来管理内存

应用程序使用的虚拟内存,可能会超过已分配的物理内存。为了解决这个问题,内存管理器会把虚拟内存交换或分页到磁盘上,就像上图一样

  • 在32位x86系统上,理论上最大虚拟地址空间为4 GB
  • 在64位现代系统上,理论上最大虚拟地址空间为256 TB

image-20241229155707886

地址空间被分为上下两个部分

下半部分(0x00000000 - 0x7FFFFFFF)分配给进程

上半部分(0x80000000 - 0xFFFFFFFF)分配给操作系统内存利用

管理员可以通过设置(increaseUserVA)或使用 AWE(Address Windowing Extensions)来改变这种分配布局,以满足需要更大地址空间的应用程序。

动态链接库(DLL)

Dynamic Link Libraries——动态链接库,简写为DLL

在微软的官方文档中,DLL是”一个包含代码和数据的库,可供多个程序共同使用

由于本人先前略微了解过一些PWN的基础知识,这个DLL按我个人浅薄的理解就类似于一个工具箱,需要的时候就建立相应的依赖

比较官方一点的说法 :

DLL 作为 Windows 应用程序执行背后的核心功能之一。

根据 Windows 文档 [What is a DLL - Microsoft Learn](https://learn.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来控制应用程序的执行或者某些方面的功能

例如:

这是来自Visual C++ Win32 Dynamic-Link Library 项目的 DLL 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#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的头文件,它定义了需要导入和导出的函数

1
2
3
4
5
6
7
8
9
#ifndef INDLL_H
#define INDLL_H
#ifdef EXPORTING_DLL
extern __declspec(dllexport) void HelloWorld();
#else
extern __declspec(dllimport) void HelloWorld();
#endif

#endif

DLL可以使用加载时动态链接货2运行时动态链接,来加载到程序中

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

1
2
3
4
5
6
7
#include "stdafx.h"
#include "sampleDLL.h"
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
HelloWorld();
return 0;
}

使用运行时动态链接时,需要使用单独的函数(LoadLibraryLoadLibraryEx)来在运行时加载 DLL。一旦加载,需要使用 GetProcAddress 来识别要调用的导出 DLL 函数。以下是在应用程序中加载和导入 DLL 函数的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
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格式

image-20241229165605582

PE数据的结构大概可以分为七个部分

  • DOS标头(DOS Header):定义文件类型,DOS存根(DOS Stub)则是默认在文件开头运行的程序,用于打印兼容性信息。对于大多数用户来说,这不对文件的任何功能产生任何影响
  • PE文件头(PE File Header):提供二进制文件的PE Header信息。定义文件格式,包含签名和图像文件头,以及其他信息头
  • 图像可选头部(Image Optional Header):是PE File Header的重要组成部分,有欺骗性名称(???)
  • 数据字典(Data Dictionaries):是Image Optional Header的一部分,它们指向图像数据目录结构
  • 节表(Sections Table):定义图像中的可用截面和信息。节用于存储文件的内容,例如代码、导入项和数据

既然文件头已经定义了文件的格式和功能,各个节就能定义文件的内容和数据了

节(Section) 作用
.text 包含可执行代码和入口点
.data 包含已初始化的数据(字符串、变量等)
.rdata或.idata 包含导入项(Windows API)和动态链接库(DLL)
.reloc 包含重定位信息
.rsrc 包含应用程序资源(图像等)
.debug 包含调试信息

与Windows内部交互

Windows内核将控制所有程序和进程,并桥接所有软件和硬件交互

默认情况下,应用程序通常无法与内核交互或者修改物理硬件,需要一个接口。这个问题,Windows用处理器模式和访问级别来解决

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

用户模式和内核模式之间的切换通常由系统和API调用来实现,在文档中,这个点有时候被称为切换点(Switching Point

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

用户模式用户空间 中启动的应用程序将保持该模式,直到通过API进行系统调用或交互。

进行系统调用时,应用程序将切换模式

image-20241230093041888

我们将在本地进程中注入一个消息框,用以演示内存交互

将消息框写入内存的步骤大概如下:

  • 为消息框分配本地进程内存
  • 将消息框写入/复制到分配的内存
  • 从本地进程内存执行消息框

对于第一步,我们可以使用OpenProcess来获取指定进程的句柄

1
2
3
4
5
6
7
// 尝试打开一个指定进程,返回一个进程句柄
HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // 定义对目标进程的访问权限,这里请求所有可能的访问权限
FALSE, // 表示打开的目标进程句柄不会被子进程继承
DWORD(atoi(argv[1])) // 通过命令行参数获取要打开的本地进程的ID。
// atoi函数将命令行参数(字符串形式)转换为整数,再转换为DWORD类型作为进程ID
);

对于第二步,我们可以用payload buffer来分配内存区域

1
2
3
4
5
6
7
8
9
10
// 在目标进程的地址空间中分配内存,并返回分配内存区域的起始地址
LPVOID remoteBuffer = VirtualAllocEx(
hProcess, // 已打开的目标进程句柄,指定要在哪个进程的地址空间中分配内存
NULL, // 通常设为 NULL,表示让系统决定分配内存的起始地址
sizeof(payload), // 要分配的内存区域大小,这里使用了名为 payload 的对象的大小
(MEM_RESERVE | MEM_COMMIT), // 分配类型,同时进行内存保留和提交操作
// MEM_RESERVE:保留指定大小的地址空间,但不实际分配物理内存
// MEM_COMMIT:为保留的地址空间分配物理内存
PAGE_EXECUTE_READWRITE // 内存保护属性,允许对提交的页面进行执行、读取和写入操作
);

第三步:将payload写入分配的内存区域

1
2
3
4
5
6
7
8
9
// WriteProcessMemory函数用于将数据写入到另一个进程的内存空间中
// 以下是对其参数的详细解释
BOOL writeResult = WriteProcessMemory(
hProcess, // 已打开的目标进程句柄,指定要写入数据的进程
remoteBuffer, // 目标进程中已分配的内存区域地址,数据将被写入到此地址开始的内存空间
payload, // 要写入目标进程内存的数据,这里是一个自定义的数据块
sizeof(payload), // 要写入的数据大小(以字节为单位),明确了写入数据的长度
NULL // 用于接收实际写入字节数的变量指针,这里设置为NULL表示不关心实际写入的字节数
);

最后一步:从内存中执行payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// CreateRemoteThread函数用于在指定的远程进程中创建一个新线程
// 以下是对其各个参数的详细注释
HANDLE remoteThread = CreateRemoteThread(
hProcess, // 已打开的目标进程句柄,表明要在哪个进程中创建新线程
NULL, // 指向SECURITY_ATTRIBUTES结构体的指针,用于指定新线程的安全属性。
// 设置为NULL时,新线程将使用默认的安全描述符,并且返回的句柄不能被子进程继承
0, // 为新线程分配的栈空间大小(以字节为单位)。
// 设置为0时,系统会为线程分配默认大小的栈空间
(LPTHREAD_START_ROUTINE)remoteBuffer, // 指向线程函数的指针,该线程函数将在新创建的线程中执行。
// 这里将之前在目标进程中分配内存的地址(remoteBuffer)强制转换为LPTHREAD_START_ROUTINE类型,
// 意味着在这个地址处的代码将作为新线程的起始执行点
NULL, // 传递给线程函数的参数。这里设置为NULL,表示线程函数不需要额外的参数
0, // 线程创建标志。设置为0时,线程在创建后立即开始执行
NULL // 用于接收新线程标识符的变量指针。设置为NULL表示不获取新线程的标识符
);