Найти статью
Теневая стековая память
Теневая стековая память (англ. shadow stack) в компьютерной безопасности — это механизм защиты сохранённого адрес возврата процедуры[1], например, от переполнения буфера стека. Сама теневая стековая память представляет собой второй, отдельный стек, который «отражает» стек вызовов программы. В пролог функции возвращаемый адрес сохраняется как в стеке вызовов, так и в теневой стековой памяти. В эпилог функции адрес возврата извлекается из обеих структур и сравнивается. Если записи адреса возврата расходятся, это считается атакой; типичной реакцией является завершение работы программы или оповещение системных администраторов о возможной попытке взлома. Теневая стековая память схожа по назначению с стековыми канареечными значениями, так как обе технологии служат для обеспечения целостности управляющего потока защищаемой программы, обнаруживая попытки изменить адрес возврата при эксплуатации уязвимостей.
Принцип работы
Теневая стековая память может реализовываться путём перекомпиляции программ с модифицированными прологами и эпилогами функций, с помощью динамической переработки двоичного кода, либо на уровне аппаратной поддержки. В отличие от стека вызовов, в котором также хранятся локальные переменные, переданные аргументы, сохранённые регистры и другие данные, теневая стековая память содержит, как правило, только вторую копию адреса возврата каждой функции.
Теневая стековая память обеспечивает более надёжную защиту адресов возврата, чем стековые канареечные значения, которые зависят от секретности своего значения и уязвимы к атакам с неразрывной записью. Для защиты самой теневой стековой памяти применяются защитные страницы или стратегия скрытия информации, затрудняющая злоумышленнику поиск места расположения теневого стека с целью изменения сохранённого адреса возврата.
Механизмы изоляции памяти зависят от архитектуры и типа реализации. В аппаратных реализациях на архитектуре x86 (Intel CET) область памяти помечается флагом VM_SHADOW_STACK, а на уровне таблиц страниц (PTE) страницы помечаются как доступные только для чтения и одновременно как «грязные» (dirty); прямая запись в такую память запрещена[2][3]. В архитектуре RISC-V (расширение zicfiss) изоляция обеспечивается специальной кодировкой PTE, при которой обычные операции записи вызывают ошибку доступа, а изменение стека возможно только специализированными инструкциями[4]. Программные реализации (например, Clang ShadowCallStack) хранят указатель на стек в выделенном аппаратном регистре и активно используют защитные страницы (Guard Pages) — большой регион без прав доступа, внутри которого случайным образом размещается теневой стек[5].
Как и стековые канареечные значения, теневая стековая память не защищает другие данные стека, кроме адресов возврата, и потому не полностью предотвращает уязвимости, связанные с нарушением безопасности обращения к памяти.
Программные и аппаратные реализации различаются по накладным расходам и уровню защиты. Программная реализация (например, Clang ShadowCallStack) требует инструментирования функций для сохранения и загрузки адресов возврата в прологе и эпилоге, что создаёт накладные расходы на процессор. Кроме того, несмотря на защиту от нелинейных переполнений, в программных реализациях возможна утечка адреса теневого стека через сторонние каналы процессора или операционной системы. Аппаратная поддержка (например, Intel CET) не имеет накладных расходов на инструментирование и обеспечивает аппаратную проверку адресов возврата, которая не подвержена уязвимостям состояния гонки[6].
В 2016 году компания Intel анонсировала поддержку теневой стековой памяти на аппаратном уровне в рамках технологии обеспечения целостности управляющего потока (Control-flow Enforcement Technology).
Аппаратная и программная поддержка
Технология теневого стека аппаратно поддерживается в процессорах AMD (начиная с микроархитектуры Zen 4) под названием CET shadow stack[7]. Для работы с теневым стеком в технологии Intel CET предусмотрены специфические инструкции, управляющие указателем теневого стека (SSP):
INCSSP— увеличивает значение указателя теневого стека (используется для его «разматывания»)[8];RDSSP— читает текущее значение указателя теневого стека в регистр общего назначения без его изменения[8];SAVEPREVSSP— сохраняет токен восстановления на предыдущем теневом стеке, извлекая токен из текущего стека[9];RSTORSSP— восстанавливает указатель теневого стека из токена восстановления (применяется при переключении потоков), переключая SSP на новый стек[10][8].
Поддержка технологии реализована в современных операционных системах в опциональном режиме:
- В Linux (начиная с ядра версии 6.6) поддержка для пространства пользователя по умолчанию отключена и требует активации через переменную окружения
GLIBC_TUNABLES=glibc.cpu.hwcaps=SHSTK[11]. - В Windows (начиная с Windows 10 версии 1903 / 19H1) технология поддерживается опционально для приложений, явно скомпилированных с флагом
/CETCOMPAT[11].
Проблемы совместимости
Использование теневой стековой памяти сопряжено с определёнными проблемами совместимости. В частности, после возникновения исключения или выполнения longjmp адрес возврата, извлечённый из вершины теневой стековой памяти, может не совпадать с соответствующим адресом из стека вызовов. Для решения этой проблемы в схемах прямого отображения синхронизация происходит автоматически, а для косвенного отображения применяется дополнительная инструментация: в теневой стек вместе с адресом возврата сохраняется указатель основного стека, а вызовы `setjmp` и `longjmp` перехватываются[12]. Взаимодействие с библиотеками раскрутки стека (например, libunwind) при обработке исключений в C++ для обеспечения консистентности также зависит от архитектуры. При прямом отображении механизм C++ просто корректирует основной стек, а при косвенном требуется инструментация библиотек раскрутки для безошибочного восстановления указателя теневого стека[12].
В многопоточных программах, где для каждого потока создаётся отдельный стек вызовов, создаётся также и отдельная теневая стековая память для отражения соответствующего стека. Для обеспечения совместимости с legacy-кодом применяются механизмы точечного отключения (per-application opt-out) теневого стека для несовместимых приложений, такие как glibc System-Wide Tunables в Linux[13].
Уязвимости и обход защиты
На конференции DEF CON 34 был представлен метод обхода защиты теневого стека «Writing to Shadow Stacks». В рамках доклада об аппаратной реализации защиты CET (Control-flow Enforcement Technology) были продемонстрированы подходы, позволяющие злоумышленнику записывать контролируемые значения непосредственно в теневой стек[14]
Примечания
Литература
- Chieuh, Tzi-cker. RAD: A Compile-Time Solution To Buffer Overflow Attacks // Proceedings 21st International Conference on Distributed Computing Systems : [англ.] / Tzi-cker Chieuh, Fu-Hau Hsu. — 2001. — P. 409–417. — ISBN 0-7695-1077-9. — doi:10.1109/ICDSC.2001.918971.
- Ozdoganoglu, H.; Brodley, C.; Vijaykumar, T.; Kuperman, B. (2006). “SmashGuard: A Hardware Solution To Prevent Attacks on the Function Return Address” (PDF). IEEE Transactions on Computers [англ.]. 55 (10): 1271—1285. DOI:10.1109/TC.2006.166. Дата обращения 2024-06-18.
- Delshadtehrani, Leila; Eldridge, Schuyler; Canakci, Sadullah; Egele, Manuel; Joshi, Ajay (1 января 2018). “Nile: A Programmable Monitoring Coprocessor”. IEEE Computer Architecture Letters [англ.]. 17 (1): 92—95. DOI:10.1109/LCA.2017.2784416. ISSN 1556-6056.