EreTIk's Box » Cтатьи, исходники » Раскрытие памяти (Memory Disclosure) ядра в современных ОС

habr: Раскрытие памяти (Memory Disclosure) ядра в современных ОС

Это копия моего перевода, опубликованного на habr: Раскрытие памяти (Memory Disclosure) ядра в современных ОС.


Disclaimer


Эта публикация является переводом части документа "Detecting Kernel Memory Disclosure with x86 Emulation and Taint Tracking" (Статья Project Zero) от Mateusz Jurczyk.

В переведенной части документа:

  • специфика языка программирования C (в рамках проблемы раскрытия памяти)
  • специфика работы ядер ОС Windows и Linux (в рамках проблемы раскрытия памяти)
  • значимость раскрытия памяти ядра и влияние на безопасность ОС
  • существующие методы и техники обнаружения и противодействия раскрытия памяти ядра

Хотя в документе пристально рассматриваются механизмы общения привилегированного ядра ОС с пользовательскими приложениями, суть проблемы можно обощить для любой передачи данных между различными доменами безопасности: гипервизор — гостевая машина, привилегированный системный сервис (демон) — GUI приложение, сетевые клиент — сервер и т.д.


Введение


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


Современные операционные системы, работающие на платформах x86/x86-64, многопоточны и используют клиент-серверную модель, в которой приложения пользовательского режима (клиенты) выполняются независимо друг от друга и вызывают ядро ОС (сервер) при намерении работать с ресурсом, управляемым системой. Механизм, используемый кодом режима пользователя (ring 3) для вызова заранее определенного набора функций ядра ОС (ring 0), называется системными вызовами (system calls) или (коротко) syscalls. Типичный системный вызов показан на рисунке 1:


Рисунок 1: Жизненный цикл системного вызова

Рисунок 1: Жизненный цикл системного вызова.


Очень важно избегать непреднамеренной утечки содержимого памяти ядра при взаимодействии с программами пользовательского режима. Существует значительный риск раскрытия чувствительных данных ядра ОС. Данные могут неявно передаваться в выходных параметрах безопасных (с остальных точек зрения) системных вызовов.


Раскрытие привилегированной системной памяти происходит, когда ядро ОС возвращает регион памяти, больший (избыточного размера), чем необходимо для хранения соответствующей информации (содержащейся внутри). Часто избыточные байты содержат данные, которые были заполнены в другом контексте, а затем память не была предварительно инициализирована, что предотвратило бы распространение информации в новые структуры данных.


Специфика языка программирования C


В этом разделе мы рассмотрим несколько аспектов языка C, которые наиболее важны для проблемы раскрытия памяти.


Неопределенное состояние неинициализированных переменных


Отдельные переменные простых типов (таких, как char или int), а также члены структур данных (массивы, структуры и объединения) остаются в неопределенном состоянии до первой инициализации (вне зависимости от размещения их на стеке или в куче). Соответствующие цитаты из спецификации C11 (ISO/IEC 9899:201x Committee Draft N1570, April 2011):


6.7.9 Initialization

...

10 If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate.

7.22.3.4 The malloc function

2 The malloc function allocates space for an object whose size is specified by size and whose value is indeterminate.

7.22.3.5 The realloc function

2 The realloc function deallocates the old object pointed to by ptr and returns a pointer to a new object that has the size specified by size. The contents of the new object shall be the same as that of the old object prior to deallocation, up to the lesser of the new and old sizes. Any bytes in the new object beyond the size of the old object have indeterminate values.


Наиболее применима к системному коду та часть, что относится к объектам, расположенным на стеке, поскольку ядро ОС обычно имеет интерфейсы динамического выделения с их собственной семантикой (не обязательно совместимые со стандартной библиотекой C, как будет описано далее).


Насколько нам известно, ни один из трех самых популярных компиляторов C для Windows и Linux (Microsoft C/C++ Compiler, gcc, LLVM) не создает код, который предварительно инициализирует неинициализированные программистом переменные на стеке в режиме Release-сборки (или ее эквивалента). Существуют опции компилятора, позволяющие помечать фреймы стека специальными байтами - маркерами (/RTCs в Microsoft Visual Studio, например) но они не используются в Release-сборках по соображениям производительности. В результате неинициализированные переменные на стеке наследуют старые значения соответствующих областей памяти.


Рассмотрим пример стандартной реализации вымышленного системного вызова Windows, который умножает входное целое на два и возвращает результат умножения (листинг 1). Очевидно, что в частном случае (InputValue == 0) переменная OutputValue остается неинициализированной и копируется обратно клиенту. Такая ошибка позволяет раскрывать четыре байта памяти стека ядра при каждом вызове.


NTSTATUS NTAPI NtMultiplyByTwo(DWORD InputValue, LPDWORD OutputPointer) {
    DWORD OutputValue;

    if (InputValue != 0) { 
        OutputValue = InputValue * 2;
    }

    *OutputPointer = OutputValue;
    return STATUS_SUCCESS;
}
                

Листинг 1: Раскрытие памяти через неинициализированную локальную переменную.


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


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


typedef struct _SYSCALL_OUTPUT {
    DWORD Sum; 
    DWORD Product; 
    DWORD Reserved;
} SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; 

NTSTATUS NTAPI NtArithOperations(
    DWORD InputValue,
    PSYSCALL_OUTPUT OutputPointer
) { 
    SYSCALL_OUTPUT OutputStruct;

    OutputStruct.Sum = InputValue + 2; 
    OutputStruct.Product = InputValue * 2; 

    RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); 

    return STATUS_SUCCESS;
}
                

Листинг 2: Раскрытие памяти через зарезервированное поле структуры.


Выравнивание структур и заполняющие (padding) байты


Инициализация всех полей выходной структуры является хорошим началом, чтобы избежать раскрытия памяти. Но и этого не достаточно, что бы гарантировать, что в низкоуровневом представлении отсутствуют неинициализированные байты. Давайте снова обратимся к спецификации C11:


6.5.3.4 The sizeof and Alignof operators

...

4 [...] When applied to an operand that has structure or union type, the result is the total number of bytes in such an object, including internal and trailing padding.

6.2.8 Alignment of objects

1 Complete object types have alignment requirements which place restrictions on the addresses at which objects of that type may be allocated. An alignment is an implementation-defined integer value representing the number of bytes between successive addresses at which a given object can be allocated. [...]

6.7.2.1 Structure and union specifiers

17 There may be unnamed padding at the end of a structure or union


То есть компиляторы языка С для x86(-64) архитектур применяют естественное выравнивание полей структур (имеющих примитивный тип): каждое такое поле выравнивается на N байт, где N - размер поля. Кроме того, целые структуры и объединения также выровнены, когда они объявляются в массиве, при этом выполняется требование к выравниванию вложенных полей. Чтобы обеспечить выравнивание, неявные байты заполнения (padding) вставляются в структуры там, где это необходимо. Хотя они не доступны напрямую в исходном коде, эти байты также наследуют старые значения из областей памяти и могут передавать информацию в пользовательский режим.


В примере из листинга 3 структура SYSCALL_OUTPUT возвращается обратно вызывающему коду. Она содержит 4-х и 8-ми байтовые поля, разделенные 4-мя байтами заполнения (padding), необходимыми для того, что бы адрес поля LargeSum стал кратен 8-ми. Несмотря на то, что оба поля правильно инициализированы, байты заполнения (padding) не заданы явно, что опять-таки приводит к раскрытию памяти стека ядра. Специфика расположение структуры в памяти показана на рисунке 2.


typedef struct _SYSCALL_OUTPUT {
    DWORD Sum; 
    QWORD LargeSum;
} SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; 

NTSTATUS NTAPI NtSmallSum(
    DWORD InputValue,
    PSYSCALL_OUTPUT OutputPointer
) { 
    SYSCALL_OUTPUT OutputStruct;

    OutputStruct.Sum = InputValue + 2;
    OutputStruct.LargeSum = 0;

    RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); 

    return STATUS_SUCCESS;
}
                

Листинг 3: Раскрытие памяти через выравнивание структуры.


Рисунок 2: Представление структуры в памяти с учетом выравнивания

Рисунок 2: Представление структуры в памяти с учетом выравнивания.


Утечки через выравнивания относительно распространены, так как достаточно много выходных параметров системных вызовов представлены структурами. Проблема особенно остро встает для 64-х битных платформ, где размер указателей, size_t и подобных типов увеличивается с 4 до 8 байт, что приводит к появлению заполнения (padding), необходимого для выравнивания полей таких структур.


Поскольку байты заполнения (padding) не могут быть адресованы в исходном коде, необходимо использовать memset или аналогичную функцию для сброса всей области памяти структуры до инициализации любого из ее полей и копирования ее в пользовательский режим, например:

    memset(&OutputStruct, 0, sizeof(OutputStruct));
                

Тем не менее, Seacord R. C. в своей книге "The CERT C Coding Standard, Second Edition: 98 Rules for Developing Safe, Reliable, and Secure Systems. Addison-Wesley Professional" 2014 утверждает, что это не идеальное решение, поскольку байты заполнения (padding) могут по-прежнему быть сбиты после вызова memset, например, как побочный эффект операций со смежными полями. Озабоченность может быть оправдана следующим утверждением в спецификации С:


6.2.6 Representations of types

6.2.6.1 General

...

6 When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values. [...]


Однако на практике ни один из компиляторов C, которые мы тестировали, не читал и не писал за пределами областей памяти явно объявленных полей. Похоже, что это мнение разделяют разработчики операционных системы, которые используют memset.


Объединения (Unions) и поля разного размера


Объединения - еще одна сложная конструкция языка C в контексте общения с менее привилегированным вызывающим кодом. Рассмотрим как спецификация C11 описывает представление объединений в памяти:


6.2.5 Types

...

20 Any number of derived types can be constructed from the object and function types, as follows: [...] A union type describes an overlapping nonempty set of member objects, each of which has an optionally specified name and possibly distinct type.

6.7.2.1 Structure and union specifiers

...

6 As discussed in 6.2.5, a structure is a type consisting of a sequence of members, whose storage is allocated in an ordered sequence, and a union is a type consisting of a sequence of members whose storage overlap.

...

16 The size of a union is sufficient to contain the largest of its members. The value of at most one of the members can be stored in a union object at any time.


Проблема состоит в том, что если объединение состоит из нескольких полей разного размера и явно инициализировано только одно полей меньшего размера, то оставшиеся байты, выделенные для размещения больших полей, остаются неинициализированными. Давайте рассмотрим пример гипотетического обработчика системных вызовов, представленный в листинге 4, вместе с распределением памяти объединения SYSCALL_OUTPUT, показанным на рисунке 3.


typedef union _SYSCALL_OUTPUT {
    DWORD Sum; 
    QWORD LargeSum;
} SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; 

NTSTATUS NTAPI NtSmallSum(
    DWORD InputValue,
    PSYSCALL_OUTPUT OutputPointer
) { 
    SYSCALL_OUTPUT OutputStruct;

    OutputStruct.Sum = InputValue + 2;

    RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); 

    return STATUS_SUCCESS;
}
                

Листинг 4: Раскрытие памяти через частичную инициализацию объединения.


Рисунок 3: Представление объединения в памяти с учетом выравнивания

Рисунок 3: Представление объединения в памяти с учетом выравнивания.


Получается, что общий размер объединения SYSCALL_OUTPUT составляет 8 байт (из-за размера большего поля LargeSum). Тем не менее, функция задает только значение меньшего поля, оставляя 4 конечных байта неинициализированными, что впоследствии и приводит к утечке их клиентскому приложению.


Безопасная реализация должна устанавливать только поле Sum в пользовательском адресном пространстве, а не копировать весь объект с потенциально неиспользуемыми областями памяти. Еще один рабочий вариант исправления - вызов функции memset для обнуления копии объединения в памяти ядра до установки любого из его полей и передачи его обратно в пользовательский режим.


Небезопасный sizeof


Как показано в двух предыдущих подразделах, использование оператора sizeof может напрямую или косвенно способствовать раскрытию памяти ядра, провоцируя копирование большего количества данных, чем ранее инициализировалось.


В языке C отсутствует аппарат, необходимый для безопасного переноса данных из ядра в пользовательское пространство - или, в более общем плане, между любыми разными контекстами безопасности. Язык не содержит метаданных времени исполнения, которые могут явно указать, какие байты были установлены в каждой структуре данных, которая используется для взаимодействия с ядром ОС. В результате ответственность ложиться на программиста, который должен сам определять какие части каждого объекта должны быть переданы вызывающему коду. Если делать правильно, то нужно написать отдельную функцию безопасного копирования для каждой структуры выходных данных, используемых в системных вызовах. Что в свою очередь приведет к раздуванию размера кода, ухудшению его читаемости и в целом будет утомительной и трудоемкой задачей.


С другой стороны, удобно и просто копировать всю область памяти ядра с помощью одного вызова memcpy и аргумента sizeof, и пусть клиент определит, какие части выходных данных будут использоваться. Получается, что это этот подход используется сегодня в Windows и Linux. А когда обнаруживается конкретный случай ѕСЃРєРѕР»СЊРєСѓ байты заполнения (padding) РЅРµ РјРѕРіСѓС‚ быть адресованы РІ РёСЃС…РѕРґРЅРѕРј РєРѕРґРµ, необходимо использовать memset или аналогичную функцию для СЃР±СЂРѕСЃР° всей области памяти структуры РґРѕ инициализации любого РёР· ее полей Рё копирования ее РІ пользовательский режим, например:

    memset(&OutputStruct, 0, sizeof(OutputStruct));
                

Тем не менее, Seacord R. C. в своей книге "The CERT C Coding Standard, Second Edition: 98 Rules for Developing Safe, Reliable, and Secure Systems. Addison-Wesley Professional" 2014 утверждает, что это не идеальное решение, поскольку байты заполнения (padding) могут по-прежнему быть сбиты после вызова memset, например, как побочный эффект операций со смежными полями. Озабоченность может быть оправдана следующим утверждением в спецификации С:


6.2.6 Representations of types

6.2.6.1 General

...

6 When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values. [...]


Однако на практике ни один из компиляторов C, которые мы тестировали, не читал и не писал за пределами областей памяти явно объявленных полей. Похоже, что это мнение разделяют разработчики операционных системы, которые используют memset.


Объединения (Unions) и поля разного размера


Объединения - еще одна сложная конструкция языка C в контексте общения с менее привилегированным вызывающим кодом. Рассмотрим как спецификация C11 описывает представление объединений в памяти:


6.2.5 Types

...

20 Any number of derived types can be constructed from the object and function types, as follows: [...] A union type describes an overlapping nonempty set of member objects, each of which has an optionally specified name and possibly distinct type.

6.7.2.1 Structure and union specifiers

...

6 As discussed in 6.2.5, a structure is a type consisting of a sequence of members, whose storage is allocated in an ordered sequence, and a union is a type consisting of a sequence of members whose storage overlap.

...

16 The size of a union is sufficient to contain the largest of its members. The value of at most one of the members can be stored in a union object at any time.


Проблема состоит в том, что если объединение состоит из нескольких полей разного размера и явно инициализировано только одно полей меньшего размера, то оставшиеся байты, выделенные для размещения больших полей, остаются неинициализированными. Давайте рассмотрим пример гипотетического обработчика системных вызовов, представленный в листинге 4, вместе с распределением памяти объединения SYSCALL_OUTPUT, показанным на рисунке 3.


typedef union _SYSCALL_OUTPUT {
    DWORD Sum; 
    QWORD LargeSum;
} SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; 

NTSTATUS NTAPI NtSmallSum(
    DWORD InputValue,
    PSYSCALL_OUTPUT OutputPointer
) { 
    SYSCALL_OUTPUT OutputStruct;

    OutputStruct.Sum = InputValue + 2;

    RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); 

    return STATUS_SUCCESS;
}
                

Листинг 4: Раскрытие памяти через частичную инициализацию объединения.


Рисунок 3: Представление объединения в памяти с учетом выравнивания

Рисунок 3: Представление объединения в памяти с учетом выравнивания.


Получается, что общий размер объединения SYSCALL_OUTPUT составляет 8 байт (из-за размера большего поля LargeSum). Тем не менее, функция задает только значение меньшего поля, оставляя 4 конечных байта неинициализированными, что впоследствии и приводит к утечке их клиентскому приложению.


Безопасная реализация должна устанавливать только поле Sum в пользовательском адресном пространстве, а не копировать весь объект с потенциально неиспользуемыми областями памяти. Еще один рабочий вариант исправления - вызов функции memset для обнуления копии объединения в памяти ядра до установки любого из его полей и передачи его обратно в пользовательский режим.


Небезопасный sizeof


Как показано в двух предыдущих подразделах, использование оператора sizeof может напрямую или косвенно способствовать раскрытию памяти ядра, провоцируя копирование большего количества данных, чем ранее инициализировалось.


В языке C отсутствует аппарат, необходимый для безопасного переноса данных из ядра в пользовательское пространство - или, в более общем плане, между любыми разными контекстами безопасности. Язык не содержит метаданных времени исполнения, которые могут явно указать, какие байты были установлены в каждой структуре данных, которая используется для взаимодействия с ядром ОС. В результате ответственность ложиться на программиста, который должен сам определять какие части каждого объекта должны быть переданы вызывающему коду. Если делать правильно, то нужно написать отдельную функцию безопасного копирования для каждой структуры выходных данных, используемых в системных вызовах. Что в свою очередь приведет к раздуванию размера кода, ухудшению его читаемости и в целом будет утомительной и трудоемкой задачей.


С другой стороны, удобно и просто копировать всю область памяти ядра с помощью одного вызова memcpy и аргумента sizeof, и пусть клиент определит, какие части выходных данных будут использоваться. Получается, что это этот подход используется сегодня в Windows и Linux. А когда обнаруживается конкретный случай ORD KernelBuffer = Allocate(OutputLength); KernelBuffer[0] = 0xdeadbeef; KernelBuffer[1] = 0xbadc0ffe; KernelBuffer[2] = 0xcafed00d; RtlCopyMemory(OutputPointer, KernelBuffer, OutputLength); Free(KernelBuffer); return STATUS_SUCCESS; }

Листинг 6: Раскрытие памяти через выходной буфер произвольного размера.


Целью системного вызова является предоставление вызывающему коду трех специальных 32-х битных значений, занимающих в общей сложности 12 байт. Хотя проверка правильности размера буфера в самом начале функции верна, использование аргумента OutputLength должно на этом и заканчиваться. Зная, что выходной буфер имеет достаточный размер, чтобы сохранить результат, ядро может выделить 12 байт памяти, заполнить ее и скопировать содержимое обратно в предоставленный буфер пользовательского режима. А вместо этого системный вызов выделяет блок пула (причем с контролируемой пользователем длиной) и копирует выделенную память целиком в пользовательское пространство. Получается, что все байты, кроме первых 12-ти, не инициализируются и ошибочно раскрываются пользователю, как показано на рисунке 5.


Рисунок 5: Память буфера произвольного размера

Рисунок 5: Память буфера произвольного размера.


Схема, обсуждаемая в этом разделе, особенно характерна для Windows. Подобная ошибка может предоставить злоумышленнику чрезвычайно полезный примитив для раскрытия памяти:

  • Оптимизация, часто используемая в системных вызовах Windows, заключается в использовании буферов на основе стека для небольших размеров и выделение памяти из пулов для более крупных. В сочетании с подобной утечкой это может облегчить раскрытие как стека ядра, так и пула с помощью одной уязвимости.
  • Возможность контролировать размер раскрываемых данных из пула дает злоумышленнику возможность управлять типом конфиденциальной информации. Это становится возможным, учитывая современные механизмы распределения памяти, имеющие тенденцию к кэшированию областей памяти для последующих запросов выделения памяти с тем же размером буфера. Что дает возможность читать специфические данные, недавно освобожденные другим (в общем случае - не связанным) компонентом ядра.

Таким образом, это один из самых опасных типов раскрытия информации. Безопасная реализация должна отслеживать количество байтов, записанных во временное хранилище ядра, и передавая только этот объем данных клиенту.


Факторы, способствующие появлению ошибок раскрытия памяти


Здесь мы расскажем о том, какие существуют проблемы с выявлением ошибок раскрытия памяти в ходе разработки. Что в течение многих лет способствовало отсутствию признания проблемы, что в свою очередь привело к нагромождению в ядре Windows десятков ошибок раскрытия памяти.


Отсутствие видимых последствий

Как правило, ошибки раскрытия информации более сложны для обнаружения, чем нарушения памяти. Последние обычно проявляются в виде сбоев программного обеспечения, особенно в сочетании со специализированными механизмами: AddressSanitizer, PageHeap или Special Pool. Раскрытие информации, напротив, не вызывает каких-либо наблюдаемых проблем и не может быть легко распознано программными решениями. Конфиденциальность любой информации очень субъективна и требуется оценка человеком, что бы определить легитимность предоставленной информации в контексте безопасности. С другой стороны, когда гигабайты траффика передаются между ядром и пользовательским пространством, из памяти программ в файлы на диск, с одной машины на другую через сеть и между контекстами безопасности, не представляется возможным вручную просмотреть весь этот трафик, что бы найти случаи ошибок раскрытия памяти. В результате многие ошибки могут активно *сливать* данных в течение многих лет, прежде чем они будут замечены (если вообще будут замечены).


Тот факт, что уведомление о подобных утечках не дает ощутимой обратной связи от разработчиков ядра, также означает, что они не учатся на своих ошибках. И что они будут повторять одни и те же небезопасные шаблоны кода в нескольких местах, так как они не знают об этом классе ошибок и не предпринимают действий к предотвращению возникновение подобных проблем.


Утечки, скрытые за системным API

Типичные клиентские приложения реализуют свою функциональность с использованием высокоуровневого системного API, особенно в Windows (Win32/User32 API). API часто отвечает за преобразование входных параметров во внутренние структуры, принятые системными вызовами, и аналогичным образом преобразует выходные данные системных вызовов в формат, понятный вызывающему коду клиентского приложения. В этом случае содержимое памяти, раскрываемое ядром, может быть отброшено системной библиотекой пользовательского режима и, следовательно, никогда не будет передана обратно программе. Это еще больше уменьшает видимость утечек и снижает вероятность их обнаружения при разработке *обычного* программного обеспечения.


Значимость и влияние на безопасность системы


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


В общем, класс подобных ошибок кажется вполне полезным, как одно из звеньев в длинной цепочке локального повышения привилегий. В настоящее время основная техника, которая основана на неразглашении данных это KASLR (Kernel Address Space Layout Randomization), а расположение различных объектов в адресном пространстве ядра является одним из самых распространенных типов утечек данных. Реальный пример: эксплойт ядра Windows, обнаруженный в дампе Hacking Team в июле 2015 года (Juan Vazquez. Revisiting an Info Leak) в котором использовалось раскрытие памяти пула для определения (derandomize) базового адреса загрузки драйвера win32k.sys, впоследствии используемого для применения в другой уязвимости. Кстати, этот же дефект был обнаружен в тоже время Matt Tait'ом из Google Project Zero (Kernel-mode ASLR leak via uninitialized memory returned to usermode by NtGdiGetTextMetrics) и исправлен в бюллетене MS15-080 (CVE-2015-2433).


Стеки

Различие между стеком и динамической памятью (пулы/кучи) заключается в том, что в стеках обычно хранятся данные, связанные непосредственно с потоком управления (control flow), такие как: адреса модулей ядра, адреса динамической памяти, и секретные значения, установленные механизмами безопасности, такими как StackGuard на Linux и /GS на Windows. Это значимые фрагменты информации, которые могут быть немедленно применены потенциальным злоумышленником в совокупности с эксплоитами порчи памяти. Тем не менее разнообразие данных на стеке ограничено, что позволяет предположить, что эксплуатация утечки стека не представляет большой ценности в виде автономной уязвимости.


Динамическая память (пулы/кучи)

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


Другие существующие исследования

КДПВ


Microsoft Windows


Обнаружение

До 2015 года в публичных источниках практически не обсуждалась проблема раскрытия памяти в Windows. В июле 2015 года Matt Tait сообщил о проблеме раскрытия неинициализированной памяти пула чЅСѓР»РµРЅРёСЏ РєРѕРїРёРё объединения Р Р† РїР°РС