x86 反汇编/Microsoft Windows
Windows 操作系统 是一个受欢迎的逆向工程目标,原因很简单:操作系统本身(市场份额,已知弱点)及其大多数应用程序都不是开源或免费的。Windows 机器上的大多数软件都没有捆绑源代码,而且大多数软件的文档不足或根本没有文档。有时,了解软件功能的唯一方法(或者确定某个软件是恶意软件还是合法软件)是进行逆向工程并检查结果。
Windows 操作系统可以很容易地分为两类:Windows9x 和 WindowsNT。
Windows9x 内核最初是为了跨越 16 位 - 32 位边界而编写的。基于 9x 内核的操作系统是 Windows 95、Windows 96、Windows 98 和 Windows Me。Windows9x 系列操作系统以容易出现错误和系统不稳定而闻名。操作系统本身实际上是其前身 MS-DOS 的 32 位扩展。9x 系列的一个重要问题是它们都基于使用 ANSI 格式来存储字符串,而不是 Unicode。
Windows Me 发布后,Windows9x 内核的开发就结束了。
WindowsNT 内核系列最初是作为企业级服务器和网络软件编写的。WindowsNT 比 Windows9x 内核更强调稳定性和安全性(尽管可以争论这种强调是否足够)。它还在内部使用 Unicode 处理所有字符串操作,在使用不同语言时提供了更大的灵活性。基于 WindowsNT 内核的操作系统是:Windows NT(版本 3.1、3.11、3.2、3.5、3.51 和 4.0)、Windows 2000(NT 5.0)、Windows XP(NT 5.1)、Windows Server 2003(NT 5.2)、Windows Vista(NT 6.0)、Windows 7(NT 6.1)、Windows 7.1(NT 6.11)、Windows 8(NT 6.2)、Windows 8.1(NT 6.3)和 Windows 10(NT 10.0)。
Microsoft Xbox 和 Xbox 360 也运行 NT 的一个变体,该变体是从 Windows 2000 分叉出来的。Microsoft 未来的大多数操作系统产品都以某种形式基于 NT。
内存被组织成默认大小为 4096 字节的“页”。系统或任何应用程序当前未使用的页面可能会被写入硬盘上的一个特殊区域,称为“分页文件”。使用分页文件可能会提高某些系统的性能,尽管在某些情况下 HDD 的 I/O 延迟较高实际上会降低性能。
32 位 Windows NT 允许每个进程最多使用 4 GiB 的虚拟内存地址空间。默认情况下,这被分成 2 GiB 用户内存和 2 GiB 内核内存。
在某些 32 位版本和版本中,可以使用 /3GB 开关启动操作系统,它将此分成 3 GiB 用户内存和 1 GiB 内核内存。只有使用大型内存标志编译的 32 位应用程序才能在这种模式下使用最多 3 GiB。/3GB 开关在 64 位 Windows 中不受支持,但带有大型内存标志的 32 位应用程序在 64 位 Windows 上可以访问最多 4 GiB。64 位应用程序不受此限制。
从 Pentium Pro CPU 开始,一些 32 位版本和版本可以使用物理地址扩展(/PAE 开关)访问 4 GiB 以上的内存,最多 64 GiB。支持 PAE 的 32 位应用程序(例如,某些版本的 32 位 Microsoft SQL Server 和 32 位 Microsoft Exchange Server)可以访问此内存。但是,需要特殊的配置。
Windows 架构是高度分层的。程序员进行的函数调用可能在实际执行任何操作之前被重定向 3 次或更多次。从用户模式应用程序调用 Win32 函数会产生不可忽略的性能损失。但是,其好处同样不可忽略:在 Windows 系统的较高层编写的代码更容易编写。涉及初始化多个数据结构和调用多个子函数的复杂操作可以通过只调用一个单一的更高层函数来完成。
Win32 API 包含 3 个模块:KERNEL32、USER32 和 GDI32。KERNEL32 建立在 NTDLL 之上,大多数对 KERNEL32 函数的调用只是被重定向到 NTDLL 函数调用。USER32 和 GDI32 都基于 WIN32K(一个内核模式模块,负责 Windows 的“外观和感觉”),尽管 USER32 也对 GDI32 中更原始的函数进行了许多调用。这些以及 NTDLL 都为 Windows NT 内核 NTOSKRNL 提供了一个接口(见下文)。
NTOSKRNL 也部分建立在 HAL(硬件抽象层)之上,但本书不会过多考虑这种交互。这种分层的目的是允许将处理器变体问题(例如资源位置)与内核本身区分开来。因此,稍有不同的系统配置只需要一个不同的 HAL 模块,而不是一个完全不同的内核模块。
在经过不同层级的子程序过滤后,大多数 API 调用都需要与操作系统的一部分进行交互。服务是通过“软件中断”提供的,传统上是通过“int 0x2e”指令提供的。这将执行控制权切换到 NT 执行程序/内核,在那里处理请求。这里应该指出,内核模式使用的堆栈与用户模式堆栈不同。这在内核和用户之间提供了一层额外的保护。函数完成后,控制权将返回到用户应用程序。
英特尔和 AMD 都提供了一组额外的指令,以允许更快的系统调用,英特尔的“SYSENTER”指令和 AMD 的 SYSCALL 指令。
WinNT 和 Win9x 系统都使用 Win32 API。但是,WinNT 版本的 API 具有更多功能和安全结构,以及 Unicode 支持。大多数 Win32 API 可以分解为 3 个独立的组件,每个组件执行一个独立的任务。
Kernel32.dll 是 KERNEL 子系统的所在地,它实现了非图形函数。KERNEL 中的一些 API 是:堆 API、虚拟内存 API、文件 I/O API、线程 API、系统对象管理器和其他类似系统服务。kernel32.dll 的大部分功能在 ntdll.dll 中实现,但在未公开的函数中。Microsoft 倾向于发布 kernel32 的文档并保证这些 API 不会改变,然后将大部分工作放在其他库中,这些库随后不会被记录。
gdi32.dll 是实现 GDI 子系统的库,在那里执行原始图形操作。GDI 将其大部分调用转移到 WIN32K,但它确实包含一个 GDI 对象管理器,例如笔、画刷和设备上下文。GDI 对象管理器和 KERNEL 对象管理器是完全分开的。
USER 子系统位于 user32.dll 库文件中。该子系统控制 USER 对象的创建和操作,USER 对象是常见的屏幕项目,例如窗口、菜单、光标等。USER 将设置要绘制的对象,但通过调用 GDI(进而多次调用 WIN32K)来执行实际的绘制操作,或者有时甚至直接调用 WIN32K。USER 使用 GDI 对象管理器。
原生 API,在此称为 NTDLL 子系统,是一系列未公开的 API 函数调用,它处理 KERNEL32 执行的大部分工作。微软也不保证原生 API 在不同版本之间保持一致,因为 Windows 开发人员会修改软件。这带来了原生 API 调用在未经警告的情况下被删除或更改的风险,从而破坏了使用它的软件。
NTDLL 子系统位于 ntdll.dll 中。该库包含许多 API 函数调用,它们都遵循特定的命名方案。每个函数都有一个前缀:Ldr、Nt、Zw、Csr、Dbg 等,所有具有特定前缀的函数都遵循特定的规则。
"官方" 原生 API 通常仅限于前缀为 Nt 或 Zw 的函数。这些调用实际上在用户模式下是相同的:相关的 导出条目 映射到内存中的同一个地址。但是,在内核模式下,Zw* 系统调用存根将先前模式设置为内核模式,确保不执行某些参数验证例程。前缀 "Zw" 的来源尚不清楚;这个前缀是由于它本身没有任何意义而选择的[1]。
在实际实现中,系统调用存根只是加载两个寄存器,其中包含描述原生 API 调用的所需值,然后执行软件中断(或sysenter
指令)。
大多数其他前缀都很模糊,但已知的包括
- Rtl 代表 "运行时库",这些调用在运行时提供帮助功能(例如 RtlAllocateHeap)
- Csr 代表 "客户端服务器运行时",它表示位于 csrss.exe 中的 win32 子系统的接口
- Dbg 函数存在于启用调试例程和操作
- Ldr 提供了从 DLL 和其他模块资源加载、操作和检索数据的能力
许多函数,尤其是运行时库例程,在 ntdll.dll 和 ntoskrnl.exe 之间共享。大多数原生 API 函数以及从内核导出的其他仅限内核模式的函数对于驱动程序编写者非常有用。因此,微软在 Microsoft Server 2003 Platform DDK 中提供了关于许多原生 API 函数的文档。DDK(驱动程序开发工具包)可以免费下载。
该模块是 Windows NT 的 "'执行器'",它提供了原生 API 以及内核本身所需的所有功能,内核本身负责维护机器状态。默认情况下,所有中断和内核调用都以某种方式通过 ntoskrnl 传递,使其成为 Windows 本身中最重要的程序。它的许多函数都是为了供设备驱动程序使用而导出的(所有这些函数都带有各种前缀,类似于 NTDLL)。
该模块是 "Win32 内核",它位于更低级别、更原始的 NTOSKRNL 之上。WIN32K 负责 Windows 的 "外观和感觉",该代码的许多部分自 Win9x 版本以来基本保持不变。该模块提供了许多导致 USER 和 GDI 按预期方式运行的具体指令。它负责将来自 USER 和 GDI 库的 API 调用转换为您在显示器上看到的图片。
随着 64 位处理器的出现,64 位软件成为必需品。因此,创建了 Win64 API 来利用新的硬件。重要的是要注意,许多函数调用的格式在 Win32 和 Win64 中是相同的,除了指针的大小以及特定于 64 位地址空间的其他数据类型。
微软发布了其 Windows 操作系统的新版本,名为 "Windows Vista"。Windows Vista 可能更广为人知的是它的开发代号 "Longhorn"。微软声称 Vista 基本上是从头开始编写的,因此可以认为 Vista API 和系统架构与以前 Windows 版本的 API 和架构之间存在根本差异。Windows Vista 于 2007 年 1 月 30 日发布。
Windows CE 是微软在小型设备上的产品。它主要使用与桌面系统相同的 Win32 API,但它的架构略有不同。本书中的一些示例可能会考虑 Windows CE。
最近的 Windows Service Pack 试图实现一个名为 "不可执行内存" 的系统,其中某些页面可以被标记为 "不可执行"。该系统的目的是通过不允许将控制权传递给攻击者插入到内存缓冲区中的代码来防止一些最常见的安全漏洞。例如,加载到溢出文本缓冲区中的 shellcode 无法执行,从而阻止了攻击。但是,这种机制的有效性还有待观察。
COM 以及一整套与 COM 相关或实际上是带有花哨名称的 COM 的技术,是反向工程 Windows 二进制文件时需要考虑的另一个因素。COM、DCOM、COM+、ActiveX、OLE、MTS 和 Windows DNA 都是同一主题或类似主题的名称,因此它们可以被视为属于同一标题。简而言之,COM 是一种以统一、跨平台和跨语言的方式导出面向对象类的方法。本质上,COM 是 .NET 版本 0 beta。使用 COM,用多种语言编写的组件可以导出、导入、实例化、修改和销毁在另一个文件中(最常是 DLL)定义的对象。虽然 COM 提供了跨平台(在某种程度上)和跨语言功能,但每个 COM 对象都编译为原生二进制文件,而不是像 Java 或 .NET 这样的中间格式。因此,COM 不需要虚拟机来执行此类对象。
由于 COM 的工作方式,通过简单地检查可执行文件,很难感知 COM 组件导出的许多方法和数据结构。如果创建程序员使用了 ATL 等库来简化他们的编程体验,情况会更糟。不幸的是,对于反向工程师来说,这会将可执行文件的内容简化为 "比特海洋",到处都是指针和数据结构。
RPC 是一个通用术语,指的是允许运行在一部机器上的程序调用实际在另一部机器上执行的函数的技术。通常情况下,这通过将所有需要用于该过程的数据(包括存储在第一台机器上的任何状态信息)进行 *序列化*,并将它们构建成单个数据结构,然后通过某种通信方式传输到第二台机器来实现。第二台机器然后执行请求的动作,并返回包含任何结果和可能改变的状态信息的数据包到发起机器。
在 Windows NT 中,RPC 通常通过两个同名库来处理,其中一个库生成 RPC 请求并接收 RPC 返回,如用户模式程序请求的那样,另一个库响应 RPC 请求并通过 RPC 返回结果。一个典型的例子是打印后台处理程序,它包含两个部分:RPC 存根 spoolss.dll 和后台处理程序本身以及 RPC 服务提供程序 spoolsv.exe。在大多数独立的机器中,使用两个模块通过 RPC 通信似乎有些过度,为什么不简单地将它们合并成一个单一的例程?然而,在网络打印中,这种设计是有意义的,因为 RPC 服务提供程序可以驻留在远离本地机器的远程机器上,该机器具有远程打印机,本地机器可以完全以与控制本地机器上的打印机相同的方式控制远程机器上的打印机。