2-WindowsAPI

一、初识WindowsAPI

PE的相关数据结构WindowsAPI 这两小节非常非常重要。在 PE的相关数据结构 这一小节中,你可能不会直观的感受它的重要性。而在 Windows API 这一节中,你将能够直观地感受到它的重要性,因为这将使你能够实际编写代码并实现功能。

定义Windows API(应用程序编程接口)是由微软提供的一组用于操作系统的功能和服务的接口,允许开发者在 Windows 操作系统上创建应用程序。Windows API 提供了对操作系统功能的访问,包括文件操作、图形处理、用户界面、网络通信等。

简单理解Windows API是一个Windows操作系统提供的可以调用的函数

对于制作木马、病毒等恶意软件,有几个核心的DLL需要特别熟悉

  1. Kernel32.dll:提供对系统功能的访问,如内存管理、进程和线程管理、文件和设备输入输出等。

  2. Ntdll.dll: 是 Windows 操作系统中的一个核心动态链接库(DLL),它提供了许多底层的系统服务和功能。这个库是 Windows NT 操作系统架构的一部分,几乎所有的 Windows 应用程序和系统服务都依赖于它。它也是反病毒软件经常hook的一个核心动态链接库。通过在Ntdll上设置钩子(hook),杀毒软件可以拦截这些API函数的调用,并检查它们的参数和返回值,以便发现潜在的恶意行为。关于 hooking技术我将会在 Hooking技术 这一节中详细讲解。

  3. User 32.dll:处理用户界面相关的功能,包括窗口管理、消息处理、输入事件等

  4. Advapi32.dll:提供高级 API 功能,如注册表访问、安全性和服务管理。

其中 Kernel32.dllNtdll.dll 将在后续章节反复提及,所以请读者引起重视,如果本节的讲解不能让你搞懂如何使用WindowsAPI,还请查阅其他资料,务必搞懂。

如何使用这些API呢?最佳的方法是查阅微软的官方文档: Windows API 索引 - Win32 apps | Microsoft Learnarrow-up-right

下面,我将介绍静态调用WindowsAPI,关于动态调用api,这将在 动态获取api函数 这一节中详细介绍

二、C++调用WindowsAPI

因为微软的官方文档的语法是C++写,所以对于初学者更建议使用C++调用WindowsAPI方式。

2.1 示例

  1. 使用msf生成一个calc的shellcode

  1. 包含必要的头文件 在你的 C++ 源文件中,你需要包含 Windows API 的头文件 windows.h

  2. 编写经典的创建线程注入的代码

2.2 介绍一下涉及到的API

(1)VirtualAlloc

官方文档VirtualAlloc 函数 (memoryapi.h) - Win32 apps | Microsoft Learnarrow-up-right

作用:保留、提交或更改调用进程的虚拟地址空间中页面区域的状态。最常用的功能就是申请一块虚拟内存区域

语法

[!NOTE] 几个数据类型解释

  1. LPVOID 是一个指针类型,表示指向任何类型的指针。它是 void* 的别名,允许指向任何数据类型的内存地址

  2. SIZE_T 是一个无符号整数类型,通常用于表示内存大小或计数。它的大小与平台相关:在 32 位系统上通常为 32 位(4 字节),在 64 位系统上为 64 位(8 字节)。

  3. DWORD 是一个无符号的 32 位整数类型(4 字节)。它是 unsigned long 的别名,通常用于表示状态码、标志和其他需要 32 位整数表示的值。


  1. lpAddress:要分配的区域的起始地址。如果为 NULL,系统将自动选择地址。可以指定一个地址,但如果该地址不合适,分配将失败。

  2. dwSize:要分配的内存块的大小(以字节为单位)。必须大于零。

  3. flAllocationType:分配类型的标志,可以是以下值之一或它们的组合

    • MEM_COMMIT:分配物理内存并将其映射到进程的虚拟地址空间。

    • MEM_RESERVE:保留地址空间,但不分配物理内存。

    • MEM_RESET:重置内存页的状态。

    • MEM_LARGE_PAGES:使用大页面进行分配(需系统支持)。

  4. flProtect:内存保护的标志,可以是以下值之一:

    • PAGE_READONLY:页面是只读的。

    • PAGE_READWRITE:页面可读可写。

    • PAGE_EXECUTE:页面可执行。

    • PAGE_EXECUTE_READ:页面可执行且可读。

(2)RtlMoveMemory

官方文档Wdm.h) (RtlMoveMemory 函数 - Win32 apps | Microsoft Learnarrow-up-right

作用:将源内存块的内容复制到目标内存块,并支持重叠的源内存块和目标内存块。

语法

因为比较简单,所以参数的含义直接看官方文档即可。

(3)CreateThread

官方文档CreateThread 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learnarrow-up-right

作用:在当前进程的虚拟地址空间内创建一个线程,这个线程会执行相应内存区域的代码

语法

  1. lpThreadAttributes:用于指定线程的安全属性。如果为 NULL,则线程句柄无法被继承。可以设置线程的安全性,例如访问控制。

  2. dwStackSize:如果指定为 0,系统将使用默认堆栈大小。可以根据需要设置更大的堆栈大小。

  3. lpStartAddress:指定线程开始执行时的地址。

  4. lpParameter:可以用于传递数据到线程。如果不需要传递参数,可以设为 NULL

  5. dwCreationFlags:可以设置为 0(默认创建线程),或使用 CREATE_SUSPENDED 等标志。CREATE_SUSPENDED 会使线程在创建后处于挂起状态,直到调用 ResumeThread

  6. lpThreadId:用于接收新线程的线程 ID。如果不需要线程 ID,可以将其设置为 NULL

(4)WaitForSingleObject

官方文档WaitForSingleObject 函数 (synchapi.h) - Win32 apps | Microsoft Learnarrow-up-right

作用:用于等待一个指定的对象(在本例中是一个线程)进入信号状态

语法

[!NOTE] 一个数据类型 HANDLE:是 Windows API 中用于表示各种对象(如文件、线程、进程、事件等)的一个数据类型。它是一个指向内部数据结构的引用,允许程序通过这个引用来管理和与操作系统资源进行交互。句柄类型是Windows操作系统中很重要的数据类型,读者感兴趣的话,可以查阅网上的资料,作进一步的了解。

  1. hHandle:这是一个句柄,指向要等待的对象

  2. dwMilliseconds:指定等待的时间,以毫秒为单位。如果设置为 INFINITE,则函数将无限期等待,直到对象进入信号状态。

三、C# 调用WindowsAPI

在C# 中调用Windows API的实质是托管代码非托管代码的调用。

在 C# 中调用 Windows API 通常通过 P/Invoke(平台调用)实现。P/Invoke 允许你在 C# 中调用非托管代码(如 Windows API 函数)

托管与非托管,CLR的概念可以详解: 什么是 CLR ? - 知乎arrow-up-right

3.1 CLR

定义

  1. 公共语言运行时(CLR)是一套完整的、高级的虚拟机,它被设计为用来支持不同的编程语言,并支持它们之间的互操作。

  2. 一个(很少见的)完备的编程平台,它指定了一个程序的完整生命周期中所需要的所有细节,从构建、绑定一直到部署和执行。

简单理解:它就是功能极其强大的一个虚拟机、一个编程平台

3.2 托管代码(Managed Code)

托管代码是由托管执行环境(如 .NET CLR,全称 NET Common Language Runtime,公共语言运行时)执行的代码。这个环境负责管理代码的执行,包括内存分配、垃圾回收、安全性和异常处理等。

例子:C#、VB.NET 和 F# 等语言编写的代码都是托管代码。

3.3 非托管代码 (Unmanaged Code)

非托管代码是直接在操作系统上运行的代码,它不受托管执行环境的控制。

例子:用 C 或 C++ 编写的代码通常被视为非托管代码。

3.4 示例

  1. 需要使用到 System.Runtime.InteropServices 这个命名空间(namespace),专门用于提供与非托管代码进行交互的功能。

  2. API声明的模板

  1. 一个弹出calc的例子(加载器代码只适用于32位程序,具体原因未知)

因为WindowsAPI是C++写的,而我们的加载器是C# 写的,所以这就涉及到我们的函数参数的数据类型要与WindowsAPI的参数类型一致。如何确定函数参数的类型呢?可以参照VS的提示,进行数据类型的匹配就可以了。比如 VirtualAlloc,鼠标光标悬停,VS就会给出函数参数的类型

运行结果

[!NOTE] 几个数据类型

  1. IntPtr 是 .NET 中的一个结构,主要用于表示一个指针句柄。它的大小与平台相关:在 32 位平台上,IntPtr 是 4 字节(32 位),而在 64 位平台上是 8 字节(64 位)。使用 IntPtr 可以在托管代码和非托管代码之间安全地传递指针和句柄。

  2. IntPtr.Zero 是一个静态属性,表示一个空指针或句柄

  3. uint 是 C# 中的一个无符号整型数据类型,表示一个 32 位的无符号整数。

四、Go调用WindowsAPI

GO语言中调用 Windows API 来分配可执行内存、复制 shellcode、创建线程并执行 shellcode,可以使用 golang.org/x/sys/windows

4.1 示例

注:

  1. 不知道为什么用msf生成的弹出计算器shellcode不起作用了,所以我又换了一个shellcode

  2. 如果不知道api的数据类型,光标悬停在Call函数上,IDE会给出提示。比如RtlCopyMemory.Call的全部参数的类型是uintptr类型的,接下来就是数据类型转换了。

  1. (uintptr)(unsafe.Pointer(&shellcode[0])) 这种数据类型的转换在Go免杀中很常见

    • 首先用 & 运算符获得数组第一个元素的地址,即数组首地址。

    • 然后用 unsafe.Pointer(&shellcode[0])*byte 类型转换成 unsafe.Pointer 类型。

    • 最后将 unsafe.Pointer 转换成 uintptr 类型。

  2. 为什么要这么做?因为 *byte 指针类型不能直接转换成 uintptr 无符号整数类型。

  3. & 运算符获得元素的地址是什么类型取决于被运算的变量的类型。如shellcode变量是 []byte 类型,所以 & 运算后得到 *byte 类型的数据。

  4. unsafe.Pointer是一种通用类型的指针,unsafe.Pointer适用的4种情况

    • 任何类型的指针值都可以转换为 Pointer。

    • Pointer 可以转换为任何类型的指针值。

    • uintptr 可以转换为 Pointer。

    • Pointer 可以转换为 uintptr。

Go语言的语法详解官网或者其他教程。 1、入门指南 | Golang 中文学习文档arrow-up-right 2、Go 语言教程 | 菜鸟教程arrow-up-right

运行结果

4.2 在GO免杀中几个值得探讨的问题

(1)&* 运算符

& 是取地址符,放到一个变量前使用就会返回相应变量的内存地址

运行结果

* 是解引用符,在指针类型的变量前面加上 * 号(前缀)来获取指针所指向的内容

运行结果

(2)指针指来指去

接下来我将用一段代码来解释 (uintptr)(unsafe.Pointer(&shellcode[0])) 这个语句背后的数据转换关系

运行结果

  1. &shellcode[0]unsafe.Pointer(&shellcode[0])shellcode2 他们的表示的值都是一样的,都表示shellcode数组的第一个元素的内存地址,只是他们的数据类型不同而已,不同类型的数据,编译器对数据的解释方法不同。&shellcode[0] 表示 *byte 类型的指针。unsafe.Pointer(&shellcode[0]) 表示通用类型的指针。shellcode2是 uintptr 是内存地址的整数形式,它不能直接当作指针来使用。

  2. 指针类型决定了指针进行解引用操作时,访问空间的大小。比如说 *int 表示了一次解应操作能访问的范围是 int 类型大小;*byte 表示了一次解应操作能访问的范围是 byte 类型大小。

  3. 指针就是地址,指针变量是用来存放指针的。解应用的本质是解析存放在指针变量里的指针,得到指针所指向的内存空间的内容

  4. 以上内容,如果觉得不好理解,那就等自己知识积累足够丰富之后再回过头来看就会突然醒悟。在Go免杀中,这一步操作 (uintptr)(unsafe.Pointer(&shellcode[0])) 是相对固定没有很大的变化。

如果想更一步的了解指针,详见:

1、【C语言】深入理解指针(一篇让你完全搞懂指针)_c语言指针通俗理解-CSDN博客arrow-up-right

2、【C 语言指针篇】指针的灵动舞步与内存的神秘疆域:于 C 编程世界中领略指针艺术的奇幻华章-腾讯云开发者社区-腾讯云arrow-up-right

五、不同语言之间重要数据类型的转换关系

C++
C#
Go

HANDLE

IntPtr

windows.Handle()

DWORD

uint

uint32

大部分指针类型

IntPtr

uintptr