0x00 前言
任何需要管理员访问令牌的程序都必须征得同意(即UAC弹窗),但是存在一个意外,即父进程和子进程之间存在关系,即子进程从父进程中继承访问令牌,这是UAC 绕过的基础。本文将选择UACME中的3种不同的ByPass UAC的例子,为各位讲解ByPass UAC的原理。然后分析UAC的基本基本原理,继而寻找ByPass UAC的通用检测策略,本文行文仓促,如有错误,请各位积极指正。
0x01 常见的ByPassUAC 原理
UACME项目中,主要的ByPass UAC方法主要有Shell API,Dll Hiject,Elevated COM interface,Whitelisted component。其中,比较常见的就是前三个。本文将根据UACME3.5.6这个版本的23,41,53号方法进行讲解。
UACME的第23号方法的类型是Dll Hijack。除了23号方法以外,UACME中同处于Dll Hijack还有18号,22号,26号,30号,37号,39号方法,其中UACME的23号方法主要是利用pkgmgr.exe实现ByPass UAC。其在Windows7下可以被成功利用,实现的方法是ucmDismMethod函数。
在编译完UACME,设置好参数(akagi.exe 23 calc.exe)之后,在MethodsManagerCall函数处下断。可以看到ucmDismMethod会依次调用ucmxGenericAutoelevation(),和ucmxDisemer()函数。其中ucmxGenericAutoelevation()主要是为了将所谓的proxy dll释放到C:\Users\kent\AppData\Local\Temp\dismcore.dl!。然后在利用COM接口——CLSID_FileOperation将文件复制到"C:\Windows\system32\dismcore.dll"。值得注意的是,如果程序是32位的话,会将dismcore.dl!释放到C:\Windows\SysWow64\目录下,如果程序是64位的话,则会释放到C:\Windows\system32。
接着调用ucmxDisemer()拉起一个pkgmgr.exe进程,从此完成ByPassUAC
为什么只需要短短两步就可以完成ByPass UAC?我们使用ProcessMon监控当执行Akagi.exe 23 calc.exe
之后,发生了什么。对ProcessMon做出如下配置。
可以清楚的看到,当akagi.exe拉起pkgmgr.exe之后,pkgmgr.exe会创建dism.exe进程。但是dism.exe又和有什么关系呢?我们试着带着上帝视野的角度看看当时Leo Davidson是怎么找到这个ByPass UAC方法的。
首先我们需要把握两个关键点,第一这是Dll HiJack方法,所以在这个方法中,关于加载Dll的行为一定要密切关注,第二,其所利用到的组件是DismCore.dll。综上两点,我们可到一个关键点,即dism.exe进程加载DismCore.dll的时候发生了ByPass UAC利用。并且从ProcessMon的细节中,也验证了我们的想法。
既然要加载dll,抛去那些复杂的注入的方式,常见的加载dll的函数无非就是LoadLibrary相关的函数,我们可以通过ProcessMon提供的Stack功能查看调用情况。很显然,dism.exe使用了LoadLibraryW加载DismCore.dll。
分析dism.exe,显然,可以将目标定位到CDismWrapper::FindAndLoadDism函数,在CDismWrapper::FindAndLoadDism()中,会调用CreateObjectFromDLL(),然后调用LoadLibraryW加载DismCore.dll,我们只需要覆盖原始的DismCore.dll,那么dism.exe就会加载我们创建的DismCore.dll,即可完成ByPassUAC。
UACME的第41号方法类型是Elevated COM interface,同属于Elevated COM interface的还有27号,42号,43号,48号,49号,50号,51号方法。鸡哥在从项目中看BypassUAC和BypassAMSI以及https://pingmaoer.github.io/2020/07/09/BypassUAC方法论学习/已经描述了Elevated COM interface的原理,即就是通过具有自动提权的COM接口对应的Dll创建进程实现ByPass UAC。所以使用这个方法需要两个条件。
- 第一:COM的Elevation的Enabled属性为True以及Auto Approval为True
- 第二:接口具有执行的功能。
除此以外,UACMe的Yuubari项目还可以帮助寻找可以被利用的COM组件。具体在BypassUAC方法论学习也有相关介绍。
UACME的53号方法需要在windows10下使用,其类型为Shell API,同属于Shell API类型的还有24号,25号,29号,33号,34号方法。这个类型下有通过修改注册表,通过环境变量,和Shell 劫持实现的,其中大多数通过修改注册表实现。实现的方法是ucmShellRegModMethod。
在设置好调试参数之后,在ucmShellRegModMethod处打个断点,可以看到ucmShellRegModMethod主要在HKCU主键下的\Folder\shell\open\command路径下创建一个符号链接,值就是传入的第三个参数,也就是需要ByPass UAC的程序。然后通过ShellExecuteEx起sdclt.exe进程,完成ByPass UAC之后做好清理即可。
首先,将ProcessMon设置成如下状态,可以看到akagi.exe首先会拉起sdclt.exe,这是已知的,然后添加参数“/name Microsoft.BackupAndRestoreCenter”,拉起“C:\Windows\System32\control.exe”进程。
接着,将ProcessMon设置成如下配置,我们看一下直接创建control.exe进程以及通过ByPass UAC,打开注册表HKCU\Software\Classes\Folder\shell\open\command的差异。如图红色框内,打开注册表的结果是NAME NOT FOUND,但是在绿色方框内,则显示REPARSE。然而我们也注意到在control.exe访问之前,Akagi.exe以及事先设置好了值进去。
0x02 UAC原理和逆向分析
了解过UAC工作原理的师傅都知道,当调用类似于CreateProcess的进程创建函数之后,通过RPC过程调用appinfo文件中的RAiLaunchAdminProcess函数,appinfo主要用于UAC权限验证,如果符合特定的条件,则自动将进程的完整性级别提升到High,否则的话,则会通过UAC弹窗请用户选择。
说到完整性级别,windows操作系统的进程完整性级别一共有4个,分别是Low(低完整性级别),Medium(中完整性级别),High(高完整性级别),System(系统)。在标准模式下,程序一般都运行在Medum级别,等到UAC提升之后,才会提升到High完整性级别。
在RAiLaunchAdminProcess()中,首先,会调用CheckElevationEnabled检查是否启用了UAC。
然后调用AiCheckSecureApplicationDirectory检查是否在文件路径是否在所有的SecureApplicationDirectory,其实就是比较几个系统路径,例如C:\Program Files\
,C:\Windows\
,C:\Windows\System32
等等。
AiCheckSecureApplicationDirectory()函数第一个参数为文件路径,第二个参数是一个标志,如果flag_path为0表示不在可信路径中,如果flag_path 为0x4000,表示在Windows Denfer目录中,或者如果为0x6000,表示在Windows System32目录中。
接着调用AiIsEXESafeToAutoApprove函数,该函数的目的是为了检查文件是否有自动提升标志的程序,或者是白名单程序。在函数中,会调用 AipCheckFusion和 AipIsAutoApprovalEXE函数。 AipCheckFusion函数是通过检查文件中autoElevate标志是否为True来判断是否可以自动提升。而AipIsAutoApprovalEXE是检查白名单路径。以及检查某些特殊的mmc文件。
接着调用AiCheckLUA()函数,AiCheckLUA()函数适用于判断是否进行UAC弹窗,该函数会传入多个参数,包括:UAC级别,可信目录标记(我自创的说法),父进程Token句柄,桌面句柄,命令行等参数。
并调用AiLaunchConsentUI函数,在AiLaunchConsentUI函数中,调用AipGetElevationPromptType通过注册表行为,确定UAC的行为,最终,会通过 AiLaunchProcess函数,拉起一个 consent.exe进程,传入参数主要有Pid,一个有关进程参数路径的结构体,该结构体的初始化可以再 AipSetFixedParams函数中进行。除此以外,还将appinfo服务对应的进程的令牌复制给了consent.exe进程,这样consent.exe进程便拥有了高权限。
拉起consent.exe进程之后,该进程处于暂停状态,需要ResumeThread唤醒,之后便出现UAC弹窗,此时appinfo服务端便处于等待状态,知道用户选择是or否。如果返回值为0,则会将句柄传递出来。这就是是 AiCheckLUA最后一个参数,带回来的是高权限Token句柄。
最后拉起指定的程序,完成UAC。
0x03 ByPass UAC通用检测策略
根据前言,我们了解到ByPass UAC的基础是访问令牌的继承关系,主要得到了异常的访问令牌的进程关系,就可以认定这是一个ByPass UAC。
首先,我们需要知道一个进程是否以完整性级别(TokenElevationTypeFull)运行,即就是进程的权限是否被提升。如果进程权限都没有被提升,那么肯定就不存在ByPass UAC。
如下GetProcessEleation函数
是《windows核心编程》一书中介绍的能够返回提升类型和是否以管理员运行的辅助函数。
BOOL GetProcessEleation(DWORD dwPid ,TOKEN_ELEVATION_TYPE* pElevationType, BOOL* pIsadmin)
{
HANDLE hToken = NULL;
BOOL bResult = FALSE;
DWORD dwSize = 0;
if (!OpenProcessToken(OpenProcess(PROCESS_ALL_ACCESS, TRUE, dwPid), TOKEN_QUERY, &hToken))
{
//printf("[!]OpenProcessToken:%d \t\n", GetLastError());
return FALSE;
}
if (GetTokenInformation(hToken, TokenElevationType, pElevationType, sizeof(TokenElevationType), &dwSize)) {
BYTE adminSID[SECURITY_MAX_SID_SIZE];
dwSize = sizeof(adminSID);
if (FALSE == CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, &adminSID, &dwSize))
{
printf("[!] CreateWellKnownSid:%d \t\n", GetLastError());
return FALSE;
}
if (*pElevationType == TokenElevationTypeLimited) {
HANDLE hUnfilteredToken = NULL;
GetTokenInformation(hToken, TokenLinkedToken, (VOID*)&hUnfilteredToken, sizeof(HANDLE), &dwSize);
if (CheckTokenMembership(hUnfilteredToken, &adminSID, pIsadmin))
bResult = TRUE;
CloseHandle(hUnfilteredToken);
}
else
{
*pIsadmin = IsUserAnAdmin();
bResult = TRUE;
}
}
CloseHandle(hToken);
return bResult;
}
其中,我们关注的函数有两个,一个是OpenProcessToken
函数和GetTokenInformation
函数,OpenProcessToken
函数是为了获取进程的Token,而GetTokenInformation
函数获取进程Token信息,通过传入不同的TokenInformationClass信息获取不同的Token信息。
BOOL WINAPI GetTokenInformation(
_In_ HANDLE TokenHandle,
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
_Out_writes_bytes_to_opt_(TokenInformationLength, *ReturnLength) LPVOID TokenInformation,
_In_ DWORD TokenInformationLength,
_Out_ PDWORD ReturnLength
);
BOOL WINAPI OpenProcessToken(
_In_ HANDLE ProcessHandle,
_In_ DWORD DesiredAccess,
_Outptr_ PHANDLE TokenHandle
);
当传入的是TokenElevationType类型,则GetTokenInformation返回值主要有三个。
- TokenEvevationTypeDefault:进程以默认用户运行,或者UAC被禁用
- TokenEvevationTypeFull:进程权限被成功提升
- TokenEvevationTypeLimited:进程以受限权限运行
所以大致只需要通过上述两个API函数即可获取进程权限是否被提升。
然后检查当前进程是否具有微软的数字签名,因为部分微软自带的程序可以不经过UAC弹窗从而自动获取高完整性级别。
首先利用WinVerifyTrust函数(在VerifyEmbeddedSignature函数中)判断是否可信,接着获取Pe文件是否具有MicroSoft的签名信息。
获取文件签名信息的代码可以从https://github.com/konstantin89/windows-pe-signature-verifying处获取。然后比较签名人是否是Microsoft Windows
即可。
BOOL CheckExeSignature(DWORD dwPid)
{
BOOL bResult = FALSE;
//LPWSTR pwszSourceFile = NULL;
WCHAR pwszSourceFile[MAX_PATH] = { 0 };
TCHAR lpFilePath[MAX_PATH] = {0};
GetProcessPath(dwPid, lpFilePath);
MultiByteToWideChar(CP_ACP, 0, lpFilePath, strlen(lpFilePath) + 1, pwszSourceFile, sizeof(pwszSourceFile) / sizeof(pwszSourceFile[0]));
if (VerifyEmbeddedSignature(pwszSourceFile) == ERROR_SUCCESS)
{
bResult = CheckMiscrosoftSignature(pwszSourceFile);
}
return bResult;
}
接着判断父进程路径,如果父进程是属于白名单或者具有自动提升标志的,则异常,否则,在检查一下父进程的完整性级别。
通过对AppInfo的逆向分析发现,UAC会针对部分Pe文件进行自动的提权,而大部分ByPass UAC都会利用到这些所谓的白名单文件,所以我们可以针对性的检测这些白名单文件。
AppInfo!AipCheckFusion函数用来检测Pe文件中是否具有“autoElevate”标志。如果为True,则可以免弹窗进行UAC。直接把IDA中的反汇编结果Copy下来,改改就行了。
BOOL CheckFusion(TCHAR* lpFilePath)
{
BOOL bResult = FALSE;
HANDLE hFile = CreateFile(lpFilePath, GENERIC_READ, FILE_SHARE_DELETE | FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == NULL)
{
return FALSE;
}
ACTCTXW pActCtx;
WCHAR lpwFilePath[MAX_PATH] = { 0 };
MultiByteToWideChar(CP_ACP, 0, lpFilePath, strlen(lpFilePath) + 1, lpwFilePath, sizeof(lpwFilePath) / sizeof(lpwFilePath[0]));
memset(&pActCtx, 0, sizeof(ACTCTXW));
pActCtx.cbSize = 32;
pActCtx.lpSource = lpwFilePath;
pActCtx.lpResourceName = MAKEINTRESOURCEW(1);
pActCtx.dwFlags = 8;
HANDLE hMapping = NULL;
LPVOID lpStartAddress = NULL;
hMapping = CreateFileMappingW(hFile, 0, 0x1000002u, 0, 0, 0);
if (hMapping)
{
lpStartAddress = MapViewOfFile(hMapping, 4u, 0, 0, 0);
if (lpStartAddress)
{
pActCtx.dwFlags |= 0x80u;
pActCtx.hModule = (HMODULE)lpStartAddress;
}
}
else
{
hMapping = 0;
}
HANDLE hActCtx = CreateActCtxW(&pActCtx);
WCHAR pvBuffer[MAX_PATH] = {0};
if (hActCtx != INVALID_HANDLE_VALUE)
{
if (QueryActCtxSettingsW(0, hActCtx, 0, L"autoElevate", pvBuffer, 8u, 0) && (pvBuffer[0] == 't' || pvBuffer[0] == 'T'))
bResult = TRUE;
ReleaseActCtx(hActCtx);
}
if (lpStartAddress)
UnmapViewOfFile(lpStartAddress);
if (hMapping)
CloseHandle(hMapping);
return bResult;
}
在AppInfo!AipIsAutoApprovalEXE函数,其实就是比较文件是否是白名单程序。
bool __stdcall AipIsAutoApprovalEXE(const unsigned __int16 *Key)
{
bool result; // al
return _bsearch(Key, &g_lpAutoApproveEXEList, 0xCu, 4u, AipCompareEXE) != 0;
}
.data:6B0FB054 unsigned short const * * g_lpAutoApproveEXEList dd offset aCttunesvrExe
.data:6B0FB054 ; DATA XREF: AipIsAutoApprovalEXE(ushort const *)+E↑o
.data:6B0FB054 ; "cttunesvr.exe"
.data:6B0FB058 dd offset aInetmgrExe ; "inetmgr.exe"
.data:6B0FB05C dd offset aInfdefaultinst ; "infdefaultinstall.exe"
.data:6B0FB060 dd offset aMigsetupExe ; "migsetup.exe"
.data:6B0FB064 dd offset aMigwizExe ; "migwiz.exe"
.data:6B0FB068 dd offset aMmcExe ; "mmc.exe"
.data:6B0FB06C dd offset aOobeExe ; "oobe.exe"
.data:6B0FB070 dd offset aPkgmgrExe ; "pkgmgr.exe"
.data:6B0FB074 dd offset aProvisionshare ; "provisionshare.exe"
.data:6B0FB078 dd offset aProvisionstora ; "provisionstorage.exe"
.data:6B0FB07C dd offset aSpinstallExe ; "spinstall.exe"
.data:6B0FB080 dd offset aWinsatExe ; "winsat.exe"
除此以外呢,还需要检测父进程的完整性即可,只需要调用GetProcessEleation函数即可。
在R0层呢,只需要通过PsSetCreateProcessNotifyRoutineEx回调获取进程,及其父进程的Pid和ProcessName即可。
typedef struct _PROCESS_LONNK_READDATA
{
HANDLE hProcessId; //进程的PID
TCHAR szProcessName[MAX_PATH]; //进程名
HANDLE hParentId; //父进程PID
TCHAR szParentProcessName[MAX_PATH]; //父进程进程名
}PROCESS_LONNK_READDATA, *PPROCESS_LONNK_READDATA;
并通过事件,控制向R3发送数据。首先在DriverEntry中创建一个事件。
//创建事件
UNICODE_STRING EventName = { 0 };
ntStatus = RtlUnicodeStringInit(&EventName, IBINARY_EVENTNAME);
if (!NT_SUCCESS(ntStatus))
{
DbgPrint("RtlUnicodeStringInit EventName :%d", ntStatus);
return ntStatus;
}
PDEVICE_EXTEN pDeviceExten = (PDEVICE_EXTEN)pDeviceObject->DeviceExtension;
if (pDeviceExten == NULL)
{
DbgPrint("pDeviceExten Failed");
return ntStatus;
}
pDeviceExten->pkProcessEvent = IoCreateNotificationEvent(&EventName, &pDeviceExten->hProcessId);
KeClearEvent(pDeviceExten->pkProcessEvent);
当捕获到进程创建,获取完所需的数据之后,通过KeSetEvent将R3程序阻塞。就相当于R0驱动通知了一下R3的程序,有数据过去了,你准备接收一下,这样,R3程序就会接收R0传递的数据。
VOID pfnCreateProcessRoutine(
PEPROCESS Process,
HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo
)
{
NTSTATUS ntStatus = STATUS_SUCCESS;
if (CreateInfo != NULL)
{
PDEVICE_EXTEN pDeviceExten = (PDEVICE_EXTEN)g_pDeviceObject->DeviceExtension;
pDeviceExten->hProcessId = ProcessId;
pDeviceExten->hParentId = CreateInfo->ParentProcessId;
//获取父进程的进程名
PEPROCESS pParentEprocess = NULL;
ntStatus = PsLookupProcessByProcessId(pDeviceExten->hParentId, &pParentEprocess);
if (pParentEprocess != NULL)
{
PCHAR tmpParentProcessName = PsGetProcessImageFileName(pParentEprocess);
if (strlen(tmpParentProcessName) != 0)
{
RtlZeroMemory(pDeviceExten->szParentProcessName, MAX_PATH);
RtlCopyMemory(pDeviceExten->szParentProcessName, tmpParentProcessName, strlen(tmpParentProcessName));
//pDeviceExten->szParentProcessName[strlen(tmpParentProcessName) + 1] = '\0';
}
}
ANSI_STRING asProcessName;
if (RtlUnicodeStringToAnsiString(&asProcessName, CreateInfo->ImageFileName, TRUE) == STATUS_SUCCESS)
{
RtlZeroMemory(pDeviceExten->szProcessName, MAX_PATH);
RtlCopyMemory(pDeviceExten->szProcessName, asProcessName.Buffer,asProcessName.Length);
//pDeviceExten->szProcessName[asProcessName.Length + 1] = '\0';
}
KeSetEvent(pDeviceExten->pkProcessEvent, 0, FALSE);
KeResetEvent(pDeviceExten->pkProcessEvent);
}
}
0x05 参考文献
大家可以在https://www.bilibili.com/video/BV1Fr4y1Q7E8/观看ByPass UAC检测视频。
参考文章: