Pull to refresh

Comments 29

Спасибо за перевод. Добавлю несколько моментов.

1. Разделяемые библиотеки в Linux компилируются в режиме PIC. Соответственно, отобразить библиотеку по произвольным адресам проще, чем динамическую библиотеку в Windows. В последней все еще осложняется тем, что системные библиотеки (kernel32, system32, ntdll) загружаются по предрасчитанным непересекающимся адресам, дабы исключить релокацию при динамической линковке. В статье как то опускается этот момент.

2. В Linux есть утилита prelink, с помощью которой можно задать рандомизацию базового адреса (указав опцию -R). Вкупе с ускорением загрузки засчет предлинковки, это позволяет получить уникальные для вашей системы адреса загрузки и тем самым существенно усложнить атаки по известным адресам.

3. В русскоязычной среде «nop sled» всю жизнь называли «посадочной полосой».
Разделяемые библиотеки в Linux компилируются в режиме PIC. Соответственно, отобразить библиотеку по произвольным адресам проще, чем динамическую библиотеку в Windows.

Это означает и то, что библиотеки в Linux компилируются менее эффективный код, чем библиотеки в Windows.

Есть в тексте и фактические ошибки, например:
До ASLR, EXE всегда загружались в адрес 0x0040000 и могли полагаться на этот факт.

Базовый адрес выбирается при компиляции EXE, и может быть любым. Например, все стандартные приложения Windows скомпилированы с базовым адресом 0x00010000. По умолчанию линкер от MS выставляет базовый адрес 0x00400000 — на один шестнадцатеричный нолик больше, чем указано в тексте.
Спасибо за третий пункт — поправил в тексте.
Никогда не понимал, кто мешает разработчикам компиляторов использовать два отдельных стека: один — только для адресов возврата, другой — только для временных переменных. Тем самым вопрос перезаписи адресов возврата неправильным доступом к переменным снялся бы раз и навсегда, и переписывать ничего не надо было бы — только один раз перекомпилировать.
Никогда не понимал, кто мешает разработчикам компиляторов использовать два отдельных стека: один — только для адресов возврата, другой — только для временных переменных.

То, что на x86 и многих других платформах указатель стека — это один регистр (да, да, помню про ebp)? Это аппаратная привязка.
1) То, что привязка аппаратная, важно только для CALL / RET (или как там в x86 эти команды назвываются). То есть текущий аппаратный стек будет как раз стеком адресов возврата. А ползание по стеку данных можно и чисто программным сделать. (Да, чуть-чуть медлененне работать будет, на пол-процента, но кого это волнует, на гигагерцовых частотах-то?)

2) А кто мешает разработчкам процессоров тоже подтянуться? Да, это бы стоило немножечко денег, места на кристалле, и т.п. Но из преимуществ — полностью ликвидируется целый класс всевозможной малвари — имхо, это дорогОго стоит! Вбухивают же бабло и транзисторы в DEP и прочие гораздо менее эффективные и радикальные решения.
Мешает только обратная совместимость.

Windows на Itanium поддерживала два стека. Для x86 были экзотические ОС с поддержкой двух стеков. Ни то, ни другое не прижилось.
> Мешает только обратная совместимость.

Так обратная совместимость полная.

> Для x86 были экзотические ОС с поддержкой двух стеков. Ни то, ни другое не прижилось.

Вспомнилось:

"— Саид, почему твоя жена идёт впереди тебя? В Коране ж написано: жена должна идти позади мужа!
— Когда Коран писали, минных полей не было. Вперёд, Фатима, вперёд!"

Тогда и вирусы ещё не так досаждали :)
Так обратная совместимость полная.

Как код, скомпилированный для двух стеков, вызовет библиотечную функцию, скомпилированную для одного стека?
Простите, а в чём, собственно, проблема? Передача аргументов идёт по адресу, а то, что искомая ячейка находится в каком-либо стеке (и находится ли вообще) — это исключительно абстракция.
Напомните-ка, как (для определённости, в Windows для x86) функция получает аргументы?
Я понял Вашу мысль, но аргументы имеют заранее известную и неизменную размерность. Если в качестве аргумента нужно передать область памяти, то передаётся указатель. А атака класса «переполнение буфера» происходит из-за того, что вылезаем за временную область памяти, выделенную на стеке.

В двух словах, моё решение старо как мир:

"— Доктор, когда я делаю *так* (показывает), мне больно!
— А вы не делайте *так!*..." (с)
Кроме аргументов функций есть ещё alloca, SEH и масса разного добра.

То есть теоретически ничто не мешает перейти, а практически — это никому нафиг не нужно, так как работы не много, очень много, а безопасность не продаётся.
Указатель это само собой, но как ты передашь несколько указателей на 10...20 параметров функции? Самый простой способ — поместить указатели в стек и вызвать функцию. Иные способы передачи предполагают дополнительные телодвижения а значит имеют все шансы быть неиспользованными.
У вас слишком теоритические рассуждения, оторванные от реального ПО на х86.
Коллега сказал, что можно идти задом напёред и будет по Корану
UFO just landed and posted this here
Мне почему-то кажется, что «зато наша платформа защищает от хакеров и 80% вирусов!» — гораздо лучший маркетинговый слоган, чем пол-процента быстродействия…
Такая платформа уже есть, продаётся она… да в общем никак почти не продаётся.

Люди очень любят говорить про безопасность, а вот платить на неё — не любят. Собственно это основная причина, всё остальное — производные.
Скорей 10% вирусов. Большая часть зловредов не используют столь хитрые подходы — их запускает сам пользователь и предоставляет все необходимые права.
То, что привязка аппаратная, важно только для CALL / RET (или как там в x86 эти команды назвываются)

PUSH и POP + PUSHA тоже аппаратные. Обработчики прерываний тоже.

(Да, чуть-чуть медлененне работать будет, на пол-процента, но кого это волнует, на гигагерцовых частотах-то?)

Вы можете предоставить реальные данные по сравнению этих методов?

А кто мешает разработчкам процессоров тоже подтянуться?

Это другой вопрос. Вы же сказали «кто мешает разработчикам компиляторов», я и ответил про это.
Извиняюсь заранее за нубский вопрос, а почему нельзя сделать так чтобы данные в буфере заполнялись в обратную сторону? Тогда перезаписать можно было бы только мусор в стеке, а не адрес возврата.
Читаешь много со стека и потом спокойно себе записываешь поверх полезных данных.
К тому же это исторически так сложилось. В программе две динамические структуры борются за ограниченное пространство — самый простой способ это разрулить — рост размера этих структур в разные стороны.
Потому что в сторонних библиотеках никто не будет реализовывать работу с «перевернутыми» данными.
А если бы изначально так сделали?
Тогда проще изменить направление роста стека, чтобы он рос от меньших адресов к большим.

Но даже в таком случае атака через переполнение буфера остается возможной, просто «биться» будут не родительские фреймы стека, а дочерние. С одной стороны, это снижает шансы на успешную атаку — процессу проще упасть.
С другой стороны, тогда можно перезаписать указатель на тот буфер, который сейчас заполняется — и в таком случае атака может стать даже еще страшнее (запись произвольных данных по произвольному адресу в памяти).
Потому что LIFO. Потому что в начале идет push… а в конце — pop %eip
Sign up to leave a comment.

Articles