Я пытаюсь выяснить последовательность событий, которые происходят внутри ядра 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()
, который отображает нулевую страницу.
@0andriy Я специально упомянул, что спрашиваю о x86_64. Я знаю, что даже функции, которые я упомянул, зависят от конкретной архитектуры. AFAIK x86_64 (не все архитектуры, конечно) не должен позволять пользователю использовать нулевую страницу по соображениям безопасности, но такие вещи, как vm86, нуждаются в этой функции, поэтому мы можем настроить mmap_min_addr. По крайней мере, переназначение не разрешено по умолчанию.
Исходная пейджинговая система i386 не позволяла отключать разрешения на выполнение для каждой страницы; если страница была читабельной, то она автоматически исполнялась. Так что если имитировать историческое поведение, PROT_EXEC
имело бы смысл.
@NateEldredge Интересно, спасибо за ответ. Это решает одну из загадок, но не представляет ли это угрозу безопасности?
Как? Все это кажется немного запутанным, но любую инструкцию, которую вы можете запустить со страницы 0, вы также можете запустить с какой-то другой страницы. И вы даже не сможете выбрать инструкции, поскольку страница недоступна для записи.
@user555045 user555045 Да, ты прав, я не обратил внимания на PROT_READ
.
Это может быть проблемой безопасности, если вы полагаетесь на то, что нулевое разыменование должно привести к сбою программы, а затем этого не происходит. С другой стороны, если вы используете настолько старые двоичные файлы, что они взяты из SysVr4, они наверняка уже полны уязвимостей.
*(char *)0 = 0;
— неопределенное поведение в C, поскольку оно разыгрывает нулевой указатель. Вы должны, по крайней мере, сделать это (volatile char*)
, чтобы побудить компилятор действительно выполнить доступ, а не, например. выдача инструкции типа ud2
(недопустимая инструкция #UDtrap -> SIGILL) или другие удивительные вещи, например, оптимизация всех инструкций из функции, как это делает clang: godbolt.org/z/EWYGGqY5M Современный C не является переносимым языком ассемблера . Либо пишите на ассемблере, либо сделайте больше, чтобы скрыть свои махинации от видимости во время компиляции, например сохраните указатель в char *volatile tmp
Риск безопасности, связанный с отсутствием защиты от атак Spectre и ROP из-за потенциального наличия байтов в исполняемой странице, которые не должны там находиться, поэтому их можно использовать в качестве гаджетов. Но, как говорит @NateEldredge, двоичные файлы SysVr4 в худшем случае имеют полноценные уязвимости, а в лучшем случае вообще не защищены, например. не PIE, поэтому нет ASLR для .text/.data/.bss, и все эти страницы будут исполняемыми, поскольку они, вероятно, также получат бит индивидуальности READ_IMPLIES_EXEC. Так что, вероятно, W^X тоже нет.
Страница, сопоставленная с нулевым адресом virt, может позволить выполнению пройти нулевое разыменование с фиктивными данными вместо сбоя, превращая некоторые ошибки в уязвимости безопасности, так что да, вы можете захотеть скопировать исполняемый файл в более современный исполняемый файл Linux ELF или что-то еще. может существовать способ запустить его без этой индивидуальности, если он не полагается на него, и вы не можете просто перекомпилировать его из исходного кода.
@PeterCordes Я не знаю, как volatile
помогает. Можно заменить целочисленную константу 0
составным литералом 0
, а именно. *(char *)(int){0} = 0;
.
@IanAbbott: Верно, это все еще нулевое разыменование, но это изменчивый доступ, поэтому некоторые компиляторы могут быть более точными в реализации этого в ассемблере. Например, clang делает ассемблер, который мы хотели, но GCC после этого использует ud2
вместо ret
. Ваша идея также не позволяет избежать этого для GCC (и для clang только с volatile char*
). Мы можем использовать char *volatile p = 0;
return *p;
, чтобы победить постоянное распространение, скрывая UB от оптимизатора, чтобы сделать реально работающий ассемблер. godbolt.org/z/eeh44E5ee
какое разрешение у нулевой страницы? Это вообще наносится на карту?
Это не так. За исключением показанного вами необычного фрагмента кода или других исключений, таких как пользовательское пространство, явно запрашивающее его сопоставление при 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
определяются каждой аркой, и вы можете увидеть разные определения здесь и здесь.
1. Это может быть отображено на некоторых архитектурах. 2. В некоторых архитектурах это может быть код, который можно выполнить, например векторы прерываний. В общем, ваши вопросы во многом зависят от конкретного оборудования, которое вы здесь не рассматриваете.