Чтение памяти чужого процесса через комбинирование памяти в Windows 10

Windows page combining

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

Начну с того, что в Microsoft были уведомлены об этой проблеме ещё года полтора назад. В ответ мне сообщили, что уязвимость в большей степени уже закрыта, и что я могу опубликовать свои исследования.

Итак, приступим. В Windows 8.1 и в Windows 10 в какой-то момент времени появилась такая фича, как комбинирование памяти (memory combining или page combining, хорошо описана в книге Windows Internals, 7 издание, 1 часть). Суть её достаточно проста: операционная система раз в 15 минут ищет в физической памяти страницы с одинаковым содержимым и объединяет их в одну с целью экономии оперативной памяти. Те процессы, которые владели одинаковыми страницами, получают ссылки на новую общую страницу с атрибутом "только для чтения" и "копирования при записи" (read-only и copy-on-write). Если какой-то из процессов изменяет свою страницу, система при возникновении соответствующего copy-on-write исключения её копирует и снова размещает в физической памяти, а процесс снова получает индивидуальную копию этой страницы.

На базе этой функциональности, которая некоторое время была включена по умолчанию, были разработаны совершенно потрясающие способы атаки на систему. Советую прочитать соответствующий материал "Dedup Est Machina". В бумаге описан способ получения контроля над Microsoft Edge, а также чтения приватного содержимое памяти nginx. Обе атаки удалённые! Хоть уже и неактуальный, материал очень интересный и достаточно просто читается, несмотря на академический стиль, рекомендую.

Если быть кратким, суть атаки сводилась к следующему. После того, как система объединила одинаковые физические страницы в одну (а это происходило раз в 15 минут), и какой-то из процессов, владеющих одной из этих страниц, производил запись в неё, это занимало заметно большее время, чем обычно (потому что содержимое страницы сначала копируется из общей объединённой в приватную для этого процесса). Это время можно замерить и таким хитрым образом определить, использует ли ещё кто-то в системе такие же данные из этой страницы, что и наш процесс. Упрощённо, если в системе есть чья-то страница памяти, которая хранит интересующий нас пароль "123", мы можем создать большое количество страниц с содержимым вида "000", "001", "002", ..., "122", "123", "124", ..., "999". Далее мы через 15 минут, когда система выполнит комбинирование, попытаемся изменить содержимое каждой из этих страниц и, так как наша страница "123" была объединена с чужой страницей (их содержимое одинаковое), наш процесс увидит, что запись в неё занимает заметно большее время. Исходя из этого наш процесс сможет сделать вывод, что содержимое "123" есть в системе у кого-то ещё, а значит, это и есть интересующий нас пароль. Получается такой своеобразный брутфорс. Методика, описанная в бумаге по ссылке выше, конечно, значительно сложнее, там используется целая комбинация техник и атак для реализации полноценной утечки данных.

К счастью, Microsoft отключила комбинирование памяти, и проблема была решена. Осталось включенным комбинирование страниц, содержащих только нулевые байты, которое может выполняться в некоторых случаях, а это в целом безопасно. Только вот сам функционал из ядра не удалили и, более того, администратор может легко его включить. Для этого ранее можно было использовать недокументированную функцию NtSetSystemInformation (код есть в GitHub Windows Internals), но используемый класс SystemCombinePhysicalMemoryInformation был отключён или удалён из новых версий Windows 10 (или его индекс просто изменился).

Однако, есть даже более простой и документированный способ активировать комбинирование памяти: это команда PowerShell Enable-MMAgent -PageCombining, запущенная от имени администратора. Отключить комбинирование можно, соответственно, командой Disable-MMAgent -PageCombining, а получить текущее состояние настройки - командой Get-MMAgent. Тут есть небольшая пикантная особенность: администратор сервера может включить эту настройку с целью оптимизировать потребление памяти и, таким образом, открыть дыру в своей системе. На серверах часто работает много схожих виртуальных маших с более-менее одинаковым содержимым памяти, и такая функция вполне может иметь смысл с точки зрения администратора. В Microsoft, в свою очередь, нигде не упоминают о том, что включение этой настройки чревато большими проблемами. Вот, например, страница документации на Enable-MMAgent. Там ни слова нет о подводных камнях:

-PageCombining
Indicates that the cmdlet enables page combining.
If you do not specify this parameter, page combining remains in its current state, either enabled or disabled.

А вот ещё одна страница документации с сайта Microsoft, где memory combining упоминается в позитивном ключе:

Enabling page combining may reduce memory usage on servers which have a lot of private, pageable pages with identical contents. For example, servers running multiple instances of the same memory-intensive app, or a single app that works with highly repetitive data, might be good candidates to try page combining. The downside of enabling page combining is increased CPU usage.

Here are some examples of server roles where page combining is unlikely to give much benefit:

  • File servers (most of the memory is consumed by file pages which are not private and therefore not combinable)
  • Microsoft SQL Servers that are configured to use AWE or large pages (most of the memory is private but non-pageable)

Page combining is disabled by default but can be enabled by using the Enable-MMAgent Windows PowerShell cmdlet. Page combining was added in Windows Server 2012.

Тут нет ни слова о том, что page combining открывает дыры в системе. Единственный упомянутый недостаток - это увеличение загрузки процессора. Админ вполне может попробовать эту настройку у себя на серверах. Таким образом, документация Microsoft по этому вопросу оставляет желать лучшего. Я бы предложил им или вообще удалить эти разделы, или явно указать, что эта настройка устарела и всегда должна быть выключена.

Вы можете включить Page Combining также напрямую через WMI, выполнив следующую команду (тоже с правами администратора):

Сам метод Enable с параметром PageCombining выполняется в контексте одного из процессов svchost (служба SysMain/Superfetch) и меняет некоторые значения в ветке реестра HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Superfetch (возможно, также реконфигурирует менеджер памяти Windows).

Но вернёмся к бумаге, которую я упомянул раньше. Она описывает два сценария с возможностью удалённой атаки на систему (через JavaScript в Edge или через внешние HTTP-запросы в nginx), но не описывает способ получения информации из чужого процесса в системе. Обе атаки рассчитаны на удаленного атакующего, который имеет возможность менять память в атакуемом процессе (посредством выполнения JavaScript в Edge или отсылкой HTTP-запросов в nginx). Предположим, что наш процесс с ограниченными правами уже запущен в системе. Рассмотрим, как может быть организован сторонний канал передачи данных от чужого процесса к нашему. Я обойдусь без замеров времени записи в страницу памяти, предложив альтернативный способ. Также, я не буду читать память реальных приложений, а напишу пару подопытных искусственных программ и на их примере продемонстрирую, как это можно реализовать.

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

Тут пока всё понятно: мы получаем из командной строки список чисел от 0 до 9999 включительно, а потом неким образом будем держать их в страницах оперативной памяти. Далее нас будет интересовать код функции keep_numbers_in_memory_pages. Добавим его в объявленный выше анонимный неймспейс:

Сначала мы создаём список (std::vector) адресов страниц памяти, которые мы подготовим для наших чисел. Каждое отдельно взятое число будет размещено в своей собственной странице памяти, и их мы выделяем в цикле с помощью VirtualAlloc (внутри функции prepare_page). Мы запрашиваем у системы 1 байт памяти, но Windows автоматически вернёт нам кусок памяти размером с физическую страницу (обычно 4096 байтов), так как мы не указали адрес (передали nullptr). Далее в каждую страницу памяти мы записываем строку memory_pattern, а после неё - преобразованное в строку значение числа, которое мы обрабатываем. После этого в бесконечном цикле мы обращаемся к каждой выделенной странице памяти, чтобы система не закешировала их на диск и не сжала, а оставила в физической памяти как есть. memory_pattern выбран некоторым случайным образом, чтобы ни один другой процесс в системе не имел таких же страниц памяти и не мешал нам с нашим экспериментом.

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

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

Теперь, если не передать ни одного аргумента, процесс запустится в режиме чтения чужой памяти. Иначе же будет выполняться код, написанный выше. Обе программы можно будет запустить из одного бинарного файла, это удобнее, но не принципиально делать именно так. Теперь всё внимание сосредоточено на функции try_read_numbers_from_another_process, перейдём к её разработке. Сначала нам потребуется вспомогательная структура memory_page_info, в которой мы будем хранить информацию о странице памяти:

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

Перейдём к реализации функции try_read_numbers_from_another_process. Поместим её всё в тот же файл в наше анонимное пространство имён, сразу после структуры memory_page_info:

Сначала мы создаём страницы памяти для каждого из возможных секретных чисел, а затем, аналогично первой программе-жертве, в цикле держим их в памяти, чтобы система их не кешировала. В этом же цикле мы регулярно проверяем атрибуты каждой из страниц. Если в какой-то момент времени мы видим, что страница была не общей, и вдруг стала общей (shared), это может означать только одно: Windows произвела комбинирование этой страницы памяти с какой-то страницей другого процесса. А это говорит о том, что страница памяти с точно таким же содержимым, как и у нас, есть у другого процесса. Так мы и узнаём, какие числа были переданы в первый процесс-жертву! Нам не нужно осуществлять запись в страницу и замерять время, система сама лично через API QueryWorkingSetEx сообщает, когда она прозвела комбинирование страниц! Также мы можем узнать, когда количество общих копий страницы изменилось. Количество хранится в поле ShareCount, но оно трёхбитовое, принимает значение от 0 до 7 включительно, поэтому его область применения может быть ограничена, если одинаковых копий страницы в памяти больше семи. Советую поподробнее изучить объединение (union) PSAPI_WORKING_SET_EX_BLOCK, чтобы понять, что ещё интересного возвращает QueryWorkingSetEx.

Сейчас такой способ всё ещё работает, но комбинирование памяти Windows производит только в пределах одного пользователя. Если у вас запущено два процесса от разных пользователей, первый не сможет узнать секреты второго, так как их страницы не будут комбинироваться. Соответственно, ограниченный в правах процесс не сможет прочитать данные привилегированного. В тот момент, когда я с этой проблемой обратился в Microsoft, всё ещё комбинировались страницы памяти elevated-процессов и процессов с фильтрованным токеном в пределах одного пользователя. Т.е., если из-под привелигированного пользователя запустить один процесс просто по двойному клику, а второй - явно от имени администратора с подтверждением через UAC, то первый мог получить содержимое страниц памяти второго. В свежих версиях Windows 10 это исправлено, хотя на момент обращения мне сообщили, что не планируют ничего с этим делать.

Итак, осталась возможность читать память чужих процессов, запущенных под одним пользователем с одинаковыми правами (даже ограниченными).

Давайте скомпилируем программу и проверим её в действии на одной из свежих публично доступных на момент написания поста версий Windows 10 (2004 19041.329). Я для этого создал специального отдельного пользователя с ограниченными правами, и запускать тестовую программу я буду следующим образом:

Page combining spy process

Теперь запустим другой экземпляр нашей тестовой программы (жертву) от того же ограниченного пользователя. Но в него мы теперь передадим несколько чисел от 0 до 9999 включительно:

Page combining victim process

Если бы в Microsoft не отключили автоматическое комбинирование памяти по умолчанию, то уязвимость проявилась бы сама по себе в ближайшие 15 минут. Мы же форсируем комбинирование памяти, выполнив описанную выше команду Enable-MMAgent -PageCombining в привилегированной консоли PowerShell:

Enable page combining using PowerShell

Вот, что сразу после этого мы увидим в первой консоли процесса-шпиона:

Page combining spy - values leaked

Набор секретных чисел процесса-жертвы стал известен процессу-шпиону!

То же самое произошло бы, если бы администратор ранее включил комбинирование памяти. Нам не пришлось бы выполнять Enable-MMAgent -PageCombining, а утечка произошла бы сама по себе в ближайшие 15 минут. Теперь, так как страницы обоих процессов связаны, если закрыть процесс-жертву, вот что будет выведено процессом-шпионом:

Page combining spy - numbers leaked again

Процесс-шпион определил, когда процесс-жертва вышел и освободил свои страницы памяти, после чего они перестали иметь атрибут shared. Подобная утечка не требует использования каких-то особенных API и поэтому также не будет замечена ни одним антивирусом.

Скачать полный исходный код примера и его скомпилированный вариант (dxdxdx - пароль к архиву).

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

P.S. В Linux тоже есть такая штука, называется kernel same page merging (KSM). В подробности я не вникал, но работает она по-другому и, кажется, её разработчики тоже уже осведомлены о потенциальных проблемах.

Чтение памяти чужого процесса через комбинирование памяти в Windows 10: 1 комментарий

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *