Windows内部
Windows内部
进程(Processes)
每个进程都提供执行程序所需要的资源
进程具有虚拟空间、可执行代码、系统对象的开放句柄、安全上下文、唯一进程标识符、环境变量、优先级类、最小和最大工作及大小、至少一个执行线程
每个进程都用单个线程(通常称为主线程)启动,但可以从其任何线程创建其他线程
进程组件 | 作用 |
---|---|
私有虚拟地址空间 | 为进程分配虚拟内存地址 |
可执行程序 | 定义存储在虚拟地址空间中的代码和数据 |
开放句柄 | 定义进程可以访问的系统资源的句柄 |
安全上下文 | 访问令牌,定义用户、权限和其他安全信息 |
进程ID | 进程的唯一数字标识符 |
线程 | 计划执行的进程部分 |
从更底层的层面来看,也就是从虚拟地址空间一层面来解释进程
进程在内存中的组件和用途大概如下
组件 | 用途 |
---|---|
代码(Code) | 提供进程执行的代码 |
全局变量(Global Variables) | 存储的变量 |
进程堆(Process Heap) | 定义数据存储的堆 |
进程资源(Process Resources) | 定义进程的更多资源 |
环境块(Environment Block) | 用于定义进程信息的数据结构 |
在任务管理器中的进程信息有这些
组件 | 作用 | 例 |
---|---|---|
Name | 定义进程的名称,通常从应用程序处继承而来 | conhost.exe |
PID | 用于标识进程的唯一数值 | 7408 |
Status | 确定进程的运行方式(正在运行、已挂起等) | Running |
User name | 启动进程的用户。也可表示进程的权限 | SYSTEM |
线程(Threads)
线程是进程内可计划执行的实体
进程的所有线程共享其虚拟地址空间和系统资源
每个线程都维护异常处理程序、计划优先级、线程本地存储、唯一线程标识符,以及系统将用于保存线程上下文的一组结构,直到计划线程上下文为止
线程上下文包括:线程的计算机的寄存器集、内核堆栈、线程环境块,以及线程进程的地址空间中的用户堆栈
线程还可以有自己的安全上下文,可用于模拟客户端
Windows支持 抢占式多任务处理 ,这将会产生同时执行多个进程中多个线程的效果。在多处理器计算机上,系统可以同时执行与计算机上存在的处理器一样多的线程
用一句话可以概括线程的作用:控制进程的进行
进程的组成大致如下
组件 | 作用 |
---|---|
栈(Stack) | 与线程相关且特定于线程的所有数据(异常、过程调用等) |
线程本地存储(Thread Local Storage) | 用于为唯一数据环境分配存储空间的指针 |
栈参数(Stack Argument) | 分配给每个线程的唯一值 |
上下文结构(Context Structure) | 保护由内核维护的机器寄存器值 |
虚拟内存(Virtual Memory)
虚拟内存是Windows内部工作原理的一个重要组成部分
虚拟内存允许其他内部组件与内存交互,就像物理内存一样,但不会有应用程序之间发生冲突的风险
虚拟内存为每个进程都提供了一个专用虚拟地址空间。而内存管理器则用于将虚拟地址转换为物理地址
通过拥有私有虚拟地址空间,而不是直接写入物理内存,能减小进程对内存造成的损害风险
内存管理器也会使用 页(Page) 或者 **交换 (Transfer)**操作来管理内存
应用程序使用的虚拟内存,可能会超过已分配的物理内存。为了解决这个问题,内存管理器会把虚拟内存交换或分页到磁盘上,就像上图一样
- 在32位x86系统上,理论上最大虚拟地址空间为4 GB
- 在64位现代系统上,理论上最大虚拟地址空间为256 TB
地址空间被分为上下两个部分
下半部分(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 | #include "stdafx.h" |
以下是DLL的头文件,它定义了需要导入和导出的函数
1 | #ifndef INDLL_H |
DLL可以使用加载时动态链接货2运行时动态链接,来加载到程序中
使用加载时动态链接时,应用程序会对DLL函数进行显示调用。只有提供了头文件(.h)和导入库(.lib)文件才能实现这种类型的链接。以下是从应用程序调用导出的 DLL 函数的示例。
1 |
|
使用运行时动态链接时,需要使用单独的函数(LoadLibrary
或 LoadLibraryEx
)来在运行时加载 DLL。一旦加载,需要使用 GetProcAddress
来识别要调用的导出 DLL 函数。以下是在应用程序中加载和导入 DLL 函数的示例。
1 | ... |
在恶意代码中,威胁行为者通常会比使用加载时动态链接更多地使用运行时动态链接。这是因为恶意程序可能需要在内存区域之间传输文件,并且传输单个 DLL 比使用其他文件要求更容易管理。
可移植可执行文件格式(Portable Executable Format )
可执行文件和应用程序是Windows内部操作的重要组成部分
PE格式定义了关于可执行文件和存储数据的信息,此外,PE格式还定义了数据组件存储结构的方式
PE格式是可执行文件和目标文件的总体结构,PE文件和COFF(通用对象文件格式)文件构成了PE格式
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进行系统调用或交互。
进行系统调用时,应用程序将切换模式
我们将在本地进程中注入一个消息框,用以演示内存交互
将消息框写入内存的步骤大概如下:
- 为消息框分配本地进程内存
- 将消息框写入/复制到分配的内存
- 从本地进程内存执行消息框
对于第一步,我们可以使用OpenProcess
来获取指定进程的句柄
1 | // 尝试打开一个指定进程,返回一个进程句柄 |
对于第二步,我们可以用payload buffer来分配内存区域
1 | // 在目标进程的地址空间中分配内存,并返回分配内存区域的起始地址 |
第三步:将payload写入分配的内存区域
1 | // WriteProcessMemory函数用于将数据写入到另一个进程的内存空间中 |
最后一步:从内存中执行payload
1 | // CreateRemoteThread函数用于在指定的远程进程中创建一个新线程 |