在 Windows 上,用户对权限并不敏感,可能最为直观的是 UAC ,但相信很多人已经关掉了它的提示。
但其实安全性早已深入了 Windows 的方方面面。Windows Vista 引入了一个称为强制完整性控制(Mandatory Integrity Controls,MIC)的新安全结构,类似于 Linux/Unix 中可用的完整性功能。在 Windows Vista 以及后续版本如Windows 11/10和Windows 8/7中,所有安全主体(用户、计算机、服务等)和对象(文件、注册表键、文件夹和资源)都被赋予MIC标签。
权限由高到低分别为Low、Medium、High、System。
如多登录用户是管理员账户,且开启了UAC,那么默认情况下双击一个程序时是以medium权限运行,称为受限的管理员权限,如果右键程序“以管理员权限运行”,那么程序是high权限,称为不受限的管理员权限。Low权限很少见,浏览器的某些进程就是Low权限的。Windows服务一般是System权限。
创建进程
当用户尝试启动可执行文件时,会使用最低用户完整性级别和文件完整性级别创建新进程。 这意味着新进程永远不会以比可执行文件更高的完整性执行。 如果管理员用户执行低完整性程序,则新进程的令牌会以低完整性级别运行。
简单来说,进程启动的子进程,默认情况下权限只会等于或小于其父进程。
如何以编程方式控制进程的执行级别?当用户启动应用程序时,其提升级别由其清单中 requestedExecutionLevel
属性的值确定,Windows 的用户帐户控制 (UAC) 会根据它采取适当的操作(例如,在需要时显示提升提示等)。但是,如果应用程序需要启动与应用程序本身执行级别不同的新进程,该怎么办?
获取当前进程执行级别
通过 GetTokenInformation 即可获取 TOKEN_ELEVATION_TYPE。
HANDLE hToken = NULL;
TOKEN_ELEVATION_TYPE tet_{};
if (::OpenProcessToken(
::GetCurrentProcess(),
TOKEN_QUERY,
&hToken))
{
DWORD dwReturnLength = 0;
if (::GetTokenInformation(
hToken,
TokenElevationType,
&tet_,
sizeof(*ptet),
&dwReturnLength)) {
//
}
}
::CloseHandle(hToken);
TOKEN_ELEVATION_TYPE 的取值分别是:
UAC 已禁用,或者进程由标准用户(不是 Administrators 组的成员)启动。 |
进程正在提升运行。 |
进程未提升运行。 |
仅当 UAC 都已启用且用户是管理员组的成员(即用户具有“拆分”令牌)时,才能返回最后两个值。
提权
有的时候我们需要子进程以较高的权限执行以完成其功能。于是,我们需要提升子进程的权限。
以产品更新为例,大部分时候产品主程序以在标准(非提升)级别运行,为了能够自我更新,它需要启动一个单独的更新进程,该更新进程需要提升才能正确执行升级。在这种情况下,非提升进程需要启动新的提升进程,则需要提权。
提升到管理员权限
- ShellExecute
很简单,通过 ShellExecute ,将 lpOperation 参数设置为 "runas" 即可
HINSTANCE hRet = ::ShellExecute(NULL, L"runas", pszFileName, NULL, NULL, SW_SHOWNORMAL);
if (32 < (DWORD)hRet)
{
return TRUE;
}
return FALSE;
当然,如果当前用户不属于管理员组,则会弹出 UAC 提示 -- 我们并不是为了绕过系统限制,仅仅是合理的请求高权限执行。
提升到 System 权限
- PsExec 工具
PsExec 是一个轻量级的 telnet-replacement,允许您在其他系统上执行进程。PsExec 最强大的用途包括在远程系统上启动交互式命令提示符。
其中,它提供了一个非常有用的参数
-s | 在系统帐户中运行远程进程。 |
如下命令即可将 foo.exe 以管理员权限启动,但需要首先以管理员权限执行这条命令:
PsExec.exe -i -s foo.exe
- 系统服务
系统中的服务进程默认都是运行在session 0下,具有 system 权限,如服务宿主进程 svchost.exe,其 Integrity 为 System。
创建一个服务,再通过服务启动子进程,则子进程默认就是 system 权限,当然创建、启动服务也需要管理员权限。
- 计划任务
可以通过批处理加创建最高权限(system)的计划任务
schtasks /Create /TN footask /SC DAILY /ST 01:00 /TR notepad.exe /RL HIGHEST
降权
还是以产品更新为例,当以提升级别运行的更新程序完成更新任务,需要启动产品主进程,以标准(非提升)级别运行,此时则需要降权。
和常识可能有点相悖,相对提权,降权难度更大一些。
降至指定权限
- 设置ProcessToken的IntegrityLevel
#include <Windows.h>
#include <sddl.h>
#pragma comment(lib, "Advapi32.lib")
void CreateIntegritySidProcess(WCHAR* wszIntegritySid)
{
BOOL bRet = FALSE;
HANDLE hToken = NULL;
HANDLE hNewToken = NULL;
// Notepad is used as an example
WCHAR wszProcessName[MAX_PATH] =L"C:\\Windows\\System32\\Notepad.exe";
PSID pIntegritySid = NULL;
TOKEN_MANDATORY_LABEL TIL = { 0 };
PROCESS_INFORMATION ProcInfo = { 0 };
STARTUPINFO StartupInfo = { 0 };
ULONG ExitCode = 0;
__try
{
if (FALSE == OpenProcessToken(GetCurrentProcess(), MAXIMUM_ALLOWED, &hToken))
{
__leave;
}
if (FALSE == DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL,
SecurityImpersonation, TokenPrimary, &hNewToken))
{
__leave;
}
if (FALSE == ConvertStringSidToSid(wszIntegritySid, &pIntegritySid))
{
__leave;
}
TIL.Label.Attributes = SE_GROUP_INTEGRITY;
TIL.Label.Sid = pIntegritySid;
// Set the process integrity level
if (FALSE == SetTokenInformation(hNewToken, TokenIntegrityLevel, &TIL,
sizeof(TOKEN_MANDATORY_LABEL)+GetLengthSid(pIntegritySid)))
{
__leave;
}
bRet = CreateProcessAsUser(hNewToken, NULL,
wszProcessName, NULL, NULL, FALSE,
0, NULL, NULL, &StartupInfo, &ProcInfo);
}
__finally
{
if (NULL != pIntegritySid)
{
LocalFree(pIntegritySid);
pIntegritySid = NULL;
}
if (NULL != hNewToken)
{
CloseHandle(hNewToken);
hNewToken = NULL;
}
if (NULL != hToken)
{
CloseHandle(hToken);
hToken = NULL;
}
}
printf("%ls bRet:%d\n", wszIntegritySid, bRet);//%ls打印宽字符
}
int _tmain(int argc, _TCHAR* argv[])
{
/*
Low (SID: S-1-16-4096),
Medium (SID: S-1-16-8192),
High (SID: S-1-16-12288)
System (SID: S-1-16-16384).
*/
//创建不同权限的进程
CreateIntegritySidProcess(L"S-1-16-4096");//low权限进程
CreateIntegritySidProcess(L"S-1-16-8192");//medium权限进程
CreateIntegritySidProcess(L"S-1-16-12288");//high权限进程
CreateIntegritySidProcess(L"S-1-16-16384");//system权限进程
printf("end\n");
getchar();
return 0;
}
通过上述代码可知,关键是获取适当的 IntegrityLevel 。举一反三,如果我们获取到 explorer.exe 进程的Token 的 IntegrityLevel,就可以以 explorer.exe 的权限启动新进程,效果和桌面双击程序启动一样。
另外,也可以通过 AllocateAndInitializeSid 指定 SECURITY_MANDATORY_LOW_RID 创建一个低权限的SID,这里用武稀松前辈的 Delphi 代码演示:
uses
WinApi.Windows;
const
SECURITY_MANDATORY_UNTRUSTED_RID = $00000000;
SECURITY_MANDATORY_LOW_RID = $00001000;
SECURITY_MANDATORY_MEDIUM_RID = $00002000;
SECURITY_MANDATORY_HIGH_RID = $00003000;
SECURITY_MANDATORY_SYSTEM_RID = $00004000;
SECURITY_MANDATORY_PROTECTED_PROCESS_RID = $00005000;
function CreateLowIntegrityProcess(const ExeName: string;
const Params: string = ”; TimeOut: DWORD = 0): HResult;
function GetIntegrityLevel(): DWORD;
implementation
type
PTokenMandatoryLabel = ^TTokenMandatoryLabel;
TTokenMandatoryLabel = packed record
Label_: TSidAndAttributes;
end;
function GetIntegrityLevel(): DWORD;
var
hProcess, hToken: THandle;
pTIL: PTokenMandatoryLabel;
dwReturnLength: DWORD;
dwTokenUserLength: DWORD;
psaCount: PUCHAR;
SubAuthority: DWORD;
begin
Result := 0;
dwReturnLength := 0;
dwTokenUserLength := 0;
pTIL := nil;
hProcess := GetCurrentProcess();
OpenProcessToken(hProcess, TOKEN_QUERY or TOKEN_QUERY_SOURCE, hToken);
if hToken = 0 then
Exit;
if not GetTokenInformation(hToken, WinApi.Windows.TTokenInformationClass
(TokenIntegrityLevel), pTIL, dwTokenUserLength, dwReturnLength) then
begin
if GetLastError = ERROR_INSUFFICIENT_BUFFER then
Begin
pTIL := Pointer(LocalAlloc(0, dwReturnLength));
if pTIL = nil then
Exit;
dwTokenUserLength := dwReturnLength;
dwReturnLength := 0;
if GetTokenInformation(hToken, WinApi.Windows.TTokenInformationClass
(TokenIntegrityLevel), pTIL, dwTokenUserLength, dwReturnLength) and
IsValidSid((pTIL.Label_).Sid) then
begin
psaCount := GetSidSubAuthorityCount((pTIL.Label_).Sid);
SubAuthority := psaCount^;
SubAuthority := SubAuthority – 1;
Result := GetSidSubAuthority((pTIL.Label_).Sid, SubAuthority)^;
end;
LocalFree(Cardinal(pTIL));
End;
end;
CloseHandle(hToken);
end;
const
userenvlib = ‘ userenv.dll ’;
function CreateEnvironmentBlock(lpEnvironment: PPointer; hToken: THandle;
bInherit: BOOL): BOOL; stdcall; external userenvlib;
function DestroyEnvironmentBlock(lpEnvironment: Pointer): BOOL; stdcall;
external userenvlib;
function CreateLowIntegrityProcess(const ExeName, Params: string;
TimeOut: DWORD): HResult;
type
_TOKEN_MANDATORY_LABEL = Record
Label_: SID_AND_ATTRIBUTES;
End;
TOKEN_MANDATORY_LABEL = _TOKEN_MANDATORY_LABEL;
PTOKEN_MANDATORY_LABEL = ^TOKEN_MANDATORY_LABEL;
const
SECURITY_MANDATORY_LABEL_AUTHORITY: TSidIdentifierAuthority =
(Value: (0, 0, 0, 0, 0, 16));
SE_GROUP_INTEGRITY = $00000020;
SE_GROUP_INTEGRITY_ENABLED = $00000040;
var
hToken, hNewToken: THandle;
MLAuthority: SID_IDENTIFIER_AUTHORITY;
pIntegritySid: PSID;
tml: TOKEN_MANDATORY_LABEL;
si: TStartupInfo;
pi: PROCESS_INFORMATION;
pszCommandLine: string;
dwCreationFlag: DWORD;
pEnvironment: LPVOID;
begin
Result := ERROR_SUCCESS;
pszCommandLine := ExeName + Params;
hToken := 0;
hNewToken := 0;
MLAuthority := SECURITY_MANDATORY_LABEL_AUTHORITY;
pIntegritySid := nil;
FillChar(tml, sizeof(tml), 0);
FillChar(si, sizeof(si), 0);
FillChar(pi, sizeof(pi), 0);
si.cb := sizeof(si);
si.lpDesktop := ‘ Winsta0 \ Default ’;
dwCreationFlag := NORMAL_PRIORITY_CLASS or CREATE_NEW_CONSOLE;
pEnvironment := nil;
try
// 从自己获取一个令牌
if (not OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE or
TOKEN_QUERY or TOKEN_ADJUST_DEFAULT or TOKEN_ASSIGN_PRIMARY, hToken)) then
begin
Result := GetLastError();
Exit;
end;
// 复制令牌
if (not DuplicateTokenEx(hToken, 0, nil, SecurityImpersonation,
TokenPrimary, hNewToken)) then
begin
Result := GetLastError();
Exit;
end;
// 创建一个低权限的SID
if (not AllocateAndInitializeSid(MLAuthority, 1, SECURITY_MANDATORY_LOW_RID,
0, 0, 0, 0, 0, 0, 0, pIntegritySid)) then
begin
Result := GetLastError();
Exit;
end;
tml.Label_.Attributes := SE_GROUP_INTEGRITY;
tml.Label_.Sid := pIntegritySid;
// 设置这个低权限SID到令牌
if (not SetTokenInformation(hNewToken, TokenIntegrityLevel, @tml,
(sizeof(tml) + GetLengthSid(pIntegritySid)))) then
begin
Result := GetLastError();
Exit;
end;
// 创建一个环境变量
if (CreateEnvironmentBlock(@pEnvironment, hToken, FALSE)) then
dwCreationFlag := dwCreationFlag or CREATE_UNICODE_ENVIRONMENT
else
pEnvironment := nil;
// 创建一个低权限的进程
if (not CreateProcessAsUser(hNewToken, nil, PChar(pszCommandLine), nil, nil,
FALSE, dwCreationFlag, pEnvironment, nil, si, pi)) then
begin
Result := GetLastError();
Exit;
end;
WaitForSingleObject(pi.hProcess, TimeOut);
finally
// 清理现场
if pEnvironment <> nil then
begin
DestroyEnvironmentBlock(pEnvironment);
pEnvironment := nil;
end;
if (hToken <> 0) then
begin
CloseHandle(hToken);
hToken := 0;
end;
if (hNewToken <> 0) then
begin
CloseHandle(hNewToken);
hNewToken := 0;
end;
if (pIntegritySid <> nil) then
begin
FreeSid(pIntegritySid);
pIntegritySid := nil;
end;
if (pi.hProcess <> 0) then
begin
CloseHandle(pi.hProcess);
pi.hProcess := 0;
end;
if (pi.hThread <> 0) then
begin
CloseHandle(pi.hThread);
pi.hThread := 0;
end;
if (ERROR_SUCCESS <> Result) then
begin
SetLastError(Result);
end
else
begin
Result := ERROR_SUCCESS;
end;
end;
end;
- 计划任务
没错,还是它,创建计划任务默认不要求高权限时就可以当前用户权限启动指定程序
schtasks /Create /TN footask /SC DAILY /ST 01:00 /TR notepad.exe
当然,创建、启动计划任务本身需要管理员权限。
像桌面一样启动进程
如果当前进用户本身是低权限,但当前进程是高权限运行,需要新的子进程以当前用户身份启动,那我们就可以让 explorer 帮我们以它的上下文方式启动子进程,这里依然用 Delphi 代码演示:
uses
ComObj, ShlObj, ActiveX, SHDocVw;
function IUnknown_QueryService(punk: IUnknown; const guidService: TGUID;
const IID: TGUID; out Obj): HRESULT; stdcall;
external 'ShLwApi' name 'IUnknown_QueryService';
procedure TForm1.Button1Click(Sender: TObject);
var
hDesk: Integer;
psw: IShellWindows;
v, dummy: OleVariant;
disp, pdispBackground, pdisp2: IDispatch;
psb: IShellBrowser;
psv: IShellView;
psfvd: IShellFolderViewDual;
psd: IShellDispatch2;
begin
CoInitialize(nil);
try
OleCheck(CoCreateInstance(CLASS_ShellWindows, nil, CLSCTX_LOCAL_SERVER,
IShellWindows, psw));
disp := psw.FindWindowSW(v, dummy, SWC_DESKTOP, hDesk, SWFO_NEEDDISPATCH);
OleCheck(IUnknown_QueryService(disp, SID_STopLevelBrowser,
IID_IShellBrowser, psb));
OleCheck(psb.QueryActiveShellView(psv));
OleCheck(psv.GetItemObject(SVGIO_BACKGROUND, IDispatch,
Pointer(pdispBackground)));
OleCheck(pdispBackground.QueryInterface(IShellFolderViewDual, psfvd));
OleCheck(psfvd.get_Application(pdisp2));
OleCheck(pdisp2.QueryInterface(IShellDispatch2, psd));
psd.ShellExecute('notepad.exe', 'C:\Windows\WindowsUpdate.log', EmptyParam,
'open', SW_SHOWNORMAL)
finally
CoUninitialize;
end;
end;
C++ 版本示例:
#include <windows.h>
#include <shlwapi.h>
#include <shlobj.h>
#pragma comment(lib, "shlwapi.lib")
// use the shell view for the desktop using the shell windows automation to find the
// desktop web browser and then grabs its view
//
// returns:
// IShellView, IFolderView and related interfaces
HRESULT GetShellViewForDesktop(REFIID riid, void **ppv)
{
*ppv = NULL;
IShellWindows *psw;
HRESULT hr = CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&psw));
if (SUCCEEDED(hr))
{
HWND hwnd;
IDispatch* pdisp;
VARIANT vEmpty = {}; // VT_EMPTY
if (S_OK == psw->FindWindowSW(&vEmpty, &vEmpty, SWC_DESKTOP, (long*)&hwnd, SWFO_NEEDDISPATCH, &pdisp))
{
IShellBrowser *psb;
hr = IUnknown_QueryService(pdisp, SID_STopLevelBrowser, IID_PPV_ARGS(&psb));
if (SUCCEEDED(hr))
{
IShellView *psv;
hr = psb->QueryActiveShellView(&psv);
if (SUCCEEDED(hr))
{
hr = psv->QueryInterface(riid, ppv);
psv->Release();
}
psb->Release();
}
pdisp->Release();
}
else
{
hr = E_FAIL;
}
psw->Release();
}
return hr;
}
// From a shell view object gets its automation interface and from that gets the shell
// application object that implements IShellDispatch2 and related interfaces.
HRESULT GetShellDispatchFromView(IShellView *psv, REFIID riid, void **ppv)
{
*ppv = NULL;
IDispatch *pdispBackground;
HRESULT hr = psv->GetItemObject(SVGIO_BACKGROUND, IID_PPV_ARGS(&pdispBackground));
if (SUCCEEDED(hr))
{
IShellFolderViewDual *psfvd;
hr = pdispBackground->QueryInterface(IID_PPV_ARGS(&psfvd));
if (SUCCEEDED(hr))
{
IDispatch *pdisp;
hr = psfvd->get_Application(&pdisp);
if (SUCCEEDED(hr))
{
hr = pdisp->QueryInterface(riid, ppv);
pdisp->Release();
}
psfvd->Release();
}
pdispBackground->Release();
}
return hr;
}
HRESULT ShellExecInExplorerProcess(PCWSTR pszFile)
{
IShellView *psv;
HRESULT hr = GetShellViewForDesktop(IID_PPV_ARGS(&psv));
if (SUCCEEDED(hr))
{
IShellDispatch2 *psd;
hr = GetShellDispatchFromView(psv, IID_PPV_ARGS(&psd));
if (SUCCEEDED(hr))
{
BSTR bstrFile = SysAllocString(pszFile);
hr = bstrFile ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr))
{
VARIANT vtEmpty = {}; // VT_EMPTY
hr = psd->ShellExecuteW(bstrFile, vtEmpty, vtEmpty, vtEmpty, vtEmpty);
SysFreeString(bstrFile);
}
psd->Release();
}
psv->Release();
}
return hr;
}
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if (SUCCEEDED(hr))
{
ShellExecInExplorerProcess(L"http://www.msn.com");
CoUninitialize();
}
return 0;
}
本篇内容介绍了几种提权、降权的方法,并不涉及到系统漏洞利用,抛砖引玉,希望对 Windows 进程权限方面感兴趣的朋友一起留言讨论,互相进步 🤝
参考
- Mandatory Integrity Control - Win32 apps | Microsoft Learn
- PsExec - Sysinternals | Microsoft Learn
- IShellDispatch2 object (Shldisp.h) - Win32 apps | Microsoft Learn
- 以低用户权限启动一个进程.比如Vista或者WIN7中的IE | 武稀松(wr960204)的博客 (raysoftware.cn)