利用 ZwCreateThreadEx 注入 DLL
1 ZwCreateThreadEx 和 CreateRemoteThread
1.1 CreateRemoteThread函数
创建在另一个进程的虚拟地址空间中运行的线程,并可选择指定扩展属性。
由于该函数没有在ntdll.dll库中申明所以需要通过GetProcAddress获取函数的导出地址
函数原型
HANDLE CreateRemoteThread(
HANDLE hProcess, // handle to process
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
SIZE_T dwStackSize, // initial stack size
LPTHREAD_START_ROUTINE lpStartAddress, // thread function
LPVOID lpParameter, // thread argument
DWORD dwCreationFlags, // creation option
LPDWORD lpThreadId // thread identifier
);
参数说明:
hProcess
[输入] 进程句柄
lpThreadAttributes
[输入] 线程安全描述字,指向SECURITY_ATTRIBUTES结构的指针
dwStackSize
[输入] 线程栈大小,以字节表示
lpStartAddress
[输入] 一个LPTHREAD_START_ROUTINE类型的指针,指向在远程进程中执行的函数地址
lpParameter
[输入] 传入参数
dwCreationFlags
[输入] 创建线程的其它标志
lpThreadId
[输出] 线程身份标志,如果为NULL,则不返回
返回值
成功返回新线程句柄,失败返回NULL,并且可调用GetLastError获得错误值。
1.2 ZwCreateThreadEx
ZwCreateThreadEx 是利用 ntdll 导出的未文档化函数
1.3 ZwCreateThreadEx与CreateRemoteThread的区别:
- CreateRemoteThread是由ZwCreateThreadEx调用而来的,而在底层的调用中发现CreateRemoteThread 中的CreateSuspened 默认为1,导致线程创建完成后一直无法挂起恢复运行
- 传统的 CreateRemoteThreadEx 注入方式极容易被查杀,不便于测试环境。
1.4 ZwCreateThreadEx 函数在 32 和 64 上的定义不相同,参见:
- x86-64:
typedef DWORD(WINAPI* typedef _ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown
);
- x86-32:
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown
);
其中,lpStartAddress 支持提供内存地址用于执行 Payload 函数,lpParameter 传入结构体指针,用于在回调中使用。
2 遍历目标进程的 PID
我们常常使用进程名作为特征,遍历进程的 PID 以及句柄,然后执行注入过程,
这里我们使用 CreateToolhelp32Snapshot 获取进程快照,再通过Process32First和Process32Next遍历目标进程的 PID
2.1 Process32First 得到父进程ID
这里,我们重点讲一下Process32First,Process32First返回进程快照中第一个进程的信息,每个进程都存着一个PROCESSENTRY32信息,里面的数据结构如下:
typedef struct tagPROCESSENTRY32W
{
DWORD dwSize; // 结构大小
DWORD cntUsage; // 此进程的引用计数 必须为 0
DWORD th32ProcessID; // this process
ULONG_PTR th32DefaultHeapID; // 进程默认堆ID 必须为0
DWORD th32ModuleID; // associated exe 进程模块ID 必须为0
DWORD cntThreads; // 进程线程数
DWORD th32ParentProcessID; // 进程父ID
LONG pcPriClassBase; // 此进程创建的任何线程的基本优先级。
DWORD dwFlags; // 必须为0
WCHAR szExeFile[MAX_PATH]; // Path 进程可执行文件名称
} PROCESSENTRY32W;
typedef PROCESSENTRY32W * PPROCESSENTRY32W;
typedef PROCESSENTRY32W * LPPROCESSENTRY32W;
2.2 通过Module32First()获取第一个模块信息
这里,我们重点讲一下Module32First,Module32First返回进程快照中第一个模块的信息,每个模块都存着一个MODULEENTRY32信息,里面的数据结构如下:
typedef struct tagMODULEENTRY32W
{
DWORD dwSize; // 结构大小
DWORD th32ModuleID; // This module
DWORD th32ProcessID; // owning process
DWORD GlblcntUsage; // Global usage count on the module
DWORD ProccntUsage; // Module usage count in th32ProcessID's context
BYTE * modBaseAddr; // Base address of module in th32ProcessID's context
DWORD modBaseSize; // Size in bytes of module starting at modBaseAddr
HMODULE hModule; // The hModule of this module in th32ProcessID's context
WCHAR szModule[MAX_MODULE_NAME32 + 1];
WCHAR szExePath[MAX_PATH];
} MODULEENTRY32W;
typedef MODULEENTRY32W * PMODULEENTRY32W;
typedef MODULEENTRY32W * LPMODULEENTRY32W;
2.3 处理崩溃的进程
需要注意的是我们可能遇到内存中已经崩溃的进程,这种时候会导致注入器不断尝试注入过程,但始终失败。PROCESSENTRY32W 结构体中的 cntThreads 包含进程的线程计数信息,崩溃的进程没有主线程,我们可以加上这个条件来过滤。
2.4 获取父进程ID信息代码如下:
std::vector<DWORD> pids; // 容器对象用于存储 PID
// 创建进程快照
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE)
{
printf("Create snapshot error:%d\n", GetLastError());
}
else
{
PROCESSENTRY32W pe = { 0 };
pe.dwSize = sizeof(PROCESSENTRY32W);
// 使用 Process32FirstW 和 Process32NextW 的组合来遍历快照信息
if (Process32FirstW(hSnap, &pe)) {
do {
// wcscmp 比较进程名称是否相同,
// pe.cntThreads >= 1 可以过滤已经崩溃的进程
if (!wcscmp(pe.szExeFile, exepth) && pe.cntThreads >= 1) {
pids.push_back(pe.th32ProcessID); // 压入 PID 信息
}
} while (Process32NextW(hSnap, &pe));
}
// 关闭句柄
CloseHandle(hSnap);
}
2.5 提升至管理员权限
现在我们有了 PID 有了 注入执行函数,但是进程可能需要提升至管理员权限并开启 DEBUG 权限才能注入DLL,我们使用 AdjustTokenPrivileges 函数实现获取进程 DEBUG 权限:
BOOL EnableDebugPrivilege(BOOL bEnablePrivilege) // to enable or disable privilege
{
HANDLE hProcess = NULL;
TOKEN_PRIVILEGES tp{};
LUID luid;
hProcess = GetCurrentProcess();
HANDLE hToken = NULL;
OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &hToken);
if (!LookupPrivilegeValueW(
NULL, // lookup privilege on local system
SE_DEBUG_NAME, // privilege to lookup
&luid)) // receives LUID of privilege
{
printf("LookupPrivilegeValue error: %u\n", GetLastError());
CloseHandle(hToken);
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if (bEnablePrivilege)
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
tp.Privileges[0].Attributes = 0;
// Enable the privilege or disable all privileges.
if (!AdjustTokenPrivileges(
hToken,
FALSE,
&tp,
sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES)NULL,
(PDWORD)NULL))
{
printf("AdjustTokenPrivileges error: %u\n", GetLastError());
CloseHandle(hToken);
return FALSE;
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
{
printf("The token does not have the specified privilege. \n");
CloseHandle(hToken);
return FALSE;
}
CloseHandle(hToken);
return TRUE;
}
3 完整注入器代码:
此注入器支持参数:
DLLInjectTool.exe <要注入的目标进程名(含后缀)> <要注入完整 DLL 路径>
#include <iostream>
#include <windows.h>
#include <vector>
#include <tlhelp32.h>
#include <shlwapi.h>
#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "advapi32.lib")
BOOL ProcessHasLoadDll(
DWORD pid,
const TCHAR* dll
);
BOOL ZwCreateThreadExInjectDll(
DWORD dwProcessId,
const wchar_t* pszDllFileName
);
BOOL EnableDebugPrivilege(
BOOL bEnablePrivilege
);
int wmain(int argc, wchar_t* argv[])
{
SetConsoleTitleW(L"DLLInjectTool v.1.0");
printf("DLLInjectTool v.1.0 通用 DLL 注入工具;@Rio-CTH\n");
if (argc != 3)
{
std::cout << "错误:参数不合法!" << std::endl;
std::cin.get();
return -1;
}
const size_t exepthlen =
wcslen(argv[1]) * sizeof(wchar_t);
const size_t dllpthlen =
wcslen(argv[2]) * sizeof(wchar_t);
if (exepthlen < 5 || dllpthlen < 1)
{
std::cout << "错误:文件路径错误!" << std::endl;
std::cin.get();
return -1;
}
wchar_t* exepth = new wchar_t[exepthlen];
wchar_t* dllpth = new wchar_t[dllpthlen];
//wchar_t pszPath[MAX_PATH] = { 0 };
wcscpy_s(exepth, exepthlen, argv[1]);
wcscpy_s(dllpth, dllpthlen, argv[2]);
//wcscpy_s(pszPath, dllpthlen, argv[2]);
//BOOL exeextflag = PathFileExistsW(exepth);
if (!wcsstr(exepth, L".exe"))
{
std::cout << "错误:目标进程名必须包含.exe后缀!" << std::endl;
std::cin.get();
return -1;
}
BOOL dllextflag = PathFileExistsW(dllpth);
if (FALSE == dllextflag)
{
std::cout << "错误:文件不存在或者无法访问!" << std::endl;
std::cin.get();
return -1;
}
if (PathGetDriveNumberW(dllpth) == -1)
{
TCHAR szPath[_MAX_PATH] = { 0 };
TCHAR szDrive[_MAX_DRIVE] = { 0 };
TCHAR szDir[_MAX_DIR] = { 0 };
TCHAR szFname[_MAX_FNAME] = { 0 };
TCHAR szExt[_MAX_EXT] = { 0 };
//TCHAR DataBinPath[MAX_PATH] = { 0 };
TCHAR CurBinPath[MAX_PATH] = { 0 };
GetModuleFileNameW(NULL, szPath, sizeof(szPath) / sizeof(TCHAR));
ZeroMemory(CurBinPath, sizeof(CurBinPath));
_wsplitpath_s(szPath, szDrive, szDir, szFname, szExt);
wsprintf(CurBinPath, L"%s%s", szDrive, szDir);
//wcscpy_s(DataBinPath, CurBinPath);
wcscat_s(CurBinPath, dllpth);
//wprintf(L"[WorkstationDirectory]: %s\n", DataBinPath);
dllextflag = PathFileExistsW(CurBinPath);
if (FALSE == dllextflag)
{
std::cout << "错误:文件不存在或者无法访问!" << std::endl;
std::cin.get();
return -1;
}
dllpth = CurBinPath;
}
EnableDebugPrivilege(TRUE);
std::vector<DWORD> pids;
HANDLE hp = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32W pe = { 0 };
pe.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(hp, &pe)) {
do {
if (!wcscmp(pe.szExeFile, exepth) && pe.cntThreads >= 1) {
pids.push_back(pe.th32ProcessID);
}
} while (Process32NextW(hp, &pe));
}
CloseHandle(hp);
for (int i = 0; i < pids.size(); i++) {
if (ProcessHasLoadDll(pids[i], dllpth) != NULL)
{
std::cout << "警告:PID 为 " << pids[i] << " 的进程已经包含目标 DLL。" << std::endl;
continue;
}
if (ZwCreateThreadExInjectDll(pids[i], dllpth) == FALSE)
{
std::cout << "错误:注入 PID 为 " << pids[i] << " 的进程时失败。" << std::endl;
std::cout << "原因:" << GetLastError() << std::endl;
}
}
if (pids.size() == 0)
{
std::wcout << "错误:未找到目标进程:" << exepth << std::endl;
std::cin.get();
EnableDebugPrivilege(FALSE);
return -1;
}
// 销毁过程
pids.clear();
pids.shrink_to_fit();
std::cout << "操作已经完成。" << std::endl;
std::cin.get();
EnableDebugPrivilege(FALSE);
return 0;
}
BOOL EnableDebugPrivilege(
BOOL bEnablePrivilege // to enable or disable privilege
)
{
HANDLE hProcess = NULL;
TOKEN_PRIVILEGES tp{};
LUID luid;
hProcess = GetCurrentProcess();
HANDLE hToken = NULL;
OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &hToken);
if (!LookupPrivilegeValueW(
NULL, // lookup privilege on local system
SE_DEBUG_NAME, // privilege to lookup
&luid)) // receives LUID of privilege
{
printf("LookupPrivilegeValue error: %u\n", GetLastError());
CloseHandle(hToken);
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if (bEnablePrivilege)
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
tp.Privileges[0].Attributes = 0;
// Enable the privilege or disable all privileges.
if (!AdjustTokenPrivileges(
hToken,
FALSE,
&tp,
sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES)NULL,
(PDWORD)NULL))
{
printf("AdjustTokenPrivileges error: %u\n", GetLastError());
CloseHandle(hToken);
return FALSE;
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
{
printf("The token does not have the specified privilege. \n");
CloseHandle(hToken);
return FALSE;
}
CloseHandle(hToken);
return TRUE;
}
BOOL ProcessHasLoadDll(DWORD pid, const TCHAR* dll) {
/*
* 参数为TH32CS_SNAPMODULE 或 TH32CS_SNAPMODULE32时,如果函数失败并返回ERROR_BAD_LENGTH,则重试该函数直至成功
* 进程创建未初始化完成时,CreateToolhelp32Snapshot会返回error 299,但其它情况下不会
*/
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
while (INVALID_HANDLE_VALUE == hSnapshot) {
DWORD dwError = GetLastError();
if (dwError == ERROR_BAD_LENGTH) {
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
continue;
}
else {
//if (dwError == ERROR_PARTIAL_COPY) {
//printf("CreateToolhelp32Snapshot failed: %d\ncurrentProcessId: %d \t targetProcessId:\n",
//dwError, GetCurrentProcessId(), pid);
//}
//else {
printf("CreateToolhelp32Snapshot failed: %d\ncurrentProcessId: %d \t targetProcessId:%d\n",
dwError, GetCurrentProcessId(), pid);
//}
return FALSE;
}
}
MODULEENTRY32W mi{};
mi.dwSize = sizeof(MODULEENTRY32W); //第一次使用必须初始化成员
BOOL bRet = Module32FirstW(hSnapshot, &mi);
while (bRet) {
// mi.szModule是短路径
if (wcsstr(dll, mi.szModule) || wcsstr(mi.szModule, dll)) {
//TCHAR* dllWithFullPath = new TCHAR[MAX_MODULE_NAME32 + 1];
//lstrcpyW(dllWithFullPath, mi.szModule);
if (hSnapshot != NULL) CloseHandle(hSnapshot);
return TRUE;
}
mi.dwSize = sizeof(MODULEENTRY32W);
bRet = Module32NextW(hSnapshot, &mi);
}
if(hSnapshot != NULL) CloseHandle(hSnapshot);
return FALSE;
}
BOOL ZwCreateThreadExInjectDll(DWORD dwProcessId, const wchar_t* pszDllFileName)
{
size_t pathSize = (wcslen(pszDllFileName) + 1) * sizeof(wchar_t);
// 1.打开目标进程
HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // 打开权限
FALSE, // 是否继承
dwProcessId); // 进程PID
if (NULL == hProcess)
{
printf("错误:打开目标进程失败!\n");
return FALSE;
}
// 2.在目标进程中申请空间
LPVOID lpPathAddr = VirtualAllocEx(
hProcess, // 目标进程句柄
0, // 指定申请地址
pathSize, // 申请空间大小
MEM_RESERVE | MEM_COMMIT, // 内存的状态
PAGE_READWRITE); // 内存属性
if (NULL == lpPathAddr)
{
printf("错误:在目标进程中申请空间失败!\n");
CloseHandle(hProcess);
return FALSE;
}
// 3.在目标进程中写入Dll路径
if (FALSE == WriteProcessMemory(
hProcess, // 目标进程句柄
lpPathAddr, // 目标进程地址
pszDllFileName, // 写入的缓冲区
pathSize, // 缓冲区大小
NULL)) // 实际写入大小
{
printf("错误:目标进程中写入Dll路径失败!\n");
CloseHandle(hProcess);
return FALSE;
}
//4.加载ntdll.dll
HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
if (NULL == hNtdll)
{
printf("错误:加载ntdll.dll失败!\n");
CloseHandle(hProcess);
return FALSE;
}
// 5.获取LoadLibraryA的函数地址
// FARPROC可以自适应32位与64位
FARPROC pFuncProcAddr = GetProcAddress(GetModuleHandle(L"Kernel32.dll"),
"LoadLibraryW");
if (NULL == pFuncProcAddr)
{
printf("错误:获取LoadLibrary函数地址失败!\n");
CloseHandle(hProcess);
return FALSE;
}
// 6.获取ZwCreateThreadEx函数地址,该函数在32位与64位下原型不同
// _WIN64用来判断编译环境 ,_WIN32用来判断是否是Windows系统
#ifdef _WIN64
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown
);
#else
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown
);
#endif
typedef_ZwCreateThreadEx ZwCreateThreadEx =
(typedef_ZwCreateThreadEx)GetProcAddress(hNtdll, "ZwCreateThreadEx");
if (NULL == ZwCreateThreadEx)
{
printf("错误:获取ZwCreateThreadEx函数地址失败!\n");
CloseHandle(hProcess);
return FALSE;
}
//7.在目标进程中创建远线程
HANDLE hRemoteThread = NULL;
DWORD lpExitCode = 0;
DWORD dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL,
hProcess,
(LPTHREAD_START_ROUTINE)pFuncProcAddr, lpPathAddr, 0, 0, 0, 0, NULL);
if (NULL == hRemoteThread)
{
printf("错误:目标进程中创建线程失败!\n");
CloseHandle(hProcess);
return FALSE;
}
// 8.等待线程结束
WaitForSingleObject(hRemoteThread, -1);
GetExitCodeThread(hRemoteThread, &lpExitCode);
if (lpExitCode == 0)
{
printf("错误:目标进程中注入 DLL 失败,请检查提供的 DLL 是否有效!\n");
CloseHandle(hProcess);
return FALSE;
}
// 9.清理环境
VirtualFreeEx(hProcess, lpPathAddr, 0, MEM_RELEASE);
CloseHandle(hRemoteThread);
CloseHandle(hProcess);
FreeLibrary(hNtdll);
return TRUE;
}