Переполнение буфера
От: Буравчик Россия  
Дата: 14.05.19 05:37
Оценка:
Если я правильно понимаю, при переполнении буфера слишком длинные данные затирают в стеке адреса возврата. Это позволяет подставить и выполнить свой код.

Но почему-бы не разделить стек? Сделать отдельный стек для данных и отдельный стек для адресов возврата.

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

Это же вроде компилятор решает, где и как хранить данные? Есть такая настройка?
Best regards, Буравчик
Отредактировано 14.05.2019 5:39 Буравчик . Предыдущая версия .
Re: Переполнение буфера
От: wildwind Россия  
Дата: 14.05.19 06:13
Оценка: +1
Здравствуйте, Буравчик, Вы писали:

Б>Но почему-бы не разделить стек? Сделать отдельный стек для данных и отдельный стек для адресов возврата.


Это будет уже другая архитектура процессора.
Re[2]: Переполнение буфера
От: Буравчик Россия  
Дата: 14.05.19 06:50
Оценка:
Здравствуйте, wildwind, Вы писали:

W>Это будет уже другая архитектура процессора.


Не, в процессор зашито использование стека только для адреса возврата. Я как раз и предлагаю оставить адрес возврата в обычном стеке (которой SS:SP).
А вот за то, что туда складываются локальные переменные и параметры процедур, отвечает компилятор. В самом процессоре это не регламентировано (x86).
Best regards, Буравчик
Re[3]: Переполнение буфера
От: Буравчик Россия  
Дата: 14.05.19 07:31
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Не, в процессор зашито использование стека только для адреса возврата. Я как раз и предлагаю оставить адрес возврата в обычном стеке (которой SS:SP).

Б>А вот за то, что туда складываются локальные переменные и параметры процедур, отвечает компилятор. В самом процессоре это не регламентировано (x86).

Или вообще можно менять SP перед и после каждым CALL/RET. Тогда можно использовать обычные PUSH/POP для данных, а не работать с памятью и кэшем, которые медленные.

Я понимаю, что это все будет чуть медленнее, чем сейчас. Но ведь это позволит убрать наиболее частую и наиболее критичную уязвимость — переполнение буфера.
По крайней мере, для определенных программ это может быть полезно, даже в ущерб скорости. Да и потеря в скорости может быть совсем крошечной.
Best regards, Буравчик
Отредактировано 14.05.2019 7:32 Буравчик . Предыдущая версия .
Re[3]: Переполнение буфера
От: mike_rs Россия  
Дата: 14.05.19 08:00
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Здравствуйте, wildwind, Вы писали:


W>>Это будет уже другая архитектура процессора.


Б>Не, в процессор зашито использование стека только для адреса возврата. Я как раз и предлагаю оставить адрес возврата в обычном стеке (которой SS:SP).

Б>А вот за то, что туда складываются локальные переменные и параметры процедур, отвечает компилятор. В самом процессоре это не регламентировано (x86).

тогда где-то (в куче?) придется выделять память для локальных переменых, это все будет жутко тормозить. А так — ну бекэнд сделай свой к llvm какому-нить, и генерируй другой asm код.
Re[4]: Переполнение буфера
От: Буравчик Россия  
Дата: 14.05.19 08:11
Оценка:
Здравствуйте, mike_rs, Вы писали:

_>тогда где-то (в куче?) придется выделять память для локальных переменых, это все будет жутко тормозить. А так — ну бекэнд сделай свой к llvm какому-нить, и генерируй другой asm код.


Cтек тоже находится в той же обычной памяти, рядом с кучей. Тормозить будет только переключение стеков "инструкции/данные", но не сами стеки.

Меня интересует, почему так еще не сделали, какие есть проблемы. Скорее всего, я чего-то не знаю или не понимаю.

P.S. На asm писать я почти не умею, сделать что-то для llvm не смогу.
Best regards, Буравчик
Re: Переполнение буфера
От: Буравчик Россия  
Дата: 14.05.19 08:28
Оценка:
Здравствуйте, Буравчик, Вы писали:

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

Б>Это же вроде компилятор решает, где и как хранить данные? Есть такая настройка?

Например, что-то типа такого должно быть:

Сейчас:

PUSH param1
PUSH param2
CALL func
...

func:
POP ax ;param2
POP bx ;param1
MOV ax, result
RET


С разделением стеков:
Каждый раз, когда программа работает с данными, она переключается на свой стек данных.
При выполнении инструкций CALL/RET переключаемся на стек инструкций.

MOV SP, data_stack
PUSH param1
PUSH param2
MOV data_stack, SP
MOV SP, instruction_stack
CALL func

...
func:
MOV instruction_stack, SP
MOV SP, data_stack
POP ax ;param2
POP bx ;param1
MOV ax, result
MOV data_stack, SP
MOV SP, instruction_stack
RET


Оверхэд — из-за MOV SP, ... т.к. адреса стеков хранятся в памяти. Но:
1. Этот участок памяти с очень большой вероятностью будет в L1 кэше
2. Можно хранить указатели в регистрах, например SP и CX. Менять их CHG sp, cx (или как там в asm)
3. Можно менять значения SP, сохраняя предыдущее значение в стеке инструкций (главное, чтобы не в стеке данных, т.к. стек данных уязвим, а стек инструкций нет).

Дополнение:
Посмотрел, есть XCHG reg, mem
т.е. можно делать XCHG SP, ячейка_для_хранения_неиспользуемого_SP, достаточно одной ячейки памяти и одной команды для переключения стека.
Ну и в идеале вместо ячейка_для_хранения_неиспользуемого_SP использовать регистр. Чтоб совсем быстро.
Best regards, Буравчик
Отредактировано 14.05.2019 8:39 Буравчик . Предыдущая версия . Еще …
Отредактировано 14.05.2019 8:33 Буравчик . Предыдущая версия .
Отредактировано 14.05.2019 8:30 Буравчик . Предыдущая версия .
Отредактировано 14.05.2019 8:29 Буравчик . Предыдущая версия .
Re: Переполнение буфера
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 14.05.19 09:49
Оценка: 15 (2)
Здравствуйте, Буравчик, Вы писали:

Б>Если я правильно понимаю, при переполнении буфера слишком длинные данные затирают в стеке адреса возврата. Это позволяет подставить и выполнить свой код.


Это самый простой вариант. Но затирание других данных тоже используется в эксплойтах.
А с приходом address space layout randomization (ASLR) ценность такого метода снизилась практически до нуля, разве что вышибешь процесс.

Б>Но почему-бы не разделить стек? Сделать отдельный стек для данных и отдельный стек для адресов возврата.


Есть архитектуры, где так сделано (IA64), есть программные реализации (такое требуется для Forth).
IA64 практически умер. Forth не используется кроме спецслучаев (загрузчики и т.п.) на процессорах с OoO и кэшированием.
Ссылок не найду, но практика показала, что получается только частичная защита от проблем.
Методы ограничения через явные проверки работают эффективнее, особенно если встроены в язык.
The God is real, unless declared integer.
Re[2]: Переполнение буфера
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 14.05.19 09:52
Оценка: 6 (1)
Здравствуйте, Буравчик, Вы писали:

Б>Дополнение:

Б>Посмотрел, есть XCHG reg, mem
Б>т.е. можно делать XCHG SP, ячейка_для_хранения_неиспользуемого_SP, достаточно одной ячейки памяти и одной команды для переключения стека.
Б>Ну и в идеале вместо ячейка_для_хранения_неиспользуемого_SP использовать регистр. Чтоб совсем быстро.

XCHG плохо, потому что Intel давно его испортил:

If a memory operand is referenced, the processor’s locking protocol is automatically implemented for the duration of the exchange operation, regardless of the presence or absence of the LOCK prefix or of the value of the IOPL. (See the LOCK prefix description in this chapter for more information on the locking protocol.)


Но любой другой регистр (особенно в x86-64) вполне может справиться.
The God is real, unless declared integer.
Re: Переполнение буфера
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 14.05.19 10:12
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Если я правильно понимаю, при переполнении буфера слишком длинные данные затирают в стеке адреса возврата. Это позволяет подставить и выполнить свой код.

Б>Но почему-бы не разделить стек? Сделать отдельный стек для данных и отдельный стек для адресов возврата.

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

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

Так что игра банально не стоит свеч.
Re: Переполнение буфера
От: Слава  
Дата: 14.05.19 11:39
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Но почему-бы не разделить стек? Сделать отдельный стек для данных и отдельный стек для адресов возврата.


Есть способ получше — верификация алгоритма, гарантирующая, что буфер не будет переполнен.
Re[4]: Переполнение буфера
От: vsb Казахстан  
Дата: 14.05.19 11:41
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Или вообще можно менять SP перед и после каждым CALL/RET. Тогда можно использовать обычные PUSH/POP для данных, а не работать с памятью и кэшем, которые медленные.


Почему медленные? Стек это ровно та же память. Не думаю, что PUSH будет существенно отличаться от "ручного" PUSH через любой другой регистр.
Отредактировано 14.05.2019 11:44 vsb . Предыдущая версия .
Re[5]: Переполнение буфера
От: Pzz Россия https://github.com/alexpevzner
Дата: 14.05.19 12:09
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Меня интересует, почему так еще не сделали, какие есть проблемы. Скорее всего, я чего-то не знаю или не понимаю.


Это не решит проблему радикально. Если переполнив буфер, можно попасть в соседнюю область памяти, то совершенно не обязательно целиться именно в адрес возврата. В памяти есть много других мест, куда можно с успехом прицелиться.

Радикально решает проблему вставляемая компилятором явная проверка переполнения буфера при каждом обращении. Всякие там managed языки (Go, Java, C# и т.д.) делают это по умолчанию, некоторые (все?) компиляторы Си тоже так умеют. Но это снижает скорость раза в два.
Re[2]: Переполнение буфера
От: mike_rs Россия  
Дата: 14.05.19 12:12
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>При выполнении инструкций CALL/RET переключаемся на стек инструкций.


Б>[code]

Б>MOV SP, data_stack
Б>PUSH param1
Б>PUSH param2
Б>MOV data_stack, SP

что такое data_stack? если глобальная переменная, то эту функцию нельзя использовать многопоточно, что чревато. Если нет, то как предполагается разруливать многопоточность?
Re[2]: Переполнение буфера
От: watchmaker  
Дата: 14.05.19 12:38
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:

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

Кому должен? Почему нельзя расширять? :)
Это не необходимое требование.
Например, в компиляторах clang или gcc уже лет десять доступна опция split-stack. С ней компилятор генерирует код, который не использует непрерывный стек. Сделано это как-раз для того, чтобы не выжирать всю виртуальную память, например если нужно на x86 в одном процессе запустить десяток тысяч потоков.

То есть, если интересно посмотреть как программы могут работать без непрерывного стека, то далеко ходить не надо — это уже давно реализовано. Востребованность этого, конечно, другой вопрос :)
Re[2]: Переполнение буфера
От: IID Россия  
Дата: 14.05.19 12:45
Оценка:
Здравствуйте, netch80, Вы писали:

N>А с приходом address space layout randomization (ASLR) ценность такого метода снизилась практически до нуля, разве что вышибешь процесс.


Снизилась только потому, что теперь надо 2 эксплоита вместо 1.
Сначала ID, потом уже RCE.
kalsarikännit
Re[3]: Переполнение буфера
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 14.05.19 12:47
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Например, в компиляторах clang или gcc уже лет десять доступна опция split-stack. С ней компилятор генерирует код, который не использует непрерывный стек.


А как при этом обеспечивается раскрутка стека средствами ОС в случае исключения? Или эта опция используется только в ОС, не поддерживающих раскрутку стека собственными средствами?
Re: Переполнение буфера
От: IID Россия  
Дата: 14.05.19 12:52
Оценка: 15 (2)
Здравствуйте, Буравчик, Вы писали:

Б>Если я правильно понимаю, при переполнении буфера слишком длинные данные затирают в стеке адреса возврата.


Против этого работают стековые канарейки/печеньки.
(Конечно, бывают исключительные случаи, типа тщательно подобранного зацикливания в рекурсии, чтобы перепрыгивать через них).

Б>Это позволяет подставить и выполнить свой код.


А твой код откуда возьмётся в атакуемом процессе ? Времена исполнения данных давно прошли, сейчас везде NX бит.
Для защиты от ROP/JOP придумали CFI (Control Flow Integrity).

Б>Но почему-бы не разделить стек? Сделать отдельный стек для данных и отдельный стек для адресов возврата.


Потому что tampered данные не менее опасны, чем адреса возврата.
Будет у тебя лежать в стеке структура с коллбеком. Или указатель на структуру с коллбеком. Ну ты понЕл...

Интел пару лет назад что-то рассказывал про двойной стек. Только в другом ключе — при возвратах проверяется теневой стек, что в него действительно заносился адрес и заносился с помощью команды CALL.
Вроде обещали сделать аппаратную реализацию.

Ещё можешь про технологию Pointer Authentication почитать. Она делает подобные атаки бесполезными.
kalsarikännit
Re[5]: Переполнение буфера
От: Буравчик Россия  
Дата: 14.05.19 12:55
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>Здравствуйте, Буравчик, Вы писали:


Б>>Или вообще можно менять SP перед и после каждым CALL/RET. Тогда можно использовать обычные PUSH/POP для данных, а не работать с памятью и кэшем, которые медленные.


vsb>Почему медленные? Стек это ровно та же память. Не думаю, что PUSH будет существенно отличаться от "ручного" PUSH через любой другой регистр.


Одна операция PUSH/POP это на самом деле две операции — запись в память и изменение регистра SP.
Нам придется явно выполнять две операции вместо одной. Плюс если SP является не регистром а ячейкой памяти, то еще добавится тормозов.
Best regards, Буравчик
Re[4]: Переполнение буфера
От: IID Россия  
Дата: 14.05.19 13:02
Оценка: 6 (1) +1
Здравствуйте, Буравчик, Вы писали:

Б>Но ведь это позволит убрать наиболее частую и наиболее критичную уязвимость — переполнение буфера.


Это очень редкая уязвимость в наши дни.

Гораздо чаще встречаются UAF (use after free) или Double Free (конвертируются в UAF).
Причём не сами-по-себе, а как результат гонки (race conditions).
kalsarikännit
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.