在上文如何在windows开启UAC(用户账号控制)的情况下优雅的管理程序的权限申请方案(一)中,我们提到当一个Client.exe进程需要注入一个DLL到目标game.exe进程的时候出现自身权限不足而无法成功注入的情况,这个时候就必须通过UAC提权操作来达到目的了;那如何高雅的进行程序的提权操作呢。
如何高雅的进行程序的提权操作
- 方案一就是重启自身的Client.exe进程,通过ShellExecute(NULL, "runas", lpFile, lpParameters, NULL, SW_SHOW);的方式来重启自己,当下个实例启动的时候会弹出UAC提权窗口来让用户进行提权,这种方式有两大问题:
-
重启自身的时候,第一是成功启动下个实例后才退出自身实例,这个时候如果程序做了限制程序多实例运行,这个时候会有实例互斥需要做另外的规避处理;第二是为了规避实例的多开限制,在还未成功启动下个实例前就退出自身实例,但这样会导致如果用户没有同意下个实例的程序提权操作就会导致所有程序实例都退出了。
-
通过重启自身实例的方式来达到提权目的最大的缺点其实就是无法延续之前程序的状态,因为下个实例启动后无法直接切换到前实例的状态。比如笔者的场景就是在直播客户端开播的状态下要注入目标游戏进程,如果重启直播客户端,那么当前的开播状态就无法延续了,都得从头开始。
2. 方案二就是启动后台代理Proxy.exe进程,让这个代理进程以管理员权限启动,继而把所需要执行注入DLL的操作通知给这个代理进程,让这个代理进程执行相应的注入操作。
如上图所示,启动一个后台代理进程,这个进程以管理员权限启动后在后台等候客户发送注入DLL的指令,然后执行相应的注入操作,可以说是很完美了。也就是当客户端Client.exe需要注入目标进程的时候,发现自己权限不足:1.这个时候后台Proxy.exe进程还没起来,就以管理员权限方式启动Proxy.exe,如果用户同意提权启动,那么proxy.exe正常启动并执行相应的注入DLL的任务;如果用户不同意,客户端显示注入DLL失败的提示,直到下次再次有注入DLL的需求才再次启动Proxy.exe并再次将选择权交给用户。2.如果后台Proxy.exe进程已经起来了,那么这个时候就可以直接把任务交给这个后台监控进程进行相应的注入操作了。
更完美的解决方案
如上面所说,启动一个代理Proxy.exe后台进程来进行相应的DLL注入操作,当有新的注入任务的时候直接交给这个代理进程,这看起来已经很完美了,但我这里还有一个更完美的解决方案,就是把这个Proxy.exe做成一个服务程序。一个服务程序跟普通程序最大的好处就是:这个服务程序可以配置为开机启动,并且开机启动的时候不会弹出UAC提权窗口来让用户进行选择,启动后服务程序的权限是System权限,System权限是很高的,甚至比管理员权限还高。当然普通程序也可以做到开机启动,但是如果程序需要有管理员权限运行的情况是程序是无法绕开UAC提权弹窗的,也就是说一旦需要UAC提权弹窗,那么就把选择利交给了用户,用户一旦不同意程序的启动,意味着进程将启动失败。
这样来看,似乎一个服务程序是非常完美的解决方案,只需要在安装服务的时候弹出UAC弹窗让用户同意即可完成服务的安装,只要服务一旦安装完成后面的事似乎就可以畅通无阻了;嗯,想想都不由让人兴奋不已 :)。可后来发现似乎并没那么简单,服务程序看似完美,可他有自己的一些与常规程序与众不同特性甚至让那些对服务程序不熟悉的人一头雾水、莫名其妙。
Windows服务(service)程序“与众不同”
-
首先如何在windows安装一个服务呢,这里用到了一个windows的API:
SC_HANDLE WINAPI CreateService(
_In_ SC_HANDLE hSCManager,
_In_ LPCTSTR lpServiceName,
_In_opt_ LPCTSTR lpDisplayName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwServiceType,
_In_ DWORD dwStartType,
_In_ DWORD dwErrorControl,
_In_opt_ LPCTSTR lpBinaryPathName,
_In_opt_ LPCTSTR lpLoadOrderGroup,
_Out_opt_ LPDWORD lpdwTagId,
_In_opt_ LPCTSTR lpDependencies,
_In_opt_ LPCTSTR lpServiceStartName,
_In_opt_ LPCTSTR lpPassword
);
参数太多,不一一介绍,详细介绍可以查看MSDN。其中第六个参数:dwStartType,代表启动方式:
SERVICE_AUTO_START 表示自动启动,这个参数就是我们想要的。SERVICE_BOOT_START 也属于自动启动,但是只能用于内核服务。SERVICE_DEMAND_START 手动启动,这是目前服务的默认启动方式。SERVICE_DISABLED 禁止启动。SERVICE_SYSTEM_START 属于自动启动,但只能用于内核服务。
- 那么一个服务安装成功之后呢,这个程序貌似跟普通运行的程序并没有多大区别;可当我们打开任务管理器并勾选查看会话ID来查看进程的会话ID就会发现不同之处了。
“CNCBUK2WDMon.exe”进程其实就是一个服务进程,他跟普通进程的最大区别好像是:1.进程权限是SYSTEM权限,普通进程权限是Administrator权限 2.会话ID是0,普通进程会话ID是1。
- 首先一个服务进程的默认权限是SYSTEM的,这个权限是比较高的,比管理员的权限还高。也就意味着,管理员权限进程能做的事对于它而言基本是没有权限限制的。
- 这里主要讲到windows会话(Session)机制了。首先我们知道windows是多用户的操作系统,多用户就意味着同一台机器可以同时登陆多个账户,当然不同用户之间是相互隔离的,他们之间可以说是互相感受不知道对方的存在的,也就是说windows把他们隔离在不同的会话(Session)当中,它们会有各自的私有桌面目录、私有桌面窗口、私有注册表项等;每个会话有自己的会话ID;而所有的服务程序会统一分配在会话(Session)0当中。这个时候管理员通过winlogon.exe登录进来,会分配到Session 1当中,假如这个时候再通过winlogon.exe登录一个用户账户,这个时候我们会发现多出了个会话(Session)2。他们之间相互隔离几乎无法感知,而且我们发现每个账户有自己独有的winlogon.exe进程。
不同会话(Session)之间的相互隔离是什么意思呢:
- 每个会话有单独的私有目录,比如“C:\Users\Administrator”是Admin的私有目录,“C:\Users\Administrator\Desktop”桌面目录也是私有目录,不同的会话之间是互相不可访问的。
- 每个会话所在的桌面(windows多桌面)是不相同的,也就是会话1启动的进程的界面在会话1的私有桌面显示,会话2启动的进程界面在会话2的私有桌面显示,不同的私有桌面之间界面相互隔离互不可见。
- 每个会话有私有的注册表项:HKEY_CURRENT_USER,这个注册表项是针对不同的用户的,不同用户对应这个注册表项各不一样,互相之间相互隔离无法访问。
- ..............
也就是说把一个普通程序转成一个服务可能会遇到很多“莫名其妙”的问题,这些问题可能是无法绕开和规避的。比如访问用户的私有目录或桌面,这对于服务进程来说是无法跨Session做到的。
进程的跨会话(Session)登录和运行
前面说到不同会话(Session)间的进程是相互隔离,它们总是有着无法跨越的障碍。那么有没有好的解决方案来规避和解决这个问题呢。有,就是进程的跨会话(Session)登录。也就是说让会话0的服务进程,跨越会话(session)登录到会话1。当然必须自身重启自身的进程,用目标会话(Session)的token来重启自身进程来达到跨会话的目的。大致流程如下:
跨Session启动进程的流程如上,也就是说当服务进程启动后马上开启一个新的跨Session的进程,自己就可以优雅的退出了,然后后面事情就可以全权交给那个新进程去处理了。而这个新启动的进程实例所拥有的权限依然继承父进程也就是System权限,但是所在SessionID就是当前Session的ID了,这样跟当前Session的进程一样几乎可以自由的通信或者进行界面显示了。所以整个流程可以概况如下:
附录:
关于跨Session重启进程的部分代码如下:
BOOL ShexecProInUserSession(const std::wstring& strProcPath, const std::wstring& strParam, int iShowWindow, DWORD dwWaiteTime)
{
BOOL bResult = FALSE;
DWORD dwSessionId, winlogonPid;
HANDLE hUserToken = NULL, hUserTokenDup = NULL, hPToken = NULL, hProcess = NULL;
DWORD dwCreationFlags;
// Get currentConsole Session ID
dwSessionId = WTSGetActiveConsoleSessionId();
PROCESSENTRY32 procEntry;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
procEntry.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(hSnap, &procEntry))
{
return FALSE;
}
do
{
if (strcmp(procEntry.szExeFile, "winlogon.exe") == 0)
{
DWORD winlogonSessId = 0;
if (ProcessIdToSessionId(procEntry.th32ProcessID, &winlogonSessId))
{
SvcDebugOut(L"WinLogon Session ID %d.", winlogonSessId);
if (winlogonSessId == dwSessionId)
{
winlogonPid = procEntry.th32ProcessID;
break;
}
}
else
{
SvcDebugOut(L"Problem in ProcessIdToSessionId, err:%d.", GetLastError());
}
}
//LogEvent(L"Process Name = %ws, ID = %d\n", procEntry.szExeFile, procEntry.th32ProcessID);
} while (Process32Next(hSnap, &procEntry));
enable_priv(SE_CREATE_TOKEN_NAME, true);
enable_priv(SE_SECURITY_NAME, true);
enable_priv(SE_DEBUG_NAME, true);
bool isValid_ = (bool)!!WTSQueryUserToken(dwSessionId, &hUserToken);
if (!isValid_)
{
SvcDebugOut(L"Problem in WTSQueryUserToken Error:%d.", GetLastError());
}
dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;
TOKEN_PRIVILEGES tp;
LUID luid;
hProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, winlogonPid);
if (!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY
| TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_SESSIONID
| TOKEN_READ | TOKEN_WRITE, &hPToken))
{
SvcDebugOut(L"Problem in OpenProcessToken Error:%d.", GetLastError());
}
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
{
SvcDebugOut(L"Problem in LookupPrivilegeValue Error:%d.", GetLastError());
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hUserTokenDup);
//Adjust Token privilege
SetTokenInformation(hUserTokenDup, TokenSessionId, (void*)dwSessionId, sizeof(DWORD));
//本身的权限是S 下面降低下权限到 H lzlong add H-S 才能成功注入 但不需要太高的权限 以免跟助手通信有问题
//'L': wszIntegritySid=L"S-1-16-4096"; 'M': wszIntegritySid=L"S-1-16-8192";
//'H': wszIntegritySid=L"S-1-16-12288"; 'S': wszIntegritySid=L"S-1-16-16384";
do
{
wchar_t il[32];
size_t cbIl = 32;
if (GetProcessIntegrityLevel(il, cbIl) == 0)
SvcDebugOut(L"service in GetProcessIntegrityLevel and Integrity lebel is:%d.", il[0]);
PSID pIntegritySid = NULL;
wchar_t *wszIntegritySid = L"S-1-16-12288";
if (!ConvertStringSidToSid(
wszIntegritySid,
&pIntegritySid)) {
SvcDebugOut(L"Problem in ConvertStringSidToSid Error:%d.", GetLastError());
break;
}
TOKEN_MANDATORY_LABEL til = { 0 };
til.Label.Attributes = SE_GROUP_INTEGRITY;
til.Label.Sid = pIntegritySid;
if (!SetTokenInformation(
hUserTokenDup,
TokenIntegrityLevel,
&til,
sizeof(TOKEN_MANDATORY_LABEL) + GetLengthSid(pIntegritySid))) {
SvcDebugOut(L"Problem in SetTokenInformation to hUserTokenDup Error:%d.", GetLastError());
}
} while (false);
if (!AdjustTokenPrivileges(hUserTokenDup, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, NULL))
{
SvcDebugOut(L"Problem in AdjustTokenPrivileges to hUserTokenDup Error:%d.", GetLastError());
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
{
SvcDebugOut(L"Token does not have the provilege Error:%d.", GetLastError());
}
LPVOID pEnv = NULL;
if (CreateEnvironmentBlock(&pEnv, hUserTokenDup, TRUE))
{
dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;
}
else
pEnv = NULL;
PROCESS_INFORMATION pi;
STARTUPINFO si;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
//si.lpDesktop = L"winsta0\\WinLogon";
si.lpDesktop = L"default";
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = iShowWindow/*SW_SHOW*/;
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
std::wstring strQuote = L"\"";
std::wstring strCmd;
wchar_t strWindir[256];
if (0 != GetWindowsDirectoryW(strWindir, 256))
{
strCmd += (strWindir);
}
else
{
strCmd += (L"C:\\Windows");
}
strCmd += L"\\Explorer.exe ";
strCmd += (L"/e, ");// 利用explorer来打开文件 或exe lzlong \\Explorer.exe
strCmd += strQuote;
strCmd += strProcPath;
strCmd += strQuote;
if (std::wstring::npos == strProcPath.find_last_of(L".exe", strProcPath.length() - 4)) //is not_end_by .exe
{
SvcDebugOut(L"CreateProcessAsUserW as not a exe %d.", 0);
bResult = CreateProcessAsUserW(hUserTokenDup, // client's access token
NULL,// file to execute
(LPWSTR)strCmd.c_str(), // command line
NULL, // pointer to process SECURITY_ATTRIBUTES
NULL, // pointer to thread SECURITY_ATTRIBUTES
FALSE, // handles are not inheritable
dwCreationFlags, // creation flags
pEnv, // pointer to new environment block
NULL, // name of current directory
&si, // pointer to STARTUPINFO structure
&pi // receives information about new process
);
}
else
{
SvcDebugOut(L"CreateProcessAsUserW as a exe %d.", 0);
bResult = CreateProcessAsUserW(hUserTokenDup,
(LPWSTR)strProcPath.c_str(),
(LPWSTR)strParam.c_str(),
NULL,
NULL,
FALSE,
dwCreationFlags,
pEnv,
NULL,
&si,
&pi);
if (pi.hProcess != NULL && pi.hProcess != INVALID_HANDLE_VALUE)
{
DWORD dwWaiteRt = WaitForSingleObject(pi.hProcess, dwWaiteTime);
DWORD dwExitCode = 0;
GetExitCodeProcess(pi.hProcess, &dwExitCode);
if (dwExitCode != 0 && dwWaiteRt != WAIT_TIMEOUT)
{
SvcDebugOut(L"CreateProcessAsUserW not ok dwExitCode: %d.", dwExitCode);
}
}
}
if (!bResult)
{
SvcDebugOut(L"CreateProcessAsUserW error ERROR: %d.", GetLastError());
}
int iResultOfCreateProcessAsUser = GetLastError();
if (pi.hProcess)
CloseHandle(pi.hProcess);
if (pi.hThread)
CloseHandle(pi.hThread);
if (hProcess)
CloseHandle(hProcess);
if (hUserToken)
CloseHandle(hUserToken);
if (hUserTokenDup)
CloseHandle(hUserTokenDup);
if (hPToken)
CloseHandle(hPToken);
if (pEnv)
DestroyEnvironmentBlock(pEnv);
return bResult;
}