Все слышали про взлом Бинанса: увели около $6.3 млрд, крупнейший взлом в истории крипты. Ключи не утекли: по отчёту самой биржи, разработчика взяли фейковым собеседованием и занесли вредоносный код на его машину (BBC).

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

29 октября днём пользователи Polymarket начали сообщать о списаниях и принудительных продажах позиций. Сначала международную площадку перевели в режим только отмены, потом остановили торговлю и relayer, через который проходят торговые заявки. Примерно через час торговля вернулась (The Paypers).

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

30 октября на статус-странице появился постмортем с деталями: возвраты $3.9M, из них $2.7M прямых потерь и $1.2M компенсации за принудительно проданные позиции и за саму паузу в торговле. 95% затронутых кошельков получили полный возврат уже 30 октября (постмортем). Заявки на возврат не требовались: затронутые кошельки определяли автоматически по ончейн- и торговым данным.

Bitquery по блокчейну насчитали 737 кошельков с суммарной потерей около $2.73M за один час, и это отдельный счёт (KuCoin, Gate). А в блокчейне видно 985 несанкционированных выводов за тот же час, и все они прошли через официальный relayer Polymarket с валидными авторизациями кошелька.

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

Как это сработало

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

Атакующие подменили DNS-записи вендора и начали отдавать вредоносный JavaScript через тот же адрес. Для браузера ничего не изменилось: правильный домен, тот же адрес, тот же origin, что у самого сайта.

Polymarket публично не назвал имя вендора. Имя DebugBear появилось в прессе, а сам вендор подтвердил, что его DNS-записи были скомпрометированы, и попросил клиентов снять скрипт RUM со своих сайтов (Search Engine Journal).

Скрипт стоял не только у Polymarket. Для атакующего это удобная точка: он работал на всех страницах, включая те, где пользователь уже вошёл в аккаунт.

Что именно отдавалось по этому адресу, разбирали в Hudson Rock. Пересказ их работы есть у SecurityWeek, The Hacker News и Freebuf. Исследователи дважды скачали файл с того адреса, с которого сайт Polymarket грузил скрипт мониторинга, и оба раза получили идентичную копию.

Я думаю, что происходило всё примерно так. Код читал из браузерного хранилища данные аккаунта, адрес кошелька и торговые API-креды. Потом искал в React-компонентах приложения торговый клиент и его подписант, которым клиент подписывает запросы. И отправлял собственные запросы на вывод через официальный relayer Polymarket.

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

Образец исследователи не запускали, так что это разбор кода, а не запись атаки.

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

Почему смарт-контракт ни при чём

Ни одна из 985 транзакций не была эксплойтом контракта. Использовались валидные авторизации кошелька, и с точки зрения сети запрос выглядел законным: его подписал подписант пользователя, а отправил в сеть официальный relayer площадки.

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

Тут же объясняется и вопрос с approvals: разрешения на траты были выданы заранее, их никто не подделывал, и вредоносный код просто воспользовался тем, что уже действовало.

Ещё одна цифра, которая отвечает на вопрос, кто нажимал кнопки. В выборке из 30 транзакций вывода каждая исполнялась в блокчейне через три-семь секунд после появления авторизации. Человек так не успевает. Это автоматика.

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

Кого выбирали целью

737 кошельков, четыре счёта дали 43% всего ущерба, а 276 счетов потеряли меньше $1k каждый.

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

Тот же класс атаки: Ledger Connect Kit

14 декабря 2023 года злоумышленник подменил пакет в цепочке поставки библиотеки Ledger Connect Kit. Вредоносный код попал в dapps, и пользователи подписывали транзакции на слив своих средств. Подменили не ключи, а библиотеку, которую dapps подключали себе сами.

Инцидент разобрали быстро, но часть пользователей всё-таки успела подписать. И официальный отчёт Ledger, и письмо CEO сходятся на одном выводе: браузерная подпись транзакций и цепочка поставки фронтенд-кода это граница доверия.

Класс атаки не новый. Меняется только точка входа: тогда подменили пакет, сейчас DNS-записи вендора мониторинга. И не меняется главное: атакующему не нужны ваши ключи. Ему нужен код, который ваша страница загружает сама.

Граница доверия в браузере

Сессия в веб-приложении это API с полными правами. Пока пользователь в системе, его вкладка может читать баланс, подписывать транзакции и выводить средства. И сервер обязан принимать такие запросы, иначе продукт не работает.

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

Скрипту веб-мониторинга мы доверяем больше, чем другим, потому что он вроде бы только меряет скорость. Но что именно он делает на странице, решает уже не тот, кто его туда подключал.

Так что уровней полномочий у продукта три: страница, сессия, relayer. Контракт это уровень четвёртый, и к этой атаке он отношения не имеет. Защита нужна на каждом из трёх. И если один уровень верит другому на слово, вся цепочка держится на этом доверии.

Что делать и что осталось неизвестным

Практика, которую я вывел для себя после этого инцидента.

  1. Меньше сторонних скриптов. Каждый, который можно убрать, убираем.

  2. CSP, чтобы страница не могла грузить код откуда угодно.

  3. SRI и явный пиннинг версий: страница должна точно знать, какой файл она грузит.

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

  5. Подтверждение на каждое движение денег, а не только на вход в систему.

  6. Лимиты на операции и задержка вывода.

  7. Разделение полномочий: один инструмент для торговли, другой для вывода.

  8. Мониторинг целостности, чтобы подмена была видна сразу.

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

Polymarket после инцидента обещает примерно то же: усиленный CSP, пиннинг версий сторонних скриптов, песочницу и постоянный мониторинг целостности.

Теперь к тому, что осталось неизвестным. Сколько попыток вообще не дошло до вывода средств, неизвестно, и описание работы кода остаётся описанием: его читали, но не запускали. Кто стоял за атакой, в доступных мне источниках не сказано. Узнаем ли мы, назовёт ли Polymarket вендора публично, тоже неизвестно.

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

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

Комментарии (0)