Pull to refresh

Comments 93

Ждал этой такой статьи именно от вас.
Вопрос немного не в тему — разве при эксплуатации этой уязвимости некоторые приложения иногда не будут падать с segfault?
UFO just landed and posted this here
Вообще то да. Атакующий не может знать как у меня распределение память, и не выйдем ли мы за пределы сегмента при попытке скопировать память. Да что там — мы и сами то не знам выйдем мы за сегмент или нет, зависит от того, как в данном конкретном случае.эта структура будет лежать в памяти, точнее — где будет лежать.

Собственно в си при случайном выходе за границу масссива (или просто обращении по не вал догму указателю) сегфолт бывает лишь иногда, а иногда просто чтение/запись не того что ожидалось. И это меняется от запуска к запуску приложения, и просто от раза к разу. Думаю каждый наблюдал подобное неоднократно в своих программах.
UFO just landed and posted this here
UFO just landed and posted this here
SIGSEGV (если мы говорим о юниксах и им подобных типа линукса) возникает следующим образом — все зарождается в MMU, при попытке обратиться к адресу не отображенному в память, это сигнал (прервывание? никогда непосредственно с MMU не работал, надо будет попробовать) дергает за хвост ядро, которое уже обрабатывает эту ситуацию, если эта страничка в свопе, то она из свопа подгружается в ОЗУ, и прикладному процессу возвращается управление как будто ничего и не было, если же приложение не имеет прав на чтение/записть этой странички, или она вообще не была для него валидна (скажем через mmap не отображена), то приложению посылается сигнал SIGSEGV.

Соответственно SIGSEGV в случае heartbleed может возникнуть ровно по той же причине, по которой он может возникнуть и в случае обычного выхода за границу массива — наступили на страничку которая не отображена в память, либо к которой у нас нет прав на read.

Поэтому, иногда, в некоторых случаях, сегфолт тут таки возможен. Вероятность возникновения сегфолта при эксплуатации дыры сильно зависит от менеджера памяти данной конкретной софтины и того как активно им пользуются.
UFO just landed and posted this here
Процесса? Сколько угодно. Ибо в этом же процессе может быть еще и вся бизнес-логика, работа с графикой, звуком и управлением 3Д печатью в одном флаконе.

А может занимать совсем мало — если это мой личный hello world работы с сокетами.
UFO just landed and posted this here
OpenSSL это либа, а не отдельное приложение. Даже если она создает отдельный поток для какой-то там своей активности (как скажем делает x264), то все равно это всё будет находиться в одном адресном пространстве с моей программой, которая печатает на 3Д принтере и запускает пони в космос. Более того — у них даже менеджер памяти скорее всего будет один и тот же.

Если бы весь OpenSSL был в отдельном процессе, то эта уязвимость стала бы сразу на порядок менее критической — ведь вся бизнеслогика, все логины, пароли, номера кредиток и так далее, были бы от нее изолированы, лежали бы в другом, отдельном адресном пространстве, до которого из OpenSSL нельзя было бы добраться даже теоретически, даже если поменять его код ради этого.

Уязвимы бы были только те данные которые прошли через OpenSSL в данную сессию в явном (не шифрованном) виде. (тут сессия — сессия работы приложения, то есть от старта процесса с OpenSSL)
OpenSSL может использоваться с неблокирующими сокетами в асинхронном сервере, где лишние форки вообще запрещены, ибо число воркеров должно быть постоянным.
Я уже ответил ниже.

SEGFAULT может возникнуть, когда буфер, из которого подпрограмма обработки TLS Heartbeat читает данные, волею звезд на небе расположился в конце динамической кучи, слишком близко к границе сегмента этой самой кучи.

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

Кстати, если натравить на сервер одновременно пару десятков тысяч атакующих, комбинируя при этом heartbleed с долгим чтением, то SEGFAULT становится неизбежным.
UFO just landed and posted this here
Ну вооббще все равно зря плюсанули, SEGFAULT не будет.
читаемый буфер НЕ вызодит за границы выделнной маллоком памяти, откуда сегфаул?
Heartbleed — это атака выхода за границы буфера. Что еще не понятно?
Посмотрел код — угу, читаем за пределами буфера pl
Не будет крэша. Потому что в один запрос — одна операция. Это возможно только для каких то редких приложений или самописных древностей, где бы СРАЗУ десятки тысячи запросов обрабатывались в пределах одного процесса (мультитред). А Apache будет форкать до предела, а потом ставить в очередь. При этом каждый форк будет обрабатывать один запрос.
Как раз наоборот, сейчас популярен асинхронный подход, как в nginx, где один и тот же процесс (и один и тот же поток) одновременно обрабатывает множество запросов.
Ну, я рассматривал случай с общим адресным пространством. Специфика ОС, в которой я работаю…

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

Для того, чтобы сервер упал с сегфолтом, очередной буфер должен быть выделен близко к концу динамической кучи — но его малый размер приводит к тому, что он чаще оказывается в начале.

Ну, и наконец, далеко не каждый segfault приводит к смерти приложения. Приложение может быть достаточно кривым, чтобы игнорировать сигнал — или же мастер-процесс может просто перезапустить упавший воркер.
Либо куча должна выделяться не непрерывным куском адресного пространства. То есть некоторые диапазоны адресов «из середины» должны быть не отображены в память.

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

Плюс, насколько я понимаю, если не увлекаться malloc/free, то есть если расположение данных довольно статично и предсказуемов (в пулах), то сделать дамп всей памяти процесса через эту уязвимость не получится, просто потому, что скорее всего через уязвимость будут утекать одни и те же 64Кб данных.
Совершенно верно. Кстати, хорошая идея для хранения приватных ключей в памяти — хранить их в отдельных блоках, окруженных блоками зарезервированными.

Кстати, работает и в обратную сторону. Я так делал с буферами, отдаваемыми сторонним библиотекам — была одна глючная либа, которая так и норовила повредить управляемую кучу и уронить сервер. Но отдельная область памяти плюс перехват AccessViolationException сотворили чудо…

Однако, такую сущность, как расшифрованный http-запрос, прятать заранее бесполезно — а значит, все на свете защитить особой работой с памятью не получится.
Если вы про heartbleed, то segfault не возможен при атаках
Мне каэется потому что при чтении указателя по указтелю pl, payload байт, memcpy не выходит за размеры, которые были выделены под буфер pl. Так как под буфер откуда копируется (pl), выделено памяти больше, чем фактически записано, а именно 64 кб. Надо смотреть исходники для пруфа)
Тогда это не было бы уязвимостью, скорее всего. По крайней мере не было бы ТАКОЙ уязвимостью. Ну и лечилось бы банальной заменой malloc на calloc.
Но и такой вариант был бы уязвимостью с примерно таким же выходом.
>Код слишком запутанный.
>Да потому, что OpenSSL качественный проект.
Может быть, сам факт того, что код запутанный говорит что-то о качестве продукта?
Был бы некачественным, не использовался бы во всём мире. А недостатки у всех есть.
Качественный проект с точки зрения статического анализатора?
Потому что с точки зрения использования — это ужасный продукт. Да, я его использовал достаточно много, но лишь потому что это единственный в свое время инструмент который мог подписывать документы по ГОСТ Р 34.10-2012 в среде linux. Других плюсов у данного инструмента — нет.
А в чем конкретно неудобство использования?
И речь идет о консольной тулзе или сишной либе?
Не знаю, но в тему, единственный минус, который я нашёл в OpenSSL это то, что его под Windows самостоятельно приходится собирать с обязательными правками, это претензия именно к качеству кода. Для любого опытного программиста, хоть раз встречавшегося с необходимостью интеграции сторонней библиотеки это всё привычно, но факт остаётся фактом.
Наверно не существует проекта где нету ляпов с проверкой указателя после его разыменования…
UFO just landed and posted this here
то есть они не используют популярные структуры данных, типо бинарного дерева, или связного списка?
UFO just landed and posted this here
Я знаю о c++. Но я всегда верил в то, что рекурсивные структуры данных не могут использовать не указатели, потому что рекурсия сразу развернется и займет всю доступную память. Я не прав?
Вы правы для Си, но не для языков типа Лиспа, к примеру.
template <typename T>
class bin_tree
{
private:
	struct node {
		node left;
		node right;
		T val;
	} root;
};

Вот такой код у меня даже не компилируется, если расставить указатели — все ок
UFO just landed and posted this here
Вы мне пример уже покажите, не терпится увидеть.
В 1960 конечно не было, но на сколько я понимаю тогда можно было оперировать сырой памятью и самому написать указатели, не называя их таковыми.
UFO just landed and posted this here
не знаю как вы, я всегда считал идиотизмом так извращаться и делать структуру, у которой главное приемущество — бесконечность в пределах памяти и динамическое добавление/удаление. В таком виде прийдется постоянно копировать элементы
UFO just landed and posted this here
Кольцевой буфер без указателей в точности также подвержен тем же ошибкам что и с указателями — промахнуться мимо конца массива как нефиг делать :-)

Какая разница, пишешь ты foo[n] или же *(foo+n)?
Но уж выход-то за границы массива отловить можно. Если вспомнить про такой язык, как Паскаль, то там проверки времени выполнения на выход за пределы диапазона включены по умолчанию…
UFO just landed and posted this here
То есть Паскаль за хранение длины строки в самой строке — уважения не достоин, в отличие от Фортрана?..
UFO just landed and posted this here
Ок. Отловите на этапе компиляции будет ли вот тут выход за границу массива:

int a = new int[rand()];
a[rand()] = 42;
Без проблем. Ошибка компиляции: невозможно привести тип int* к типу int.

PS мы же вроде договаривались без указателей?
Да какая разница?

int a[1234];
a[rand()] = 42;


Вместо rand может быть что угодно — например значение которое приехало по сети (собственно так heartbleed и работает), или вычисляемое по хитрому алгоритму значение. Или что-то там еще.

Единственное что тут можно делать — заставить явным образом перед использованием этого значения проверять его на вхождение в диапазон индексов массива. Но это, мягко говоря, не всегда эффективно будет, и в ряде случаев будет замусоривать код, делая его менее читабельным. Соответственно эту проверку будут отключать.
UFO just landed and posted this here
В паскале это отлавливается только в рантайме. В compile time, в общем случае, это не ловится даже теоретически.

Да, и отличий тут от указателей опять таки никаких — отловить обращение указателя не туда тоже можно в runtime. При желании.
И что? Чем вас не устраивает отлов таких ситуаций в рантайме?
Обычно устраивает (кроме тех случаев когда это начинает существенно тормозить работу алгоритма, бывают такие ситуации).

Но речь шла не об этом — речь шла о том, что если выпилить указатели из того же кольцевого буфера, то все сразу станет безопасней. Не станет. Валидность указателей в рантайме тоже можно проверять. Или точно также не проверять.

Опять таки, если вернуться в Си, то там убрав указатели и заменив на индексы массива мы не добьемся вообще ничего — они там просто эквивалентны.
Да, сразу — не станет. Но если действовать последовательно, то вполне реально получить проект, который в принципе не может испытывать проблем с указателями (если вы не забыли, началось все это обсуждение с вопроса — существуют ли такие проекты).
Ну да, проблем с указателями не будет. Будут другие проблемы :-) Например проблемы с индексами ;-)

Понятно что Си — штука не безопасная в том смысле, что там вероятность того, что там самодиагностика приложения находится в жутко рудиментарном состоянии. Там где в java будет exception, там в Си часто будет просто порча памяти. Молча. (причем оно потом может быть даже и крашнется, но совершенно внезапно и в совершенно другом месте)

Вопрос лишь в цене которую мы готовы заплатить за улучшенную самодиагностику приложения в рантайме, и большую предсказуемость поведения в случае ошибок :-)
Не «сразу — не станет», а никогда не станет, по крайней мере, если не переписать OpenSSL на другом языке, на котором ссылки и указатели — не одно и то же, как в С.
Ну внутри-то STD все равно используются указатели. Без них трудно.
UFO just landed and posted this here
Как насчёт Common Lisp или Scheme? Всё там есть — и деревья, и собаки. Однако указателей нет.
Я к сожалению мало знаком с функциональными языками. И я говорил на счет указателей относительно C/C++. В моем понимании указатель — это переменная, которая хранит адрес значения, а не само значение. Без такой возможности языка я не понимаю как можно создать рекурсивные структуры данных, которые хранят самих себя, с бесконечной (ограниченно памятью) вложенностью.
Ну и мы обсуждаем написание с нуля структуры данных, а не встроенное в язык. В C++ тоже есть std::list, который, кстати, использует указатели, но саму структуру можно использовать без указателей.
зато есть итераторы, мимо которых тоже можно промахнуться, и получить undefined behaviour ;-)
Расскажите, пожалуйста, а как анализатор узнал о BIO_write? Он сам увидел, что вместе со строкой функции всегда передается ее длина, или это увидели Вы и пометили ее для правила?
В статье написано:
Отвлекусь немного. У читателя мог возникнуть вопрос, как PVS-Studio находит такие ошибки. Поясню. Он собирает информацию о вызовах функций, в данном случае о strncmp(), и строит матрицу данных:

vstart, «ASCII», 5
vstart, «UTF8», 4
vstart, «HEX», 3
vstart, «BITLIST», 3

У функции есть строковый аргумент и числовой. В основном длина строки совпадает с числом. Значит, этот аргумент задаёт длину строки. В одном месте это не так и значит надо выдать предупреждение V666.
Я в конце рассказал, как работает диагностика V666. Ещё раз кратко. Анализатор PVS-Studio не требует никаких «пометок». Он собирает однотипные вызовы функций и строит матрицу данных. Затем, делает на её основе некоторые предположения. Если в функцию передаются строки и какие-то числовые аргументы, значения которых совпадает с длиной строки, то, наверное, это и есть длина строки. Если скажем, 20 раз совпало, а два раза нет, то видимо есть 2 ошибки.
О! Каждое правило это целая история и философские поиски. Я как ни будь начну рассказывать эти истории о разработке наиболее интересных диагностик. Просто всё никак. Вот Qt проверили, уже статья есть, но тут OpenSSL вклинился. Не публиковать же всё сразу. Статья про WinSCP лежит протухает… Прям очередь… А ведь ещё обязательно нужно статью про сравнение другими словами написать. А то прошлую заминусовали за то, что непочтительно отозвался о священной корове (Cppcheck). Никто так и не понял, какой объём работы был проделан и насколько адекватно мы старались сделать сравнение. Один только автор Cppcheck оценил проделанную работу :).
Но ничего, расскажу, расскажу про разработку привил.
Читаю все ваши статьи что попадаются. Но только после этого вашего комментария понял на сколько неординарный продукт вы делаете: «Он собирает однотипные вызовы функций и строит матрицу данных. Затем, делает на её основе некоторые предположения. Если в функцию передаются строки и какие-то числовые аргументы, значения которых совпадает с длиной строки, то, наверное, это и есть длина строки. Если скажем, 20 раз совпало, а два раза нет, то видимо есть 2 ошибки.»
Рад что попал на вашу статью. Теперь читаю все с интересом. У вас шикарный инструмент, надеюсь когда нибудь я смогу его себе приобрести!
Отдельно очень интересно было бы почитать рассказ о формировании правил проверки.
Можно начать с CppCat. Это намного более доступный инструмент.
А толку то. Это ведь для галочки сделано. Будет другая такая сложная ситуация и опять мимо. Я не планирую делать такую диагностику в PVS-Studio. Маркетологи конечно расскажут, как Coverity теперь вообще «все уязвимости ищет» и в том числе heartbleed. Толку-то. Parasoft вот тоже говорят «нашёл» эту ошибку. Где они раньше то были.
А чем так уникален этот баг что нельзя вывести из него обобщенный случай? Поиск выхода за границы и чтения «чужой» памяти будет полезна.
Потому, что статический анализ не всесилен. Я как раз недавно писал заметку на эту тему: Статический и динамический анализ кода. Плюс предлагаю почитать объяснения Coverity, почему всё не так просто: blog.regehr.org/archives/1125, ericlippert.com/2014/04/15/heartbleed-and-static-analysis/, blog.regehr.org/archives/1128. Их видимо тоже достали, раз уже несколько заметок на эту тему написали. :)
Да, согласен. Маркетинг ж)
Маркетологи заставили написать детектор именно для него. Будут теперь трубить на каждом шагу, что мол наш анализатор детектит heartbleed (неявно подразумевая, что и другие подобные баги тоже)! А на деле нет, но зато продается.
> Код корректен, но не аккуратен. Лучше вначале проверить переменную 'o', а уже потом использовать 'i':
На самом деле, возможно, что и нет. Т.к. тогда время выполнения кода при инициализированной o и при неинициализированной будет сильно различаться, что может как-то использовать атакующий. en.wikipedia.org/wiki/Timing_attack
Мне кажется, Вы что-то перемудрили. По вашим словам получается, что сложить два числа (j+=i;) это медленно и можно заметить потраченное время. Не верю.
Каюсь, невнимательно посмотрел. Мне показалось, что вы рекомендовали проверять o в САМОМ начале, до DecryptUpdate и т.п. Вот это действительно может быть проблемой, а перестановка двух соседних команд, скорее всего, нет. Хотя разницу в десяток сложений уже легко поймать статистически и использовать.
Чтобы делать такие заявления, надо иметь перед глазами листинг ассмеблера после всех оптимизаций. Компилятор не дурак и вполне может реорганизовать дерево условий так, чтобы вынести одинаковые подвыражения «за скобки». CSE называется. Как и в примере с ASN1_PRINTABLE_type.
Там парой строчек ниже и так стоит досрочный выход из функции! Время выполнения будет отличаться, независимо от местоположения j+=i
А разве качество кода как-то связано с его malware-назначением?
Ясно, что в openssl это был банальный недосмотр — но по-моему требовать, чтобы подобное было обнаружено статическим анализатором — это уж слишком.
Если так рассуждать — то при анализе исходников явных вирусов он тоже должен сразу распознавать явно злобные намерения кода и ругаться, что есть мочи?
Переменная 'i' может оказаться неинициализированной, если (o == false). В результате, к 'j' будет прибавлено непонятно что. Это не страшно, так как, если (o == false), то срабатывает обработчик ошибки, и функция прекращает свою работу.

IMO, это всё-таки достаточно серьёзно, поскольку использование неинициализированной переменной вызывает неопределённое поведение, и в результате может произойти что угодно.

Например, компилятор может думать так:
  1. В строчке j += i; неопределённое поведение, если o == false перед вызовом EVP_DecryptUpdate.
  2. Правильная программа никогда не вызовет неопределённое поведение в этой строчке.
  3. То есть, o никогда не будет false перед вызовом EVP_DecryptUpdate.
  4. Поэтому выбрасываем из кода первый if (o).
Компиляторы так не думают. И не будут так думать.
Просто потому, что это превратит огромное количество кода в русскую рулетку.
Так что не думаю что ваши опасения оправданны.
ну именно поэтому часть оптимизаций идут под грифом unsafe и не включаются автоматически. то есть компиляторы могут так думать.
но по дефолту не будут, так как это слишком умно для людей.
Sign up to leave a comment.