Перед ежегодной конференцией ZeroNights 2017, помимо Hackquest 2017, мы решили организовать еще один конкурс, а именно — провести свое ICO (Initial coin offering). Но только не такое, как все привыкли видеть, а для хакеров. А как мы могли понять, что они хакеры? Они должны были взломать ICO! За подробностями прошу под кат. Для начала — легенда.  Задача участников проекта состояла в следующем: И вот так выглядела часть, на которой отображались заявки в whitelist, сам whitelist и заветная кнопка, "которую должен был нажать владец".   Если проанализировать предоставленные ссылки на etherscan.io, то вырисовывается следующая архитектура смарт-контраков: После прочтения ICO становится очевидно, что просто так заявку на рассмотрение владельцу не подашь: Где же взять столько эфиров? Первое, что может прийти в голову, — намайнить! Сеть-то тестовая, участников должно быть немного… Но нет. В сети Rinkbey используется Proof-of-Authority консенсус, поэтому майнят только избранные ноды и раздают полученый эфир всем желающим. Так что один из вариантов был — наплодить аккаунтов в Twitter, Google+, github.com и собрать необходимое количество эфира. По расчетам, имея чуть более сотни аккаунтов, можно было собрать такое количество за сутки. Если кто-то из участников, читающих разбор, прибегал к такому решению, — отпишитесь в комментах, нам интересен ваш опыт.  Те, кому такой вариант показался скучным, могли заметить в описании (или контракте), что есть некая лотерея, которая каждые пять блоков предоставляет возможность любому подать заявку. Все, что нужно, — угадать число, которое загадал лотерейный робот.
Функция смарт-контракта, которую вызывал робот, выглядит так: Предполагалось, что участники в течение пяти блоков отправляют свои числа, а после этого робот присылает то, которое загадал он. Смарт-контракт должен был проверить, угадал ли кто-то из игроков. В случае успеха участник отправлялся в desires. Казалось бы, с точки зрения смарт-контракта нет никаких уязвимостей: робот своей транзакцией закрывал раунд и подсмотреть, какое число было загадано, можно было только постфактум. Но на самом деле нет. Для того, чтобы узнать, какое число загадал робот, и — самое главное — послать транзакцию раньше него, нужно было понимать, как эти транзакции обрабатываются сетью. Если коротко:
все новые транзакции сначала попадают в пул неподтвержденных (общий для всех участников сети), и майнеры, при формировании очередного блока, набирают себе транзакций именно оттуда. Но не в том порядке, как они были присланы, а в по убыванию комиссии и порядкового номера транзакции — gasPrice и nonce соответственно (на самом деле, цена "газа" — это только одна из составляющих комиссии, которую получает майнер; вторая — это сам потраченый газ).
Таким образом, все, что нужно было — это посмотреть на число, которое отправил робот, пока транзакция еще находилась в пуле неподтвержденных, и отправить следом свою с этим же числом, но большей ценой "газа". Принимая во внимание, что нахождение нового блока занимает 12-30 секунд, — у атакующего было достаточно времени, чтобы провернуть Front-running attack. Пример эксплоита можно изучить тут. (Лирическое отступление) Если бы в конкурсе участвовал какой-нибудь Интернет-провайдер, имеющий возможность провести BGP hijacking атаки, по аналогии с описанными здесь, то он так же мог бы управлять порядком транзакций. Итак, заявка подана. Остается только ждать, пока владелец внесет меня в белый список и я смогу купить заветные HACK-коины. Звучит скучно, не правда ли? Может, есть способ как-то еще попасть в белый список? Смотрим в контракт: К функции addParticipant, которая вызывается при нажатии на одноименную кнопку в веб-интерфейсе, применен модификатор onlyController поэтому, чтобы ее вызвать, надо подписать транзакцию приватным ключом адреса controller. А может, за неимением такового, заменить самого владельца? Изучая исходники одного из наследуемых контрактов, — Controlled — можно заметить, что предусмотрена смена владельца через функцию changeController: На самом деле, функция актуальна только для смарт-контакта HACK-коина (чтобы при деплое изменить controller с адреса разработчика на адрес контракта ICO), но, поскольку контракт Controlled полезен и наследуется обоими контрактами, то и changeOwner будет у обоих. Пробуем? Неудача :( Разработчик предусмотрел это и вызвал функцию сразу при деплое со своим же адресом.  После того, как были исчерпаны все теории, могло и вправду показаться, что владелец заносит участников с whitelist вручную. Но это, конечно же, не так. Для прохождения второго этапа необходимо было провести blockchain stored XSS-атаку против владельца. Намек на это можно было увидеть в функции изменения email: Заметили? Никаких проверок на то, что содержится в строке _email нет. Нет их главным образом потому, что делать такие проверки дорого (потраченый gas), да и никаких встроеных функций для работы со строками нет. Выходит, единственный барьер защиты будет, скорее всего, реализован на клиентской стороне. Взглянем, как реализовано добавление email на страницу Statistic: Конечно же, участники видели уже отрендеренный вариант. Но ничего не мешало экспериментировать: С точки зрения соревнования, также важно приходить уже с рабочим вектором атаки, поскольку в блокчейне все могут увидеть действия соперников! Вот первый присланный вектор: Полезная нагрузка в большинстве случаев подтягивалась отдельно (и я даже не пытался ходить по этим ссылкам и смотреть, что там), но вот авторский пример: Стоит также отметить, что атака удалась потому, что владелец использует клиент geth с разлоченным аккаунтом coinbase. Если бы применялся какой-то кошелек, то при инициации транзакции пользователю высветилось бы окошко, требующее подтверждение транзакции. Однако, не стоит считать, что использование кошелька спасет от всех бед. Атакующий все еще может управлять данными, из которых формируется транзакция (например, подменить адрес, выбранный владельцем на свой).  Ну что ж, мы почти у цели. Пора покупать 31337 HACK-коинов. Смотрим функцию покупки: С ходу видно, что смарт-контракт не позволяет приобрести больше 1000 HACK-коинов, а нужно более 31337. Однако не беда! Обратите внимание, как происходит проверка баланса покупателя. Учитывается только текущий баланс! Логичным решением будет просто перевести коины куда-то еще и купить снова. Смотрим, как можно сделать перевод: Функция-то есть для перевода, но к ней приведен модификатор, который должен ограничить возможность ее вызова до окончания ICO. Однако, на самом деле модификатор не выполняет своей функции вне зависимости от условия, поскольку это самое условие еще нужно обработать. Вот правильный вариант: Таким образом, можно повторить операцию "покупка-вывод" 13 раз и накопить заветное количество токенов. Другой вариант — применить функцию transferFrom — процесс немного более сложный, но тоже рабочий. Этап бонусный, потому что не задумывался как этап вовсе, но многих заставил серьезно погуглить, и после прохождения прислать эмоциональный отзыв вместе с флагом (в рамках приличия, конечно же). Итак, подпись к форме на сайте гласит: Для передачи флага требовалось именно off-chain взаимодействие с участниками (то есть вне сети Ethereum). Поскольку приглашение можно было получить именно в обмен на флаг (секрет), а хранение секретов в блокчейне дело непростое — даже если зашифровать флаг, то как отдать правильному участнику ключ? Собственно, поэтому мы просили участника сгенерировать подписанную транзакцию с того адреса, на котором имеется нужное количество HACK-коинов и отправить ее на бекенд ico.dsec.ru, а не в сеть. Вот подробный пример, как можно сгенерировать подобную транзакцию. Вот и все. Спасибо всем, кто принял участие в ICO и наши поздравления победителям! Для тех, у кого проснулось желание пройти квест — он поработает еще пару дней :) Так же огромное спасибо тем людям, которые перед ZeroNights нашли время и помогли мне. Pentester, Security researcher. @_p4lex