Понимание потока ядра при получении SIGSEGV для нулевого разыменования

Я пытаюсь выяснить последовательность событий, которые происходят внутри ядра Linux (x86_64, v6.9), когда мы пишем эти два кода:

// Null-dereference + writing to page zero
*(char *)0 = 0;
// Null-dereference + only reading from page zero
char c = *(char *)0;

Я попробовал проанализировать это с помощью Ftrace и вот что получил:

handle_mm_fault <-- do_user_addr_fault
sanitize_fault_flags <-- handle_mm_fault
arch_vma_access_permitted <-- handle_mm_fault
bad_area_nosemaphore <-- do_user_addr_fault
__bad_area_nosemaphore <-- do_user_addr_fault
force_sig_fault <-- __bad_area_nosemaphore

Итак, насколько я понимаю, мы вызываем ошибку страницы, и каким-то образом arch_vma_access_permitted() ИЛИ sanitize_fault_flags() решает вернуть VM_FAULT_SIGSEGV и __bad_area_nosemaphore() использует это, чтобы отправить SIGSEGV процессу с force_sig_fault(). Мой вопрос: каково разрешение нулевой страницы? Это вообще наносится на карту? Если это не так, то я думаю, что vma_is_foreign() должен охватить эту ситуацию и вызвать ошибку сегментации. Я также нашел кое-что интересное в load_elf_binary() для эмуляции поведения ABI предыдущих версий Linux:

if (current->personality & MMAP_PAGE_ZERO) {
        /* Why this, you ask???  Well SVr4 maps page 0 as read-only,
           and some applications "depend" upon this behavior.
           Since we do not have the power to recompile these, we
           emulate the SVr4 behavior. Sigh. */
        error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
                        MAP_FIXED | MAP_PRIVATE, 0);
}

Самая запутанная часть — это PROT_EXEC. Зачем нам нужно хранить инструкции внутри нулевой страницы? А если PROT_READ тоже есть, то, хотя current->personality есть MMAP_PAGE_ZERO, чтение с нулевой страницы не должно вызывать ошибку сегментации, верно? Не удалось найти спецификацию SVr4, поэтому я не уверен в деталях. Я также не уверен, когда это personality применимо к задаче, но мы можем сделать вывод, что в некоторых сценариях нулевая страница ПОЛУЧАЕТ сопоставление (конечно, мы можем переназначить ее, используя mmap(), если mmap_min_addr равно нулю, но я говорю о поведении по умолчанию, верно) теперь без переназначения). Я не могу найти ни одного другого vm_mmap() или do_mmap(), который отображает нулевую страницу.

1. Это может быть отображено на некоторых архитектурах. 2. В некоторых архитектурах это может быть код, который можно выполнить, например векторы прерываний. В общем, ваши вопросы во многом зависят от конкретного оборудования, которое вы здесь не рассматриваете.

0andriy 13.07.2024 15:34

@0andriy Я специально упомянул, что спрашиваю о x86_64. Я знаю, что даже функции, которые я упомянул, зависят от конкретной архитектуры. AFAIK x86_64 (не все архитектуры, конечно) не должен позволять пользователю использовать нулевую страницу по соображениям безопасности, но такие вещи, как vm86, нуждаются в этой функции, поэтому мы можем настроить mmap_min_addr. По крайней мере, переназначение не разрешено по умолчанию.

Iman Seyed 13.07.2024 16:24

Исходная пейджинговая система i386 не позволяла отключать разрешения на выполнение для каждой страницы; если страница была читабельной, то она автоматически исполнялась. Так что если имитировать историческое поведение, PROT_EXEC имело бы смысл.

Nate Eldredge 13.07.2024 17:54

@NateEldredge Интересно, спасибо за ответ. Это решает одну из загадок, но не представляет ли это угрозу безопасности?

Iman Seyed 13.07.2024 19:52

Как? Все это кажется немного запутанным, но любую инструкцию, которую вы можете запустить со страницы 0, вы также можете запустить с какой-то другой страницы. И вы даже не сможете выбрать инструкции, поскольку страница недоступна для записи.

user555045 13.07.2024 21:11

@user555045 user555045 Да, ты прав, я не обратил внимания на PROT_READ.

Iman Seyed 13.07.2024 21:56

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

Nate Eldredge 13.07.2024 22:19
*(char *)0 = 0; — неопределенное поведение в C, поскольку оно разыгрывает нулевой указатель. Вы должны, по крайней мере, сделать это (volatile char*), чтобы побудить компилятор действительно выполнить доступ, а не, например. выдача инструкции типа ud2 (недопустимая инструкция #UDtrap -> SIGILL) или другие удивительные вещи, например, оптимизация всех инструкций из функции, как это делает clang: godbolt.org/z/EWYGGqY5M Современный C не является переносимым языком ассемблера . Либо пишите на ассемблере, либо сделайте больше, чтобы скрыть свои махинации от видимости во время компиляции, например сохраните указатель в char *volatile tmp
Peter Cordes 15.07.2024 02:26

Риск безопасности, связанный с отсутствием защиты от атак Spectre и ROP из-за потенциального наличия байтов в исполняемой странице, которые не должны там находиться, поэтому их можно использовать в качестве гаджетов. Но, как говорит @NateEldredge, двоичные файлы SysVr4 в худшем случае имеют полноценные уязвимости, а в лучшем случае вообще не защищены, например. не PIE, поэтому нет ASLR для .text/.data/.bss, и все эти страницы будут исполняемыми, поскольку они, вероятно, также получат бит индивидуальности READ_IMPLIES_EXEC. Так что, вероятно, W^X тоже нет.

Peter Cordes 15.07.2024 02:34

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

Peter Cordes 15.07.2024 02:36

@PeterCordes Я не знаю, как volatile помогает. Можно заменить целочисленную константу 0 составным литералом 0, а именно. *(char *)(int){0} = 0;.

Ian Abbott 16.07.2024 17:34

@IanAbbott: Верно, это все еще нулевое разыменование, но это изменчивый доступ, поэтому некоторые компиляторы могут быть более точными в реализации этого в ассемблере. Например, clang делает ассемблер, который мы хотели, но GCC после этого использует ud2 вместо ret. Ваша идея также не позволяет избежать этого для GCC (и для clang только с volatile char*). Мы можем использовать char *volatile p = 0;return *p;, чтобы победить постоянное распространение, скрывая UB от оптимизатора, чтобы сделать реально работающий ассемблер. godbolt.org/z/eeh44E5ee

Peter Cordes 16.07.2024 20:00
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
12
171
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

какое разрешение у нулевой страницы? Это вообще наносится на карту?

Это не так. За исключением показанного вами необычного фрагмента кода или других исключений, таких как пользовательское пространство, явно запрашивающее его сопоставление при vm.mmap_min_addr = 0, ни одна страница обычно никогда не отображается по нулевому виртуальному адресу. Даже если бы это было так, поведение было бы таким же, как и у любой другой страницы, тот факт, что виртуальный адрес равен нулю, на самом деле не делает ее особенной.

Когда в программе, которую вы показываете, возникает ошибка, она оказывается точно внутри этой ветки из do_user_addr_fault(), потому что у процесса нет отображения (vma) для 0x0:

    ...
lock_mmap:

retry:
    vma = lock_mm_and_find_vma(mm, address, regs);
    if (unlikely(!vma)) {
        bad_area_nosemaphore(regs, error_code, address); // <== HERE
        return;
    }
    ...

Не знаю, почему вы упоминаете vma_is_foreign(), но на самом деле это нельзя назвать, если у вас изначально нет vma. bad_area_nosemaphore() будет отвечать за SIGSEGV доставку, вызывая force_sig_fault(), который затем вызывает другую серию функций для доставки сигнала.


Самая запутанная часть — это PROT_EXEC. Зачем нам нужно хранить инструкции внутри нулевой страницы?

Старое программное обеспечение ожидает такого поведения «чтение подразумевает выполнение», потому что именно так раньше работало оборудование. Фактически, это по-прежнему справедливо для старых 32-битных процессоров x86 и даже для некоторых 32-битных программ, работающих на x86_64. Посмотрите, например, на этот комментарий.

А если у него еще и PROT_READ, то, хотя у current->personality есть MMAP_PAGE_ZERO, чтение с нулевой страницы не должно вызывать ошибку сегментации, верно?

Верно.

Я также не уверен, когда эта личность применима к задаче.

В основном это зависит от базовой архитектуры. Код, специфичный для Arch, выбирает, какие биты личности переключать на новых руководителей. Макросы SET_PERSONALITY и SET_PERSONALITY2 определяются каждой аркой, и вы можете увидеть разные определения здесь и здесь.

Другие вопросы по теме