Pull to refresh

Comments 11

Принцип эксплуатации этого бородатого бага основан на увеличении объема стека без записи в него. Это делается по-разному в разных ОС, например, с помощью рекурсивного вызова процедуры, или многомегабайтными аргументами командной строки. Указатель стека перемещается на его начало (нижний адрес), стек резко наращивается, и – хоп, – указатель стека оказывается уже за сторожевой страницей. Доступа к самой сторожевой странице не происходит, ошибки нет. Получается, что область стека перекрывается с динамической областью какого-то процесса.

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


У вас здесь написана ерунда, потому что вы смешали в кучу пользовательский и ядерный стек.
Смотрите: сторожевая страница находится на границе пользовательского стека.

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


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

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


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

В статьях на которые приведены ссылки идёт речь о переполнении ядерного стека.
Вы правы. Исправил путаницу, насколько возможно.
Честно говоря, странно видеть, что уязвимость перезаписи кучи стеком (или наоборот) до сих пор существует на 64-битных системах. Что мешает использовать для кучи и для стека адреса, разнесенные на сто тысяч гигабайт (условно)? Ядро же живет себе в верхней половине адресного пространства и его это не беспокоит.
В Windows изначально под esp нет физических страниц памяти, и чтобы туда замапилась очередная страница, нужно сделать чтение по адресу под последней выделенной страницей (и не ниже).

Например, если на стеке 10кб локальных переменных, MSVC в начале функции выполняет два холостых чтения из [esp-0x1000] и [esp-0x2000], и это не для защиты от уязвимостей, а так работает расширение стека в Windows.

Поэтому в Windows компиляторы вынуждены увеличивать стек постранично, не имея возможность пропустить guard page. Интересно, как работает стек в Linux.
Тогда почему esp проскакивает guard page?
Для ядра компилятор генерирует код по-другому?

Насколько я понимаю, в стеке ядра нет защитных страниц.

mayorovp не так же. Вот кусок обработчика страничного сбоя для linux x86:

        vma = find_vma(mm, address);
        if (unlikely(!vma)) {
                bad_area(regs, error_code, address);
                return;
        }
        if (likely(vma->vm_start <= address))
                goto good_area;
        if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
                bad_area(regs, error_code, address);
                return;
        }
        if (error_code & PF_USER) {
                /*
                 * Accessing the stack below %sp is always a bug.
                 * The large cushion allows instructions like enter
                 * and pusha to work. ("enter $65535, $31" pushes
                 * 32 pointers and then decrements %sp by 65535.)
                 */
                if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
                        bad_area(regs, error_code, address);
                        return;
                }
        }
        if (unlikely(expand_stack(vma, address))) {
                bad_area(regs, error_code, address);
                return;
        }

В переводе на русский: для сбойного адреса ищется первая VMA, конечный адрес которой больше чем адрес сбоя. Если такая VMA найдена, и она расширяется вниз (VM_GROWSDOWN) — то это область стека. Попытаться её расширить функцией expand_stack.

Т.е. сбой по любому адресу в пустом месте перед стековой VMA вызовет попытку расширения стека.

Для ядра компилятор генерирует код по-другому?

Это не о ядерном коде вообще. Ядерные стеки имеют фиксированный размер и никогда не расширяются.
Это не о ядерном коде вообще. Ядерные стеки имеют фиксированный размер и никогда не расширяются.

Вот именно, об этом я и говорил.

jcmvbkbc, теперь понятно.

Linux в usermode может расширять стек не на 1 страницу, а сразу намного (но не более 64KB за 1 раз, наверное чтобы случайное чтение на 15 гигов ниже стека не скушало всю физическую память под стек).
но не более 64KB за 1 раз

Это вы из проверки внутри if (error_code & PF_USER) сделали вывод? Но стек необязательно двигать командой entry. Можно тупо sub esp, 100500. Эта проверка о том, что обращаться к стеку ниже текущего указателя стека вообще-то нельзя, но если надо — то можно, но недалеко (64К).
Sign up to leave a comment.