如何在Windows 10上从提升的管理员上下文中启动非提升的管理员进程?

5
有没有一种简单的方法可以从提升的管理员进程中创建一个普通管理员进程? 我正在使用Windows 10专业版。情况是我正在尝试制作某种部署工具。该工具将在提升的管理员上下文中运行,以便写入文件到“Program Files”(和访问其他特权资源)。但其中一步是调用外部程序。当使用提升的管理员权限创建该程序时,该程序似乎存在奇怪的问题。我们必须在非提升的管理员上下文中启动它。 我尝试了MSDN博客中的方法,https://blogs.msdn.microsoft.com/winsdk/2010/05/31/dealing-with-administrator-and-standard-users-context 它根本不起作用。

请分享您尝试过的代码和步骤以及失败的原因。 - Muhammad Sumon Molla Selim
如果我能找到它们的话,我会的。但实际上,我是从我提供的链接中复制了代码。我理解这个例子试图展示有两个关联的令牌,一个是高级管理员,一个是非高级管理员。通过获取高级管理员,你可以找到关联部分。使用关联部分,你可以获取CreateProcessWithToken所需的参数。在Windows 10上,结论是不正确的。关联令牌可以被获取,但那似乎是一种缩小版本或令牌的视图。它不能用于CreateProcessWithToken。 - H. Tao
有一个名为ProcessHacker的开源项目,据我所知,它是sysinternals的ProcessExplorer的克隆版。我已经从ProcessHacker移植了代码。他们的方法是获取提升的管理员令牌并调整其安全描述符。对于像记事本这样的简单程序似乎可以工作,但在我们的情况下无法启动复杂的程序。我倾向于认为他们制造的令牌是一个近似值,但仍然不是与非提升管理员相关联的真实令牌。 - H. Tao
4个回答

8

Raymond Chen在他的MSDN上的“Old New Thing”博客中回答了这个确切的问题:

如何从我的提权进程启动未提权进程,反之亦然?

反过来比较棘手。首先,很难正确修改令牌以删除提权属性。其次,即使你能够实现这一点,也不是正确的做法,因为未提权的用户可能与提权的用户不同。

...

解决方案是返回到资源管理器并请求资源管理器为您启动程序。由于资源管理器正在以原始未提权用户身份运行,因此程序(在本例中为Web浏览器)将以Bob的身份运行。在打开的文件的处理程序作为一个进程内扩展而不是作为一个独立的进程运行的情况下,这也非常重要,因为尝试撤消提权是无意义的,因为首先没有创建新的进程。(如果文件的处理程序尝试与现有的未提权副本通信,则可能会因UIPI而失败。)

Raymond使用IShellFolderViewDualIShellDispatch2来实现这一点1

#define STRICT
#include <windows.h>
#include <shldisp.h>
#include <shlobj.h>
#include <exdisp.h>
#include <atlbase.h>
#include <stdlib.h>

// FindDesktopFolderView incorporated by reference

void GetDesktopAutomationObject(REFIID riid, void **ppv)
{
 CComPtr<IShellView> spsv;
 FindDesktopFolderView(IID_PPV_ARGS(&spsv));
 CComPtr<IDispatch> spdispView;
 spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
 spdispView->QueryInterface(riid, ppv);
}

void ShellExecuteFromExplorer(
    PCWSTR pszFile,
    PCWSTR pszParameters = nullptr,
    PCWSTR pszDirectory  = nullptr,
    PCWSTR pszOperation  = nullptr,
    int nShowCmd         = SW_SHOWNORMAL)
{
 CComPtr<IShellFolderViewDual> spFolderView;
 GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView));
 CComPtr<IDispatch> spdispShell;
 spFolderView->get_Application(&spdispShell);

 CComQIPtr<IShellDispatch2>(spdispShell)
    ->ShellExecute(CComBSTR(pszFile),
                   CComVariant(pszParameters ? pszParameters : L""),
                   CComVariant(pszDirectory ? pszDirectory : L""),
                   CComVariant(pszOperation ? pszOperation : L""),
                   CComVariant(nShowCmd));
}

int __cdecl wmain(int argc, wchar_t **argv)
{
 if (argc < 2) return 0;

 CCoInitialize init;
 ShellExecuteFromExplorer(
    argv[1],
    argc >= 3 ? argv[2] : L"",
    argc >= 4 ? argv[3] : L"",
    argc >= 5 ? argv[4] : L"",
    argc >= 6 ? _wtoi(argv[5]) : SW_SHOWNORMAL);

 return 0;
}

打开提升命令提示符,然后以各种方式运行此程序。 scratch http://www.msn.com/ 在用户的默认Web浏览器中打开未提升的 Web 页面。 scratch cmd.exe "" C:\Users "" 3 在 C:\Users 中打开未提升的命令提示符,并最大化。 scratch C:\Path\To\Image.bmp "" "" edit 在未提升的图像编辑器中编辑位图。 1: FindDesktopFolderView() 的实现在 Raymond 博客的另一篇文章中:操作桌面图标位置
void FindDesktopFolderView(REFIID riid, void **ppv)
{
 CComPtr<IShellWindows> spShellWindows;
 spShellWindows.CoCreateInstance(CLSID_ShellWindows);

 CComVariant vtLoc(CSIDL_DESKTOP);
 CComVariant vtEmpty;
 long lhwnd;
 CComPtr<IDispatch> spdisp;
 spShellWindows->FindWindowSW(
     &vtLoc, &vtEmpty,
     SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);

 CComPtr<IShellBrowser> spBrowser;
 CComQIPtr<IServiceProvider>(spdisp)->
     QueryService(SID_STopLevelBrowser,
                  IID_PPV_ARGS(&spBrowser));

 CComPtr<IShellView> spView;
 spBrowser->QueryActiveShellView(&spView);

 spView->QueryInterface(riid, ppv);
}

谢谢!我尝试了shellexecute方法,但对我来说并不起作用。不过,与您发布的代码相比,我的shellexecute代码非常简单。也许这里的区别很关键。我会再次尝试并报告情况。 - H. Tao
@H.TaoпјҡдҪ иҜ•еӣҫдҪҝз”Ёе®һйҷ…зҡ„ShellExecute()еҮҪж•°пјҢиҖҢRaymondеҲҷдҪҝз”ЁIShellDispatch2.ShellExecute()жҺҘеҸЈж–№жі•жқҘ委жүҳз»ҷйқһжҸҗеҚҮз”ЁжҲ·жӯЈеңЁиҝҗиЎҢзҡ„ExplorerеүҜжң¬гҖӮ - Remy Lebeau
我刚刚发现了这段代码的用途,因此在我的回答中添加了一些C++异常处理。谢谢! - zett42
我在我们的HTML5帮助中使用这段代码,以避免出于安全原因从我们的提升工具启动提升的浏览器。经过QA测试,这段代码运行良好。 - Paul Baxter
这个使用的是 CreateProcessWithToken 而不是 IShellDispatch2::ShellExecute: https://dev59.com/5KTja4cB1Zd3GeqPF7Ea#45921237 这意味着 (1) 完全不需要运行 explorer.exe 来复制令牌,在我的情况下,这是 smss.exe 进程。(2) 当 ShellExecute 不返回任何子进程句柄时,仍然可以等待子进程关闭(以及与 CreateProcess 函数相关的许多其他功能)。 - Andry
显示剩余5条评论

2
这里是Raymond Chen的方法,具备错误处理功能,且没有可怕的异常。
#include <atlbase.h>
#include <Shlobj.h>

HRESULT FindDesktopFolderView(REFIID riid, void **ppv)
{
HRESULT hr;
CComPtr<IShellWindows> spShellWindows;
hr = spShellWindows.CoCreateInstance(CLSID_ShellWindows);
if(FAILED(hr))
    return hr;

CComVariant vtLoc { 0 };    // 0 = CSIDL_DESKTOP
CComVariant vtEmpty;
long lhwnd = 0;
CComPtr<IDispatch> spdisp;
hr = spShellWindows->FindWindowSW(&vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);
if(FAILED(hr))
    return hr;

CComQIPtr<IServiceProvider> spProv{ spdisp };
if(!spProv)
    return E_NOINTERFACE;

CComPtr<IShellBrowser> spBrowser;
hr = spProv->QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&spBrowser));
if(FAILED(hr))
    return hr;

CComPtr<IShellView> spView;
hr = spBrowser->QueryActiveShellView(&spView);
if(FAILED(hr))
    return hr;

return spView->QueryInterface(riid, ppv);
}


HRESULT GetDesktopAutomationObject(REFIID riid, void **ppv)
{
HRESULT hr;
CComPtr<IShellView> spsv;
hr = FindDesktopFolderView(IID_PPV_ARGS(&spsv));
if(FAILED(hr))
    return hr;
if(!spsv)
    return E_NOINTERFACE;

CComPtr<IDispatch> spdispView;
hr = spsv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView));
if(FAILED(hr))
    return hr;

return spdispView->QueryInterface(riid, ppv);
}


HRESULT ShellExecuteFromExplorer(PCWSTR pszFile, PCWSTR pszParameters, PCWSTR pszDirectory, PCWSTR pszOperation, int nShowCmd)
{
HRESULT hr;
CComPtr<IShellFolderViewDual> spFolderView;
hr = GetDesktopAutomationObject(IID_PPV_ARGS(&spFolderView));
if(FAILED(hr))
    return hr;

CComPtr<IDispatch> spdispShell;
hr = spFolderView->get_Application(&spdispShell);
if(FAILED(hr))
    return hr;

CComQIPtr<IShellDispatch2> spdispShell2{ spdispShell };
if(!spdispShell2)
    return E_NOINTERFACE;

// without this, the launched app is not moved to the foreground
AllowSetForegroundWindow(ASFW_ANY);

return spdispShell2->ShellExecute(
        CComBSTR{ pszFile },
        CComVariant{ pszParameters ? pszParameters : L"" },
        CComVariant{ pszDirectory ? pszDirectory : L"" },
        CComVariant{ pszOperation ? pszOperation : L"" },
        CComVariant{ nShowCmd } );
}

2
这里是我对Raymond Chen的代码进行了修改,通过C++异常添加了错误处理。
首先,我们声明了一些辅助函数,用于从HRESULT抛出std::system_error异常、将GUID转换为字符串以及COM初始化的RAII包装器。
#include <windows.h>
#include <shldisp.h>
#include <shlobj.h>
#include <exdisp.h>
#include <atlbase.h>
#include <stdlib.h>
#include <iostream>
#include <system_error>

template< typename T >
void ThrowIfFailed( HRESULT hr, T&& msg )
{
    if( FAILED( hr ) )
        throw std::system_error{ hr, std::system_category(), std::forward<T>( msg ) };
}

template< typename ResultT = std::string >
ResultT to_string( REFIID riid )
{
    LPOLESTR pstr = nullptr;
    if( SUCCEEDED( ::StringFromCLSID( riid, &pstr ) ) ) 
    {
        // Use iterator arguments to cast from wchar_t to char if element type of ResultT is char.
        ResultT result{ pstr, pstr + wcslen( pstr ) };
        ::CoTaskMemFree( pstr ); pstr = nullptr;
        return result;
    }
    return {};
}

struct ComInit
{
    ComInit() { ThrowIfFailed( ::CoInitialize( nullptr ), "Could not initialize COM" ); }
    ~ComInit() { ::CoUninitialize(); }
    ComInit( ComInit const& ) = delete;
    ComInit& operator=( ComInit const& ) = delete;
};

这部分后面是实际执行工作的函数。基本上,这段代码与Remy Lebeau的回答中的代码相同,但增加了错误处理(以及我的个人格式风格)。
void FindDesktopFolderView( REFIID riid, void **ppv )
{
    CComPtr<IShellWindows> spShellWindows;
    ThrowIfFailed( 
        spShellWindows.CoCreateInstance( CLSID_ShellWindows ),
        "Could not create instance of IShellWindows" );

    CComVariant vtLoc{ CSIDL_DESKTOP };
    CComVariant vtEmpty;
    long lhwnd = 0;
    CComPtr<IDispatch> spdisp;
    ThrowIfFailed( 
        spShellWindows->FindWindowSW(
            &vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp ),
        "Could not find desktop shell window" );

    CComQIPtr<IServiceProvider> spProv{ spdisp };
    if( ! spProv )
        ThrowIfFailed( E_NOINTERFACE, "Could not query interface IServiceProvider" );

    CComPtr<IShellBrowser> spBrowser;
    ThrowIfFailed( 
        spProv->QueryService( SID_STopLevelBrowser, IID_PPV_ARGS(&spBrowser) ),
        "Could not query service IShellBrowser" );

    CComPtr<IShellView> spView;
    ThrowIfFailed( 
        spBrowser->QueryActiveShellView( &spView ),
        "Could not query active IShellView" );

    ThrowIfFailed( 
        spView->QueryInterface( riid, ppv ),
        "Could not query interface " + to_string( riid ) + " from IShellView" );
}

void GetDesktopAutomationObject( REFIID riid, void **ppv )
{
    CComPtr<IShellView> spsv;
    FindDesktopFolderView( IID_PPV_ARGS(&spsv) );

    CComPtr<IDispatch> spdispView;
    ThrowIfFailed( 
        spsv->GetItemObject( SVGIO_BACKGROUND, IID_PPV_ARGS(&spdispView) ),
        "Could not get item object SVGIO_BACKGROUND from IShellView" );
    ThrowIfFailed( 
        spdispView->QueryInterface( riid, ppv ),
        "Could not query interface " + to_string( riid ) + " from ShellFolderView" );
}

void ShellExecuteFromExplorer(
    PCWSTR pszFile,
    PCWSTR pszParameters = nullptr,
    PCWSTR pszDirectory  = nullptr,
    PCWSTR pszOperation  = nullptr,
    int nShowCmd         = SW_SHOWNORMAL)
{
    CComPtr<IShellFolderViewDual> spFolderView;
    GetDesktopAutomationObject( IID_PPV_ARGS(&spFolderView) );

    CComPtr<IDispatch> spdispShell;
    ThrowIfFailed( 
        spFolderView->get_Application( &spdispShell ),
        "Could not get application object from IShellFolderViewDual" );

    CComQIPtr<IShellDispatch2> spdispShell2{ spdispShell };
    if( !spdispShell2 )
        ThrowIfFailed( E_NOINTERFACE, "Could not query interface IShellDispatch2" );

    ThrowIfFailed( 
        spdispShell2->ShellExecute(
            CComBSTR{ pszFile },
            CComVariant{ pszParameters ? pszParameters : L"" },
            CComVariant{ pszDirectory ? pszDirectory : L"" },
            CComVariant{ pszOperation ? pszOperation : L"" },
            CComVariant{ nShowCmd } ),
        "ShellExecute failed" );
}

用法示例:
int main()
{
    try
    {
        ComInit init;
        ShellExecuteFromExplorer( L"http://www.stackoverflow.com" );
    }
    catch( std::system_error& e )
    {
        std::cout << "ERROR: " << e.what() << "\n"
            << "Error code: " << e.code() << std::endl;
    }
}

附加说明:
使用此方法时,您可能会注意到启动的应用程序窗口并不总是出现在前景,特别是如果它已经在运行。我的解决方法是在调用 ShellExecuteFromExplorer() 之前调用 AllowSetForegroundWindow( ASFW_ANY ),以使进程能够将自己置于前景(我们无法事先知道将要启动的进程的进程 ID)。

0

这个解决方案有点复杂。也许你不能通过获取某种访问令牌并将其传递给createprocesswithtoken直接从提升的管理员转换为非提升的管理员,但是你可以在另一个方向上进一步进行一步。你可以从提升的管理员转换为系统帐户,该帐户具有更高的特权。从系统帐户特权,你应该能够以非提升的管理员上下文启动进程。使用关键字“模拟”进行搜索可以给你很多例子。 那么如何从提升的管理员转换为系统?你只能编写一个系统服务,并在提升的管理员上下文中创建/启动该服务。


我之所以选择这个复杂的答案作为“适合我的”答案的原因是:我可以获得一个进程ID,用于后续的等待函数。在shellexecute方法中,我没有找到一种简单的方法来识别由我的代码确切启动的进程ID。 - H. Tao
你能提供更多有关这个模拟的细节吗?需要用户的密码才能进行吗? - zimmerrol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接