UAC(User Account Control) 是 Windows 平台的用户权限控制。它可以让程序使用管理员权限执行某些操作。

静态 UAC 提权

静态 UAC 提权让程序一直运行在管理员权限下,只要在项目设置里把 "UAC Execution Level" 设置为 "requireAdministrator"。这样生成的 exe 文件图标会自动加上一个小盾牌的角标 Overlay。执行 exe 文件会自动弹出 UAC 对话框。

image-20240120115154732

静态 UAC 提权对程序员来说是一种偷懒的办法,只需要修改一个配置就行。但对用户来说非常麻烦,每次打开程序都需要确认 UAC 对话框。比如“小黑盒加速器”,每次打开它都会弹 UAC 对话框。更奇葩的是“小黑盒加速器” 可以设置开机自启,每次开机都会弹一个 UAC 对话框要你确认。

动态 UAC 提权

动态 UAC 提权让程序一直运行在普通用户权限下,并且只有需要管理员权限操作时才会弹出 UAC 对话框。这种做法比静态 UAC 提权更加细致。一个普通的应用程序 99% 的功能都不需要管理员权限,只在极少数情况下才需要。比如“QQ音乐”,它只是一个音乐播放软件。用户大部分的时间都仅使用音乐播放功能。而需要管理员权限的“将QQ音乐设为默认应用”功能很少会被使用。所以动态 UAC 提权很有必要。

按照微软官方文档
Developing Applications that Require Administrator Privilege
,有四种方法可以实现动态 UAC 提权:

可以根据具体需要实现的功能选择合适的方法。比如:“添加防火墙规则”这个功能需要使用 INetFwPolicy2。这是一个 COM 接口,可以直接用第 4 种方法“Administrator COM Object Model”实现。使用这种方法有个前提,就是这个 COM 接口必须在注册表里是配置为可以提权的。比如 INetFwPolicy2 接口,先找到 NetFwPolicy2 的 GUID 为
E2B3C97F-6AE1-41AC-817A-F6F92166D7DD
,再打开 regedit,输入
HKLM\Software\Classes\CLSID\{E2B3C97F-6AE1-41AC-817A-F6F92166D7DD}\Elevation

Enabled
值为
1
就可以用。对于这种方法,
The COM Elevation Moniker
介绍得比较详细,所以直接贴出完整代码:

#include <Windows.h>
#include <netfw.h>
#include <comdef.h>

#include <spdlog/spdlog.h>
#include <memory>
#include <filesystem>
#include <wil/resource.h>
#include <wil/com.h>
#include <iostream>

int add_firewall_rule();

int main()
{
    add_firewall_rule();
}

int add_firewall_rule()
{
    auto couninitialize_call = wil::CoInitializeEx();

    auto pNetFwPolicy2 = wil::CoCreateInstance<INetFwPolicy2>(__uuidof(NetFwPolicy2));

    wil::unique_cotaskmem_string clsid;
    RETURN_IF_FAILED(StringFromCLSID(__uuidof(NetFwPolicy2), &clsid));

    // https://learn.microsoft.com/en-us/windows/win32/secauthz/developing-applications-that-require-administrator-privilege
    // https://learn.microsoft.com/en-us/windows/win32/com/the-com-elevation-moniker
    BIND_OPTS3 bo{};
    bo.cbStruct = sizeof(bo);
    bo.hwnd = GetConsoleWindow();
    bo.dwClassContext = CLSCTX_LOCAL_SERVER;
    auto moniker = fmt::format(L"Elevation:Administrator!new:{}", clsid.get());
    spdlog::info(L"moniker: {}", moniker);
    auto hr = (CoGetObject(moniker.c_str(), &bo, IID_PPV_ARGS(&pNetFwPolicy2)));

    wil::com_ptr<INetFwRules> pNetFwRules;
    RETURN_IF_FAILED(pNetFwPolicy2->get_Rules(&pNetFwRules));

    long count{};
    RETURN_IF_FAILED(pNetFwRules->get_Count(&count));

    spdlog::info("rule count: {}", count);

    wil::com_ptr<IUnknown> pEnumerator;
    pNetFwRules->get__NewEnum(&pEnumerator);

    auto pVariant = pEnumerator.query<IEnumVARIANT>();

    std::vector<wchar_t> buf(1024);
    GetModuleFileNameW(nullptr, buf.data(), (DWORD)buf.size());
    std::filesystem::path exe_path(buf.data());

    // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ics/c-enumerating-firewall-rules
    while (true) {
        wil::unique_variant var;
        ULONG cFecthed = 0;
        if (pVariant->Next(1, &var, &cFecthed) != S_OK) {
            break;
        }

        wil::com_ptr<INetFwRule> pNetFwRule;
        var.pdispVal->QueryInterface(__uuidof(INetFwRule), (void**)&pNetFwRule);

        _bstr_t app_name;
        pNetFwRule->get_ApplicationName(app_name.GetAddress());
        std::error_code ec{};
        if (!app_name || !std::filesystem::equivalent(exe_path, (wchar_t*)app_name, ec)) {
            continue;
        }

        pNetFwRule->put_Name(app_name);
        pNetFwRules->Remove(app_name);
        spdlog::info(L"remove firewall rule: {}", (wchar_t*)app_name);
    }

    auto pNetFwRule = wil::CoCreateInstance<INetFwRule3>(__uuidof(NetFwRule));
    pNetFwRule->put_Enabled(VARIANT_TRUE);
    pNetFwRule->put_Action(NET_FW_ACTION_ALLOW);
    pNetFwRule->put_ApplicationName(_bstr_t(exe_path.c_str()));
    pNetFwRule->put_Profiles(NET_FW_PROFILE2_ALL);

    pNetFwRule->put_Name(_bstr_t(L"firewall-test.exe(TCP-In)"));
    pNetFwRule->put_Protocol(NET_FW_IP_PROTOCOL_TCP);
    RETURN_IF_FAILED(pNetFwRules->Add(pNetFwRule.get()));

    pNetFwRule->put_Name(_bstr_t(L"firewall-test.exe(UDP-In)"));
    pNetFwRule->put_Protocol(NET_FW_IP_PROTOCOL_UDP);
    RETURN_IF_FAILED(pNetFwRules->Add(pNetFwRule.get()));

    spdlog::info("success");
    return S_OK;
}

关于动态 UAC 提权的代码只有第 30-36 行。需要注意的点有:

  • 第 32 行的
    bo.hwnd
    设置 UAC 对话框的 owner。如果代码是控制台程序,应该设置为
    GetConsoleWindow()
    ,如果是 GUI 程序可以直接填
    nullptr
  • 第 33 行的
    bo.dwClassContext
    应填
    CLSCTX_LOCAL_SERVER
    ,而不是
    CLSCTX_INPROC_SERVER
    。否则提权失败,
    CoGetObject
    仍然返回
    S_OK
  • UAC 对话框出现在
    CoGetObject
    调用时,而不是执行需要管理员权限的操作(如
    pNetFwRules->Add
    )时。
  • 每次执行
    add_firewall_rule()
    都会弹出 UAC 对话框。

很多动态 UAC 提权的程序都会在需要管理员权限的操作按钮上显示一个小盾牌图标
sheild
,表示点击它会请求管理员权限,比如:

image-20240120141602183

可以直接调用
SHGetStockIconInfo
方法直接得到它的 HICON 句柄。

SHSTOCKICONINFO sii{};
sii.cbSize = sizeof(sii);
HRESULT hr = SHGetStockIconInfo(SIID_SHIELD, SHGSI_ICON | SHGSI_SMALLICON, &sii);
m_buttonRepairFirewall.SetIcon(sii.hIcon);
DestroyIcon(sii.hIcon);

标签: none

添加新评论