«Невидимые» проблемы и безопасность

Аннотация.

Эта статья написана одним из разработчиков Bitcoin Core, приглашенным автором BitMEX Майклом Фордом. Майкл — первый получатель гранта по программе для разработчиков продуктов на базе биткоина, учрежденной компанией HDR Global Trading Limited. Этот материал продолжает первую статью Майкла, написанную для нашего блога — «Построение систем и безопасность: эволюция биткоина». В новой статье Майкл рассказывает о четырех сделанных им улучшениях системы безопасности Bitcoin Core:

  1. Исправление скрытой ошибки, которая препятствовала проверке безопасности.
  2. Устранение слабых мест в системе безопасности Windows.
  3. Решение проблемы, которая ослабляла генератор случайных чисел и была вызвана неспособностью обнаружить одну из функций в системе MacOS, и
  4. Добавление проверок на расхождение в работе/характеристиках компоновщика macOS по сравнению с документацией.

Этот материал наглядно доказывает важность кроссплатформенного тестирования.

Обзор

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

Для такого проекта, как Bitcoin Core, критически важна бесперебойная работа системы сборки, ведь от нее зависят версия и возможности C++, которые мы можем использовать, наши зависимости и их настройка, а также функции усиления защиты и безопасности, которые применяются к нашим двоичным файлам. Она также обеспечивает кроссплатформенность, обратную совместимость и по большому счету дает нам детализированный контроль над структурой bitcoind.

security-check.py и symbol-check.py пропускают bitcoind

Правила «проверки символов» (check-symbols) и «проверки безопасности» (check-security) были добавлены в наш сборочный файл (Makefile) в патче #7424. Эти правила — удобный способ проверить исполняемые файлы на безопасность и обратную совместимость, и они стандартно используются в рамках нашего процесса выпуска новых релизов.

Но они были добавлены уже довольно давно, и с тех пор в них появилась  трудно поддающаяся выявлению ошибка, которая мешала передаче bitcoind в скрипты. Эта проблему можно продемонстрировать с помощью нескольких строчек на Python:

# touch a, touch b, touch c
# python3 args.py < a b c

import sys
if __name__ == '__main__':
        print(sys.argv)
        # ['args.py', 'b', 'c']
       
        # if you add some lines to "a",
        # you'll see them here.
        for line in sys.stdin:
                print(line)

 

Использование символа «<» в BASH означает переадресацию ввода, что в нашем случае привело к тому, что первый аргумент, который передавался любому из скриптов (т.е. bitcoind), открывался для чтения при стандартном вводе. По сути, это делало его «невидимым» для обоих скриптов,  считывающих аргументы из файла sys.argv.

Эта проблема была исправлена в патче #17857; исправления были бэкпортированы в некоторые ветки. Обратите внимание, что реальное влияние этой проблемы было довольно незначительным, так как bitcoind по сути является подмножеством (с кодом и зависимостями) bitcoin-qt, который по-прежнему проверялся обоими скриптами. Как бы там ни было, это хороший пример «невидимой» проблемы, которая могла иметь более серьезные последствия и которой удавалось оставаться незамеченной в течение почти 4 лет.

Технология ASLR не работала в файлах bitcoin-cli в Windows

Рандомизация размещения адресного пространства (ASLR) — это технология обеспечения безопасности, которая не дает хакерам повторно использовать данные, которые использовались процессом для атаки, произвольно (рандомизированно) размещая их в адресном пространстве процесса. Из-за фатального сочетания ошибки в компоновщике, который мы используем для релизов на Windows (binutils-mingw-w64-x86-64), и того факта, что один из наших исполняемых файлов, `bitcoin-cli`, не экспортировал никаких символов, до недавнего времени бинарный клиент не использовал технологию ASLR во время исполнения.

Компоновщик, который поставляется с бинутилями (GNU ld), известен  многочисленными проблемами с созданием двоичных файлов Windows. Одной из этих проблем было то, что он убирает раздел .reloc из двоичных файлов, даже если этот раздел необходим для работы ASLR в Windows (наряду с дополнительными битами заголовка и т. д.); это не происходило только в том случае, если двоичный файл экспортировал символы.

Все наши двоичные файлы Windows, кроме bitcoin-cli, сейчас экспортируют символы secp256k1, поэтому эта проблема их не коснулась. Мы придумали временное решение проблемы для файлов bitcoin-cli (патч #18702), которое заключается в экспортировании одного символа (`main()`). В конце концов мы решим эту проблему и уберем это временное решение, когда сможем использовать совершенно новую версию бинутилей для сборки наших релизов.

Эта заплатка также была включена в релиз 0.20.0. В патче #18629 в наш сценарий проверки безопасности был добавлен дополнительный тест, проверяющий наличие раздела .reloc во всех исполняемых файлах для Windows. В сочетании с существующими проверками безопасности это должно гарантировать использование защиты ASLR во всех исполняемых файлах.

sysctl() API отличался на платформах macOS и *BSD

Системный вызов `sysctl()` можно использовать для получения и установки определенной системной информации, в том числе сведений о времени загрузки ядра, типе процессора, объеме оперативной памяти и т. д. В зависимости от того, вызываете ли вы `sysctl` в macOS или в варианте BSD (FreeBSD, NetBSD и пр.), приходится использовать немного отличающиеся параметры функции. В системе macOS первым аргументом sysctl() является `int *name`, в то время как в *BSD первым аргументом является `const int *name`.

Из-за этого небольшого отличия вызовы в нашей системе сборки, которые мы использовали для определения наличия sysctl(), иногда давали сбой, хотя не должны были. Изначально мы использовали для вызова параметр `const int *name`, и при компиляции в системе MacOS происходил «невидимый» сбой (который можно было обнаружить, только заглянув в файл журнала config.log).

Почему это интересно? Причина этого сбоя не была указана в сводке патча, который исправил проблему (#18359), или оригинального изменения, которое не коснулось random.cpp.

До удаления OpenSSL в наш генератор случайных чисел (ГСЧ) был добавлен новый модуль randomenv, который используется раз в минуту для сбора дополнительной энтропии из окружающей среды; и sysctl() — один из методов, который используется для сбора энтропии. Как следствие, если система сборки не могла обнаружить наличие sysctl() в момент компиляции, в нашем бинарном файле не использовались вызовы для сбора энтропии.

В результате этого сбоя в ГСЧ в системе macOS поступало примерно на 22 байта меньше энтропии, чем если бы вызовы sysctl() обрабатывались должным образом.

Эта проблема была некритичной и не нарушала работу нового модуля ГСЧ (будет выпущен в версии 0.20.0), так как в некоторых системах вызовы sysctl() обрабатываются по принципу “if-supported” (при наличии поддержки). Тем не менее, эта проблема — еще один  хороший пример «невидимого» сбоя в момент сборки, который может иметь неочевидные последствия в другом месте бинарного файла.

 «Ленивая» привязка, ld64 и загрузчик на MacOS

В патче  #17686 я добавил `-bind_at_load` как одну из «проверенных» меток для нашей сборки для macOS. При наличии этой метки ld64 устанавливает в заголовке программы бит, который показывает динамическому загрузчику (dyld), что все символы разрешаются при запуске программы, а не в «ленивом» режиме (при первом использовании). Аналогичного эффекта можно достичь с помощью передачи метки (флажка) `-z,now` в GNU ld.

Однако было ясно, что вопреки документации, ld64 на самом деле не вставляет бит `MH_BINDATLOAD` в заголовок двоичного файла. У меня возникло два вопроса: работает ли эта функция как нужно, и если да, то как проверить ее выполнение в ходе наших проверок безопасности, если в заголовке нет бита (т.е. его нельзя задать в поиске)?

Дальнейшее тестирование показало, что при использовании для сборки метки `-bind_at_load` привязка символов происходит при запуске, и ключ к тестированию, скорее всего, кроется в структурах lazy_bind_* в другом разделе заголовка MACHO.

Потратив некоторое время на изучение источника ряда инструментов Apple, мы поначалу собирались сделать патч для вставки недостающего бита с помощью ld64, который мы используем для сборки наших релизов (что нашло отражение в запоздалом патче #18295).  Но благодаря информации, полученной от Ника Кледзика (Nick Kledzik), который занимается разработкой компоновщика и загрузчика в Apple, было решено, что в патче нет необходимости, и мы просто добавим проверку безопасности структур заголовков, указанных в #17686, при этом по сути игнорируя бит MH_BINDATLOAD (почти то же самое, что сейчас делает dyld).

В последнем релизе утилит командной строки Apple (ld64-556.6) на справочной странице ld64 по-прежнему указано, что при использовании метки ‘bind_at_load’ в заголовок вставляется дополнительный бит.

Заключение

Хотя эта статья — всего лишь краткий обзор изменений, над которыми я работал в последнее время, надеюсь, что они наглядно объясняют, почему:

  • Кроссплатформенное тестирование всегда важно.
  • Первичные инструменты не всегда работают, как должны, поэтому не стоит на них полностью полагаться.
  • Даже если они работают правильно, не всегда можно верить документации.
  • В любом случае вам нужна собственная (рабочая) система сдержек (проверок) и противовесов.

Майкл Форд, разработчик Bitcoin Core

CC BY-SA 4.0