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

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



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

  • Инициализация контроллера дисплея (установка видеорежима, управление портами GPU, формирование одного/нескольких независимых изображений, ...);
  • Управление адресуемой памятью (очереди команд, линейная/тайловая адресации, выделение поверхностей, таблицы трансляции адресов, расширение PCI апертуры, ...);
  • 2D акселерация (курсор, аппаратные слои, alpha/chroma ключи, ROP, примитивы, ...);
  • 3D акселерация (OpenGL, OpenGL ES / EGL, OpenVG / EGL, OpenCL, Open*);
  • Видео декодирование / воспроизведение аудио / вычитывание EDID / сжатие буфера кадров, ...

Применяемые на каждом этапе подходы к решению поставленных задач уже достаточно давно сведены к устоявшимся практикам. Этим как раз и объясняется воспроизводимость обозначенной проблемы на устройствах различных производителей. Забегая вперед скажу, что можно получить подобный эффект также и на контроллерах Intel. Автор багрепорта совершенно точно определил в рамках решения какой задачи возникает эффект — управление адресуемой памятью.



Управление памятью


Основной сущностью, которой оперирует драйвер на данном этапе — это поверхность. Поверхностью в общем случае называется непрерывный фрагмент видео или оперативной памяти, используемый для формирования изображения некоторым приложением. Для контроллеров не имеющих собственной памяти, выделенный из ОЗУ ресурс может стать адресуемым через таблицу трансляции адресов (Graphics Translation Table, GTT). В противном случае изображение может быть отображено только при копировании поверхности в видеопамять либо средствами DMA-контроллера, если такой имеется, либо за счет ресурсов CPU.

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

Пример тайловой адресации в видеопамяти из исходной статьи
image

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

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

Исходя из вышесказанного можно сделать вывод о том, что располагая информацией об общем объеме доступной памяти контроллера и требуемых характеристиках поверхности, определение офсета для всех контроллеров может решаться единообразно. На практике так оно и есть (руководствуясь опытом работы с различными unix-подобными ОС): ОС предоставляет системный сервис/библиотеку, которая хранит список уже использованных блоков памяти и позволяет оперативно вычислить первый доступный офсет для логического резервирования в рамках этой библиотеки. При этом, располагая информацией от драйвера о механизме доступа к блоку памяти, ОС в общем случае допускает формирование для прикладного ПО по одним и тем же физическим адресам разделяемых/пересекающихся поверхностей.

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

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




Возвращаясь к исходной проблеме


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

На самом деле тест, написанный для публикации багрепорта избыточен. В общем случае (когда видео-память укладывается в PCI/GPU апертуру) для unix-подобных систем не требуется никаких прикладных API. Достаточно обратиться к /dev/mem по известному из вывода утилиты «lspci» офсету.

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

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

Надеюсь, что заметка была интересна.

Комментарии (15)


  1. Maximus43
    12.01.2016 16:09

    Как я понял, проблема в том, что драйвер (или ОС) выводит содержимое выделенной области памяти на экран пользователя, отображая старое содержимое этой области. Если же отображение начиналось по команде приложения, когда загрузка данных в эту область уже произведена, то проблемы бы не возникло. Правильно я понял?


    1. Egor3f
      12.01.2016 16:42

      Тут дело даже не в показе «не той картинки» юзеру, а в возможности бесприпятственного получения содержимого области другим приложением. То есть, я могу написать с виду безобидную программу, которая будет, например, получая доступ к видеопамяти, парсить получившуюся картинку и отправлять её злоумышленникам. А там уже могут быть ценные данные.


      1. tbl
        12.01.2016 17:03
        +3

        можно вообще на сайте где-нить в iframe крутить яваскрипт, который, используя webgl, будет слизывать содержимое VRAM пользователя и отсылать картинки на сервер.


        1. FoxF
          12.01.2016 17:30
          +1

          Где-то уже можно посмотреть пример реализации?


          1. tbl
            12.01.2016 17:48

            В первоначальной статье была ссылка на баг. Там есть в аттачменте к исходному сообщению «chrome_vram_leak.zip». Написано на Go. Думаю, для тех, кто знает javascript, проблем переложить в браузерную версию не будет.


    1. anger32
      12.01.2016 18:45
      +1

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

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


  1. Barefoot
    12.01.2016 18:15

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


  1. KOLANICH
    12.01.2016 19:30
    +1

    ОС должна следить за необходимостью того или иного приложения прибрать за собой (это либо некое маркирование защищаемых поверхностей, либо избыточное зануление любых поверхностей как в ОЗУ, так и в видеопамяти);

    Именно так. Страницы памяти при передаче их другому приложению зануляет именно ОС, потому что приложение не имеет возможности их занулить. Почему для памяти в GPU должно быть иначе?


    1. iSage
      12.01.2016 20:12

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


    1. anger32
      12.01.2016 20:59

      Приложение имеет возможность прибрать за собой как в случае видеопамяти, так и в случае оперативной. Для первого варианта как справедливо отметил iSage, достаточно одной команды, с оперативной памятью чуть сложнее и несколько более затратно, но тем не менее осуществимо.

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


  1. RPG
    12.01.2016 19:50
    +1

    В современных операционных системах принято занулять память перед тем как отдать её приложению. Это это очень хорошо, так как при выполнении malloc «мусор» в буфере окажется мусором освобождённой памяти текущего приложения, а не какого-то другого. Зануление при освобождении, как правило, не происходит (в угоду производительности) и используется на параноидальных сборках ядра Linux с патчами PAX/Grsecurity.

    Я на скорую руку переписал тест автора бага на Си: github.com/scriptum/vmem_test (go активно отказывался компилироваться). Проверил на двух драйверах под Linux: nouveau и nvidia 358.16. В драйвере nouveau эффект присутствует (да какой — данные сохраняются даже после перезагрузки), в случае же с последним актуальным проприетарным драйвером nvidia 358.16 — возвращается чёрный экран.

    Достаточно обратиться к /dev/mem по известному из вывода утилиты «lspci» офсету.

    Это очень плохая «фича». Я очень рад, что моя система запрещает обращаться к /dev/mem не только кому попало, но и руту. Так, на всякий случай.

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


    1. tbl
      12.01.2016 20:26

      В оригинальном коде на Go ещё затекстуренный полигон рисуется перед чтением vram. В комментах указано, что драйвера для оптимизации выкидывают вызовы если этого не делать. Видимо это ваш случай с проприетарными дровами.


      1. RPG
        12.01.2016 20:46
        +1

        Если я правильно понял, то эта оптимизация имеет отношение к блоку clearMemory, но не readpixels, к тому же автор оставил блок clearMemory опциональным. Мне же было интересно сохранить весь дамп VRAM для последующего изучения. Есть вероятность что драйвер «оптимизировал» также и чтение из только что аллоцированного пустого блока. Наверное, стоит попробовать рисовать небольшой прямоугольник поверх этого буфера, чтобы драйвер пометил его как «грязный». Попробую ещё потестить с разными драйверами.


      1. RPG
        13.01.2016 15:58

        Проверил на разных машинах. В драйвере 358 пофикшено, в 352 ещё не пофикшено (с помощью этой программы можно получить остаточные буферы памяти). В драйверах Intel и VESA проблемы нет.


        1. datacompboy
          18.01.2016 19:42
          +1

          Если интересно, там в баге хрома смогли воспроизвести и на интеле.