WIN32 动态 UAC 提权

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

静态 UAC 提权

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

静态 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}\ElevationEnabled 值为 1 就可以用。对于这种方法,The COM Elevation Moniker 介绍得比较详细,所以直接贴出完整代码:

cpp 复制代码
#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 提权的程序都会在需要管理员权限的操作按钮上显示一个小盾牌图标,表示点击它会请求管理员权限,比如:

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

cpp 复制代码
SHSTOCKICONINFO sii{};
sii.cbSize = sizeof(sii);
HRESULT hr = SHGetStockIconInfo(SIID_SHIELD, SHGSI_ICON | SHGSI_SMALLICON, &sii);
m_buttonRepairFirewall.SetIcon(sii.hIcon);
DestroyIcon(sii.hIcon);
相关推荐
老四啊laosi5 小时前
[C++进阶] 24. 哈希表封装unordered_map && unordered_set
c++·哈希表·封装·unordered_map·unordered_set
妙为5 小时前
银河麒麟V4下编译Qt5.12.12源码
c++·qt·国产化·osg3.6.5·osgearth3.2·银河麒麟v4
史迪仔01128 小时前
[QML] QML IMage图像处理
开发语言·前端·javascript·c++·qt
会编程的土豆10 小时前
【数据结构与算法】再次全面了解LCS底层
开发语言·数据结构·c++·算法
低频电磁之道10 小时前
解决 Windows C++ DLL 导出类不可见的编译错误
c++·windows
君义_noip11 小时前
信息学奥赛一本通 4150:【GESP2509七级】⾦币收集 | 洛谷 P14078 [GESP202509 七级] 金币收集
c++·算法·gesp·信息学奥赛·csp-s
Ricky_Theseus11 小时前
静态链接与动态链接
c++
澈20712 小时前
双指针,数组去重
c++·算法
小辉同志12 小时前
207. 课程表
c++·算法·力扣·图论
feng_you_ying_li13 小时前
C++11,{}的初始化情况与左右值及其引用
开发语言·数据结构·c++