InjLib - 一个实现适用于所有 Windows 版本的远程代码注入的库






4.96/5 (50投票s)
一个实现所有 Windows 版本远程代码注入的库。
引言
在我发表了上一篇文章([2]),解释了如何模拟一些用于远程代码执行的缺失的 Windows 函数之后,接下来的逻辑步骤是使用这些函数作为框架来实现一个允许轻松进行远程代码注入的库。远程代码注入是一种允许在当前进程以外的进程地址空间中执行代码的方法。由于 Windows 的架构将每个进程隔离起来以防止内存覆盖和其他应用程序错误,因此将代码注入到远程进程并非易事。该库实现了允许直接远程代码注入、DLL 远程注入以及 Win32 进程(GUI 和 CUI)和 NT 原生进程的远程子类化的函数。不要指望找到任何创新代码,因为该库主要基于 Robert Kuster 在其文章“将代码注入另一个进程的三种方法”([1]) 中描述的技术。尽管如此,我希望您会发现该库很有用,并在您的项目中使用它。
远程 SEH (结构化异常处理)
所有远程代码执行都通过 SEH 进行保护,以避免任何异常导致远程进程崩溃。您通常在 C/C++ 应用程序中找到的 SEH 代码如下所示:
__try
{
// try code
}
__except(filter-expression)
{
// except code
}
您无法在远程代码中使用此代码,因为这是编译器的 SEH 实现,并且在内部调用位于当前进程的标准库函数 (__except_handler3
)。您需要使用系统级别的 SEH([6])。系统级别的 SEH 被实现为每个线程的回调异常处理函数链表。此列表开头的指针可以从 TIB
(线程信息块)的第一个 DWORD
中检索。FS
段寄存器始终指向当前的 TIB
。实现 SEH 所需的全部就是将一个异常处理程序添加到链表中。最简单形式可以通过以下代码完成:
push addr _exception_handler ; Addr. of our exception handler
push dword ptr fs:[0] ; Addr. of previous handler
mov fs:[0], esp ; Add it to the list
; try code goes here
pop dword ptr fs:[0] ; Remove our handler
add esp, 4 ; Clean up stack
每次在 try
代码块中发生异常时,操作系统都会调用 _exception_handler
例程。最简单形式下,只需要将两个 DWORD
(构成一个 EXCEPTION_REGISTRATION
结构)推入堆栈。当然,没有什么能阻止我们向该结构添加其他数据字段(例如,VC 会推送一个包含五个字段的扩展 EXCEPTION_REGISTRATION
结构)。在我的实现中,我向标准的 SEH 帧添加了两个字段:EBP
寄存器的值以及异常发生后执行应恢复的地址。最终代码将如下所示(您会注意到代码是用汇编编写的。我使用了汇编有两个原因:汇编允许对生成的代码进行更大的控制,并且只有在汇编中才能访问 FS
寄存器):
; Set a new SEH frame
push ebp ; EBP at safe-place (needed for ENTER/LEAVE)
push addr _resume_at_safe_place ; Addr. of safe-place
push addr _exception_handler ; Addr. of our exception handler
push dword ptr fs:[0] ; Addr. of previous handler
mov fs:[0], esp ; Install new SEH handler
; ... try code ...
_resume_at_safe_place:
; Remove SEH frame
pop dword ptr fs:[0] ; Remove SEH handler
add esp, 3*4 ; Remove additional data from stack
EXCEPTION_DISPOSITION __cdecl _exception_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
struct _EXTENDED_EXCEPTION_REGISTRATION *EstablisherFrame,
struct _CONTEXT *ContextRecord,
void *DispatcherContext)
{
ContextRecord->cx_Eax = ExceptionRecord->ExceptionCode;
ContextRecord->cx_Eip = EstablisherFrame->SafeExit;
ContextRecord->cx_Ebp = EstablisherFrame->SafeEBP;
ContextRecord->cx_Esp = EstablisherFrame;
return ExceptionContinueExecution;
}
_exception_handler
恢复 EBP
寄存器,将 EAX
寄存器设置为异常代码,并在 _resume_at_safe_place
处恢复执行。完整的源代码可以在文件“Stub.asm”中找到。
GetProcessInfo()
GetProcessInfo()
函数返回有关进程的宝贵信息,这些信息对于决定可以在该进程中执行哪种类型的注入至关重要。返回的信息如下:
- 操作系统系列:Windows 9x (95, 98, Me) 或 Windows NT (3, 4, 2000, XP, Vista, 7)
- 进程无效:DOS、16 位、系统、其他
- 进程正在被调试
- 进程尚未完成初始化
- 受保护进程
操作系统系列
此信息是必需的,因为 Windows 9x (95, 98, Me) 和 NT (3, 4, 2000, XP, Vista, 7) 系列的注入算法是不同的。这些信息直接通过调用 GetVersionEx()
返回。
OSVERSIONINFO osvi;
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&osvi);
fWin9x = (osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS);
fWinNT = (osvi.dwPlatformId == VER_PLATFORM_WIN32_NT);
无效进程
NT
如果 NT 进程的退出代码不等于 259(十六进制 0x103)或者它没有 PEB
(进程环境块)(即系统进程),则该 NT 进程被视为无效。
PROCESS_BASIC_INFORMATION pbi;
NtQueryInformationProcess(hProcess,
ProcessBasicInformation,
&pbi,
sizeof(pbi),
NULL);
fINVALID = ((pbi.ExitStatus != 0x103) ||
(pbi.PebBaseAddress == NULL));
9x
如果 Win9x 进程的退出代码不为 259(十六进制 0x103),除非它是 DOS 16 位进程或处于终止状态,否则该进程被视为无效。
#define fINVALIDPROCFLAGS (fTerminated | fTerminating |
fNearlyTerminating | fDosProcess | fWin16Process)
PDB *pPDB = GetPDB(dwPID);
fINVALID = ((pPDB->TerminationStatus != 0x103) ||
(pPDB->Flags & fINVALIDPROCFLAGS));
进程正在被调试
NT
如果 ProcessDebugPort
或 PEB
BeingDebugged
字段非零,则 NT 进程正在被调试。
PROCESS_BASIC_INFORMATION pbi;
BOOL DebugPort;
PEB_NT PEB, *pPEB;
NtQueryInformationProcess(hProcess,
ProcessDebugPort,
&DebugPort,
sizeof(DebugPort),
NULL);
NtQueryInformationProcess(hProcess,
ProcessBasicInformation,
&pbi,
sizeof(pbi),
NULL);
pPEB = pbi.PebBaseAddress;
ReadProcessMemory(hProcess, pPEB, &PEB, sizeof(PEB), NULL);
fDEBUGGED = DebugPort || PEB.BeingDebugged;
9x
如果 PDB
(进程数据库)调试上下文指针非 NULL
或 PDB
标志的 fDebugSingle
位被设置,则 Win9x 进程正在被调试。
PDB *pPDB = GetPDB(dwPID);
fDEBUGGED = ((pPDB->DebuggeeCB != NULL) ||
(pPDB->Flags & fDebugSingle));
进程未初始化
NT
如果 PEB
的 LdrData
或 LoaderLock
字段为 NULL
,则 NT 进程未初始化。这两个字段在进程初始化期间由 NT 加载程序用户模式 APC 例程 LdrpInitialize()
设置。
fNOTINITIALIZED = (PEB.LdrData == NULL || PEB.LoaderLock == NULL);
9x
只有当主线程堆栈的最后一个 DWORD
低于 2GB (0x80000000) 时,Win9x 进程才算初始化([3])。
PDB *pPDB = GetPDB(dwPID);
DWORD *pThreadHead = pPDB->ThreadList;
THREADLIST *pThreadNode = *pThreadHead;
TDB *pTDB = pThreadNode->pTDB;
void *pvStackUserTop = pTDB->tib.pvStackUserTop;
pvStackUserTop = (DWORD *)((DWORD)pvStackUserTop - sizeof(DWORD));
DWORD StackUserTopContents;
ReadProcessMemory(hProcess, pvStackUserTop, &StackUserTopContents,
sizeof(StackUserTopContents), NULL);
fNOTINITIALIZED = ((int)StackUserTopContents < 0);
受保护进程
从 Windows Vista 开始,引入了一种称为受保护进程的新类型进程。在受保护进程中,无法执行以下操作:注入线程、访问虚拟内存、调试进程、复制句柄或更改配额或工作集。因此,在受保护进程中无法进行远程注入。使用以下代码检测受保护进程:
HANDLE hProcess;
PROCESS_EXTENDED_BASIC_INFORMATION ExtendedBasicInformation;
// Get process handle (note the PROCESS_QUERY_LIMITED_INFORMATION access !)
hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwPID);
// Get process Extended Basic Info
ExtendedBasicInformation.Size = sizeof(PROCESS_EXTENDED_BASIC_INFORMATION);
NtQueryInformationProcess(hProcess,
ProcessBasicInformation,
&ExtendedBasicInformation,
sizeof(ExtendedBasicInformation),
NULL);
fPROTECTED = ExtendedBasicInformation.IsProtectedProcess;
Subsystem
这是进程用于其用户界面的子系统类型。它与磁盘文件中的 PE 头(以及内存中的模块头)中的 Subsystem
字段相同。
NT
在 NT 中,子系统类型可以直接从 PEB
的 ImageSubsystem
字段检索。
Subsystem = PEB.ImageSubsystem;
9x
子系统类型可以从模块头部的 Subsystem
字段检索。要定位内存中的模块头部,我们可以使用 Kernel32 的 GetModuleHandle()
函数或 MTEModTable
。指向 NT 头部的指针从 IMTE
(内部模块表条目)的 pNTHdr
字段获取。IMTE
地址通过使用 PDB
的 MTEIndex
字段作为索引从 MTEModTable
获取([4] 第 3 章详细介绍了所有这些结构,并解释了从 Kernel32 的 GDIReallyCares()
函数获取 MTEModTable
地址所需的技巧)。
#define GDIREALLYCARES_ORDINAL 23 // 0x17
HMODULE hKernel32 = GetModuleHandle("Kernel32.dll");
void *pGDIReallyCares = _GetProcAddress(hKernel32, GDIREALLYCARES_ORDINAL);
int GDIReallyCaresLength = GetProcLength(hKernel32, GDIREALLYCARES_ORDINAL);
// Search for MOV ECX,[addr] (8B,0D,...) inside GDIReallyCares() function
BYTE *p = MemSearch(pGDIReallyCares, GDIReallyCaresLength, "\x8B\x0D", 2);
IMTE **pMTEModTable = (IMTE **)*(DWORD *)*(DWORD *)(p+2);
PDB *pPDB = GetPDB(dwPID);
IMTE *pIMTE = pMTEModTable[pPDB->MTEIndex];
PIMAGE_NT_HEADERS32 pNTHeader = pIMTE->pNTHdr;
Subsystem = pNTHeader->OptionalHeader.Subsystem;
RemoteExecute()
RemoteExecute()
函数在远程进程的上下文中执行代码。它接受 7 个参数:
hProcess
:远程进程的句柄。ProcessFlags
:由GetProcessInfo()
返回。可以为零。Function
:将在远程进程上下文中执行的线程函数。线程函数受 SEH 保护,免受异常影响。pData
:将复制到远程进程地址空间的内存块。可以为NULL
。Size
:pData
块的大小。如果指定为零,则pData
被视为DWORD
。dwTimeout
:用于等待函数的超时(毫秒)。可以为INFINITE
。ExitCode
:指向一个DWORD
的指针,该指针将接收远程代码的退出状态。
RemoteExecute()
执行以下步骤(请参阅 [1]):
- 如果
ProcessFlags
为零,则调用GetProcessInfo()
。 - 检查函数代码是否可以安全重定位(没有调用或绝对寻址),并计算其长度。请注意,这并非 100% 安全!您应该编写可重定位的代码并分析生成的代码。
- 分配远程内存块并将函数代码复制到其中。
- 如果指定了数据块,则分配远程内存块并将数据复制到其中。
- 分配远程内存块并将存根代码复制到其中(请参阅文件“Stub.asm”)。存根代码将设置 SEH 帧并调用用户线程函数。特殊的原生进程退出也由此代码处理。
- 根据
ProcessFlags
,它将使用以下可用方法之一运行远程代码:CreateRemoteThread()
、RtlCreateUserThread()
或NtQueueApcThread()
。 - 使用
WaitForSingleObject(hThread)
等待远程代码完成,或检查由存根代码设置的Finished
标志。 - 如果指定了数据块,则从远程内存块读回数据。
- 清理并返回错误代码。
根据 ProcessFlags
,必须使用不同的远程代码执行方法:
Win32 已初始化进程
使用 CreateRemoteThread()
函数执行远程代码(因为此函数在 Win9x 中不存在,所以必须模拟它(请参阅 [2]))。从 Windows Vista 开始,如果目标进程与调用进程不在同一个会话中,CreateRemoteThread()
将会失败。此限制的解决方案是在 Windows Vista 和 7 上使用未公开的 NtCreateThreadEx()
函数([8])。通过调用返回的线程句柄上的 WaitForSingleObject()
等待远程代码完成,并通过调用 GetExitCodeThread()
获取远程退出代码。
Win32 未初始化进程
在未初始化进程中,您可以做的事情非常有限(因为您不能假定系统内部结构已初始化、DLL 已加载等),因此您在向此类进程注入代码时应极其谨慎。建议等待进程完成初始化。对于 GUI 进程,可以通过使用 WaitForInputIdle()
函数来完成,但不幸的是,对于其他类型的进程没有等效函数。另一种可能的技巧是将断点设置在进程入口点(这允许检测进程初始化系统部分何时终止)。
9x
只需在 CreateRemoteThread()
的 dwCreationFlags
参数中设置一个位,该位会导致此函数内部防止在 PROCESS_ATTACH
之前发送 THREAD_ATTACH
消息(请参阅 [3])。
NT
NtQueueApcThread()
函数用于将 APC 例程(我们的远程代码)排队到一个现有的远程线程。APC 例程将在线程变为已信号状态后立即运行。我们不能等待 APC 已排队的线程,因此要获取远程代码的退出状态,我们会轮询由远程存根代码设置的 Finished
标志。我们也无法使用 GetExitCodeThread()
获取远程退出代码(这将返回“被劫持”线程的退出状态),因此我们始终将退出代码设置为零(当然,我们可以将退出状态保存在一个变量中,稍后像处理 Finished
标志一样读取它)。
NT 原生进程
要创建 NT 原生进程,可以使用 RtlCreateUserThread()
函数。可以在返回的线程句柄上使用 WaitForSingleObject()
和 GetExitCodeThread()
。请注意,原生远程代码需要不同的退出代码。这由远程存根代码处理。用于原生退出的代码是 Kernel32 的 ExitThread()
等效项,但针对原生进程:
- 调用
LdrShutdownThread()
以通知线程退出时的所有 DLL。 - 通过调用
NtFreeVirtualMemory()
释放线程堆栈。请注意,在释放堆栈之前,我们必须切换到临时堆栈。TEB
中的UserReserved
区域用于此目的。 - 通过调用
NtTerminateThread()
终止线程。
InjectDll()
InjectDll()
函数将 DLL 加载到远程进程的地址空间。它接受 5 个参数:
hProcess
:远程进程的句柄。ProcessFlags
:由GetProcessInfo()
返回。可以为零。szDllPath
:要加载的 DLL 的路径。可以将 ANSI/Unicode 字符串传递给InjectDllA()
/InjectDllW()
。dwTimeout
:用于等待函数的超时(毫秒)。可以为INFINITE
。hRemoteDll
:指向一个HINSTANCE
变量的指针,该变量将接收加载的 DLL 句柄。
InjectDll()
仅初始化远程代码所需的数据块,并使用 RemoteExecute()
远程执行 RemoteInjectDll()
函数。
DWORD WINAPI RemoteInjectDll(RDATADLL *pData)
{
return (pData->hRemoteDll = pData->LoadLibrary(pData->szDll));
}
RemoteInjectDll()
将在远程进程的地址空间中运行,并调用 LoadLibrary()
将指定的 DLL 加载到远程进程的地址空间中。返回加载 DLL 的句柄。
EjectDll()
EjectDll()
函数从远程进程的地址空间卸载 DLL。它接受 5 个参数:
hProcess
:远程进程的句柄。ProcessFlags
:由GetProcessInfo()
返回。可以为零。szDllPath
:要卸载的 DLL 的路径。可以将 ANSI/Unicode 字符串传递给EjectDllA()
/EjectDllW()
。可以为NULL
。hRemoteDll
:如果szDllPath
为NULL
,则hRemoteDll
参数用作 DLL 句柄。dwTimeout
:用于等待函数的超时(毫秒)。可以为INFINITE
。
EjectDll()
初始化远程函数所需的数据块,并使用 RemoteExecute()
远程执行 RemoteEjectDll()
函数。
DWORD WINAPI RemoteEjectDll(RDATADLL *pData)
{
if (pData->szDll[0] != '\0')
pData->hRemoteDll = pData->GetModuleHandle(pData->szDll);
do {
pData->Result = pData->FreeLibrary(pData->hRemoteDll);
} while (pData->Result);
return 0;
}
RemoteEjectDll()
将在远程进程的地址空间中运行,并调用 FreeLibrary()
卸载指定的 DLL。调用 FreeLibrary()
的次数将引用计数减少到零。如果指定了 DLL 名称,则使用 GetModuleHandle()
来检索 FreeLibrary()
所需的 DLL 句柄。
StartRemoteSubclass()
StartRemoteSubclass()
函数对远程窗口进行子类化(即更改远程进程的窗口过程)。它接受 2 个参数:
rd
:指向RDATA
结构的指针,定义如下:
typedef struct _RDATA {
int Size; // Size of structure
HANDLE hProcess; // Process handle
DWORD ProcessFlags; // Process flags
DWORD dwTimeout; // Timeout
HWND hWnd; // Window handle
struct _RDATA *pRDATA; // Pointer to RDATA structure
WNDPROC pfnStubWndProc; // Address of stub window handler
USERWNDPROC pfnUserWndProc; // Address of user's window
// procedure handler
WNDPROC pfnOldWndProc; // Address of old window handler
LRESULT Result; // Result from user's
// window procedure handler
SETWINDOWLONG pfnSetWindowLong; // Address of SetWindowLong()
CALLWINDOWPROC pfnCallWindowProc; // Address of CallWindowProc()
} RDATA;
如果您需要将额外数据传递给新的窗口过程处理程序,则必须将其附加到现有的 RDATA
。在调用 StartRemoteSubclass()
之前,必须初始化 RDATA
结构的以下字段:Size
必须包含 RDATA
结构的大小以及任何附加数据,hProcess
必须包含远程进程的句柄,hWnd
必须包含要进行子类化的窗口的句柄。附加数据的额外字段也应在此点初始化。所有剩余字段均视为私有,不使用。
WndProc
:将处理子类化窗口消息的用户窗口过程。定义如下:typedef LRESULT (WINAPI* USERWNDPROC)(RDATA *, HWND, UINT, WPARAM, LPARAM);
除了第一个参数(指向 RDATA
结构的指针)之外,其余参数是在任何窗口过程处理程序中找到的正常窗口句柄、消息类型以及 wParam
和 lParam
。每当必须处理窗口的消息时,Windows 都会调用新的窗口过程处理程序,因此该函数应编码为“正常”窗口过程处理程序(带有 switch(Msg)
循环)。请注意,由于此函数将在远程进程上执行,因此它必须遵循与任何远程代码执行相同的规则。任何未处理的消息都应由默认窗口过程处理程序处理。为此,函数必须返回 FALSE
。如果您想自己处理某些消息,请在 RDATA
结构的 Result
字段中返回值,并为函数返回 TRUE
。此函数受远程 SEH 帧保护,免受异常影响。
StartRemoteSubclass()
初始化剩余的 RDATA
字段,并使用 RemoteExecute()
远程执行 RemoteStartSubclass()
函数。
DWORD WINAPI RemoteStartSubclass(RDATA *pData)
{
return (pData->pfnOldWndProc =
pData->pfnSetWindowLong(pData->hWnd,
GWL_WNDPROC,
pData->pfnStubWndProc));
}
RemoteStartSubclass()
将在远程进程的地址空间中运行,并使用 GWL_WNDPROC
参数调用 SetWindowLong()
将窗口过程处理程序更改为新的窗口处理程序。每当必须处理窗口的消息时,Windows 都会调用此处理程序。新的窗口过程处理程序(“Stub.asm”文件中的 StubWndProc()
)设置 SEH 帧并调用 UserWndProc()
。如果 UserWndProc()
返回 FALSE
,则调用 CallWindowProc()
允许原始窗口过程处理消息。
StopRemoteSubclass()
StopRemoteSubclass()
函数恢复远程进程的原始窗口处理程序。它接受一个参数:
rd
:这是传递给StartRemoteSubclass()
的相同的RDATA
结构,并且包含由该函数初始化的所需数据。
StopRemoteSubclass()
释放分配的内存,并使用 RemoteExecute()
远程执行 RemoteStopSubclass()
函数。
DWORD WINAPI RemoteStopSubclass(RDATA *pData)
{
return (pData->pfnSetWindowLong(pData->hWnd,
GWL_WNDPROC, pData->pfnOldWndProc));
}
RemoteStopSubclass()
将在远程进程的地址空间中运行,并使用 GWL_WNDPROC
参数调用 SetWindowLong()
来恢复原始窗口过程处理程序。
演示
最后,为了演示如何使用 Injection Library 导出的函数,我编写了一个应用程序,允许您将所有注入方法应用于任何正在运行的进程(如果适用!)。该应用程序只是用所有正在运行的进程填充一个列表视图控件,并根据用户的选择,注入代码、DLL 或对进程窗口进行子类化。根据我的测试,只有以下进程无法注入:
- Windows 9x:16 位进程(它们被视为无效进程)。
- Windows NT:空闲进程(PID = 0)、系统进程(PID = 4)和受保护的进程。
历史
- 2005 年 9 月 27 日:版本 1.0 - Windows 95 到 Windows XP。
- 2011 年 11 月 1 日:版本 2.0 - 更新至 Vista、Windows 7。
参考文献
- Robert Kuster 的“将代码注入另一个进程的三种方法”。
- António Feijão 的“远程库”。
- Radim Picha 的“PrcHelp”。
- Matt Pietrek 的《Windows 95 系统编程秘诀》。
- Gary Nebbett 的《Windows NT/2000 原生 API 参考》。
- Matt Pietrek 的“Win32 结构化异常处理深度速成课”。
- Alex Fedotov 的“枚举 Windows 进程”。
- SecurityXploded 的“使用 NtCreateThreadEx 在 Vista & Windows 7 的系统进程中远程线程执行”。
- wj32 的“Process Hacker”。