UAC 原理与检测

HaCky 2022-08-30 09:58:00

0x00 前言

任何需要管理员访问令牌的程序都必须征得同意(即UAC弹窗),但是存在一个意外,即父进程和子进程之间存在关系,即子进程从父进程中继承访问令牌,这是UAC 绕过的基础。本文将选择UACME中的3种不同的ByPass UAC的例子,为各位讲解ByPass UAC的原理。然后分析UAC的基本基本原理,继而寻找ByPass UAC的通用检测策略,本文行文仓促,如有错误,请各位积极指正。

0x01 常见的ByPassUAC 原理

UACME项目中,主要的ByPass UAC方法主要有Shell APIDll 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。
mark

接着调用ucmxDisemer()拉起一个pkgmgr.exe进程,从此完成ByPassUAC
mark
mark

为什么只需要短短两步就可以完成ByPass UAC?我们使用ProcessMon监控当执行Akagi.exe 23 calc.exe之后,发生了什么。对ProcessMon做出如下配置。
mark

可以清楚的看到,当akagi.exe拉起pkgmgr.exe之后,pkgmgr.exe会创建dism.exe进程。但是dism.exe又和有什么关系呢?我们试着带着上帝视野的角度看看当时Leo Davidson是怎么找到这个ByPass UAC方法的。
mark

首先我们需要把握两个关键点,第一这是Dll HiJack方法,所以在这个方法中,关于加载Dll的行为一定要密切关注,第二,其所利用到的组件是DismCore.dll。综上两点,我们可到一个关键点,即dism.exe进程加载DismCore.dll的时候发生了ByPass UAC利用。并且从ProcessMon的细节中,也验证了我们的想法。
mark

既然要加载dll,抛去那些复杂的注入的方式,常见的加载dll的函数无非就是LoadLibrary相关的函数,我们可以通过ProcessMon提供的Stack功能查看调用情况。很显然,dism.exe使用了LoadLibraryW加载DismCore.dll。
mark

分析dism.exe,显然,可以将目标定位到CDismWrapper::FindAndLoadDism函数,在CDismWrapper::FindAndLoadDism()中,会调用CreateObjectFromDLL(),然后调用LoadLibraryW加载DismCore.dll,我们只需要覆盖原始的DismCore.dll,那么dism.exe就会加载我们创建的DismCore.dll,即可完成ByPassUAC。
mark
mark

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之后做好清理即可。
mark
mark
mark

首先,将ProcessMon设置成如下状态,可以看到akagi.exe首先会拉起sdclt.exe,这是已知的,然后添加参数“/name Microsoft.BackupAndRestoreCenter”,拉起“C:\Windows\System32\control.exe”进程。
mark
mark

接着,将ProcessMon设置成如下配置,我们看一下直接创建control.exe进程以及通过ByPass UAC,打开注册表HKCU\Software\Classes\Folder\shell\open\command的差异。如图红色框内,打开注册表的结果是NAME NOT FOUND,但是在绿色方框内,则显示REPARSE。然而我们也注意到在control.exe访问之前,Akagi.exe以及事先设置好了值进去。
mark
mark

0x02 UAC原理和逆向分析

了解过UAC工作原理的师傅都知道,当调用类似于CreateProcess的进程创建函数之后,通过RPC过程调用appinfo文件中的RAiLaunchAdminProcess函数,appinfo主要用于UAC权限验证,如果符合特定的条件,则自动将进程的完整性级别提升到High,否则的话,则会通过UAC弹窗请用户选择。

说到完整性级别,windows操作系统的进程完整性级别一共有4个,分别是Low(低完整性级别),Medium(中完整性级别),High(高完整性级别),System(系统)。在标准模式下,程序一般都运行在Medum级别,等到UAC提升之后,才会提升到High完整性级别。

在RAiLaunchAdminProcess()中,首先,会调用CheckElevationEnabled检查是否启用了UAC。
mark

然后调用AiCheckSecureApplicationDirectory检查是否在文件路径是否在所有的SecureApplicationDirectory,其实就是比较几个系统路径,例如C:\Program Files\,C:\Windows\,C:\Windows\System32等等。
AiCheckSecureApplicationDirectory()函数第一个参数为文件路径,第二个参数是一个标志,如果flag_path为0表示不在可信路径中,如果flag_path 为0x4000,表示在Windows Denfer目录中,或者如果为0x6000,表示在Windows System32目录中。
mark
mark
mark

接着调用AiIsEXESafeToAutoApprove函数,该函数的目的是为了检查文件是否有自动提升标志的程序,或者是白名单程序。在函数中,会调用 AipCheckFusion和 AipIsAutoApprovalEXE函数。 AipCheckFusion函数是通过检查文件中autoElevate标志是否为True来判断是否可以自动提升。而AipIsAutoApprovalEXE是检查白名单路径。以及检查某些特殊的mmc文件。
mark
mark
mark

接着调用AiCheckLUA()函数,AiCheckLUA()函数适用于判断是否进行UAC弹窗,该函数会传入多个参数,包括:UAC级别,可信目录标记(我自创的说法),父进程Token句柄,桌面句柄,命令行等参数。
mark

并调用AiLaunchConsentUI函数,在AiLaunchConsentUI函数中,调用AipGetElevationPromptType通过注册表行为,确定UAC的行为,最终,会通过 AiLaunchProcess函数,拉起一个 consent.exe进程,传入参数主要有Pid,一个有关进程参数路径的结构体,该结构体的初始化可以再 AipSetFixedParams函数中进行。除此以外,还将appinfo服务对应的进程的令牌复制给了consent.exe进程,这样consent.exe进程便拥有了高权限。
mark
mark
mark

拉起consent.exe进程之后,该进程处于暂停状态,需要ResumeThread唤醒,之后便出现UAC弹窗,此时appinfo服务端便处于等待状态,知道用户选择是or否。如果返回值为0,则会将句柄传递出来。这就是是 AiCheckLUA最后一个参数,带回来的是高权限Token句柄。
mark

最后拉起指定的程序,完成UAC。
mark

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检测视频。

参考文章:

评论

NanHu 2022-08-31 00:06:13

图片没了哇

secdragon 2022-09-01 09:49:10

@Kre1se 恢复了

H

HaCky

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

IoT安全 文章:29 篇
MongoDB安全 文章:3 篇
渗透测试 文章:154 篇
Java安全 文章:34 篇
漏洞分析 文章:212 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录