Вчера хакер совершил второе по величине ограбление в истории цифровых валют.

Около 12:00 по тихоокеанскому стандартному времени неизвестный злоумышленник воспользовался критической уязвимостью в кошельке с несколькими подписями Parity в сети Ethereum, опустошив три огромных кошелька на сумму более 31000000 долларов Эфира за считанные минуты. Через пару часов хакер мог бы скрыться с более чем 180 миллионами долларов из уязвимых кошельков.

Но кто-то их остановил.

Забив тревогу, быстро организовалась группа доброжелательных хакеров в белых шляпах из сообщества Ethereum. Они проанализировали атаку и поняли, что предотвратить кражу невозможно, но гораздо больше кошельков оказались уязвимыми. Время имело значение, поэтому они увидели только один доступный вариант: взломать оставшиеся кошельки до того, как это сделает злоумышленник.

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

Да, вы правильно прочитали.

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

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

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

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

  1. Что именно произошло? Объяснение Ethereum, смарт-контрактов и кошельков с несколькими подписями.
  2. Как они это сделали? Техническое объяснение атаки (специально для программистов).
  3. Что теперь? Последствия атаки для будущего и безопасности смарт-контрактов.

Если вы знакомы с Ethereum и криптовалютным миром, вы можете перейти ко второму разделу.

1. Что именно произошло?

Эта история состоит из трех строительных блоков: Ethereum, смарт-контракты и цифровые кошельки.

Ethereum - это цифровая валюта, изобретенная в 2013 году, то есть через 4 года после выпуска биткойнов. С тех пор он стал второй по величине цифровой валютой в мире по рыночной капитализации - 20 миллиардов долларов по сравнению с 40 миллиардами биткойнов.

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

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

Другими словами, Ethereum - это буквально компьютер, охватывающий весь мир. Любой, кто запускает программное обеспечение Ethereum на своем компьютере, участвует в операциях этого мирового компьютера, виртуальной машины Ethereum (EVM). Поскольку EVM был разработан как полный по Тьюрингу (без учета ограничений по газу), он может делать почти все, что можно выразить в компьютерной программе.

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

Сообщество разработчиков сплотилось за этим, и есть много энтузиазма по поводу того, что можно построить на основе EVM - и это подводит нас к смарт-контрактам.

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

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

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

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

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

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

Это тип кошелька, который атаковал хакер.

Так что же пошло не так? Они сломали закрытые ключи? Использовали ли они квантовый компьютер или какой-то передовой алгоритм факторинга?

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

2. Как это произошло?

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

Ethereum имеет довольно уникальную модель программирования. В Ethereum вы пишете код, публикуя контракты (которые можно рассматривать как объекты), а транзакции выполняются путем вызова методов этих объектов для изменения их состояния.

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

Один из способов снизить затраты - использовать библиотеки. Сделав вызов вашего контракта в общую библиотеку, которая была развернута в предыдущий раз, вам не нужно повторно развертывать какой-либо общий код. В Ethereum хранение кода в сухом состоянии напрямую сэкономит вам деньги.

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

// FIELDS
address constant _walletLibrary = 0xa657491c1e7f16adb39b9b60e87bbb8d93988bc3;

Библиотека вызывается в нескольких местах с помощью инструкции EVM под названием DELEGATECALL, которая выполняет следующие действия: для любого метода, вызывающего DELEGATECALL, он будет вызывать тот же метод в контракте, которому вы делегируете, но с использованием контекста текущего контракта. . По сути, это похоже на вызов super, за исключением части наследования. (Эквивалент в JavaScript будет OtherClass.functionName.apply(this, args).)

Вот пример этого в их кошельке с несколькими подписями: метод isOwner просто делегирует isOwner методу библиотеки общего кошелька, используя текущее состояние контракта:

function isOwner(address _addr) constant returns (bool) {  
  return _walletLibrary.delegatecall(msg.data);
}

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

Но они сделали одну критическую ошибку.

Solidity позволяет вам определить «запасной метод». Это метод, который вызывается, когда нет метода, соответствующего заданному имени метода. Вы определяете его, не давая ему имени:

function() {  
  // do stuff here for all unknown methods
}

Команда Parity решила позволить любому неизвестному методу, который отправил эфир в контракт, просто по умолчанию депонировать отправленный эфир.

function() payable {  
  // payable is just a keyword that means this method can receive/pay Ether
if (msg.value > 0) {
    // just being sent some cash?
    Deposit(msg.sender, msg.value);  
  } else {
    throw;
  }
}

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

function() payable {  
  // just being sent some cash?
  if (msg.value > 0)
    Deposit(msg.sender, msg.value);
  else if (msg.data.length > 0)
    _walletLibrary.delegatecall(msg.data);
}

По сути:

  • Если имя метода не определено в этом контракте…
  • И в транзакции нет эфира ...
  • И в полезной нагрузке сообщения есть некоторые данные ...

Затем он вызовет тот же метод, если он определен в _walletLibrary, но в контексте этого контракта.

Используя это, злоумышленник вызвал метод с именем initWallet(), который не был определен в контракте с несколькими подписями , но был определен в библиотеке общего кошелька:

function initWallet(address[] _owners, uint _required, uint _daylimit) {  
  initDaylimit(_daylimit);
  initMultiowned(_owners, _required);
}

Что вызывает метод initMultiowned ...

function initMultiowned(address[] _owners, uint _required) {  
  m_numOwners = _owners.length + 1;
  m_owners[1] = uint(msg.sender);
  m_ownerIndex[uint(msg.sender)] = 1;
  for (uint i = 0; i < _owners.length; ++i)
  {
    m_owners[2 + i] = uint(_owners[i]);
    m_ownerIndex[uint(_owners[i])] = 2 + i;
  }
  m_required = _required;
}

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

Учитывая, что теперь они контролируют весь кошелек, они могут легко извлечь оставшуюся часть баланса. Именно это они и сделали.

InitWallet: https://etherscan.io/tx/0x707aabc2f24d756480330b75fb4890ef6b8a26ce0554ec80e3d8ab105e63db07

Перевод:
https://etherscan.io/tx/0x9654a93939e98ce84f09038b9855b099da38863b3c2e0e04fd59a540de1cb1e5

Так что же в конечном итоге было уязвимостью? Вы можете поспорить, что их было двое. Во-первых, initWallet и initMultiowned в библиотеке кошелька не были помечены как internal (это похоже на метод private, который предотвратил бы этот делегированный вызов), и эти методы не проверяли, что кошелек еще не был инициализирован. Любая проверка сделала бы этот взлом невозможным.

Второй уязвимостью была необработанная delegateCall. Вы можете думать об этом как об эквиваленте необработанного оператора eval, работающего с введенной пользователем строкой. В попытке быть кратким, этот контракт использовал метапрограммирование для проксирования потенциальных вызовов методов в базовую библиотеку. Более безопасным подходом здесь было бы занесение в белый список определенных методов, которые пользователю разрешено вызывать.

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

Итак, это была атака.

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

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

Вы можете задать вопрос - почему бы им просто не откатить этот хак, как это было с хаком DAO?

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

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

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

Таким образом, транзакция не будет отменена. Убыток в 31 миллион долларов остается в силе. Это дорогостоящий, но необходимый урок.

Итак, что мы должны извлечь из этого?

3. Что эта атака означает для Ethereum?

Здесь есть несколько важных выводов.

Во-первых, помните, это не было недостатком Ethereum или смарт-контрактов в целом. Скорее всего, это была ошибка разработчика в конкретном контракте.

Так кто же были те сумасшедшие разработчики, которые написали это? Им следовало бы знать лучше, правда?

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

Эти разработчики были людьми. Они ошиблись. То же самое сделали и рецензенты, проверявшие этот код.

Я читал несколько комментариев на Reddit и HackerNews примерно следующего содержания: «Какая очевидная ошибка! Как это вообще могло быть возможно, что они это пропустили? » (Игнорируя, что «очевидная» уязвимость была введена в январе и обнаружена только сейчас.)

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

Ошибки такого рода обычно допускаются в программировании. Все программы несут в себе риск ошибки разработчика. Мы должны отказаться от мышления, что «если бы они были просто более осторожными, этого бы не произошло». При определенном масштабе осторожности недостаточно.

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

Было бы хорошо, если бы мы взяли страницу из практики надежности сайтов таких компаний, как Google и Airbnb. Каждый раз, когда возникает производственная ошибка или сбой, они проводят посмертный анализ и распространяют его внутри компании. В этих вскрытиях всегда присутствует принцип никогда не обвинять людей.

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

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

Проблема в том, что их набор инструментов программирования позволял им совершать эти ошибки.

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

Это подводит меня к следующему пункту.

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

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

Чем меньше позволяет язык, тем проще анализировать и доказывать свойства контракта. Безопасность - это сложно, потому что единственный способ доказать положительное утверждение типа «этот контракт безопасен» - это опровергнуть все возможные векторы атаки: «этот контракт не может быть повторно инициализирован», «его средства не могут быть доступны кроме владельцев» и т. Д. • Чем меньше возможных векторов атаки вам придется учитывать, тем проще разработать безопасный контракт.

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

Здесь тоже есть более важный урок.

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

Проблема в том, что программирование на блокчейне принципиально отличается от веб-разработки.

Позволь мне объяснить.

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

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

Эти две модели разработки принципиально различаются. Только благодаря веб-разработке можно получить девиз: "Двигайся быстро и ломай все".

Большинство программистов сегодня обучаются модели веб-разработки. К сожалению, модель безопасности блокчейна больше похожа на более старую модель.

В блокчейне код по сути необратим. После того как вы развернете плохой смарт-контракт, любой сможет атаковать его так долго и жестко, как только сможет, и нет никакого способа вернуть его, если они доберутся до него первыми. Если вы не встроите в свои контракты интеллектуальные механизмы безопасности, в случае ошибки или успешной атаки невозможно отключить серверы и исправить ошибку. Находиться на Ethereum по определению означает, что каждый владеет вашим сервером.

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

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

В случае удачной атаки защита крайне затруднена. «Белые шляпы» из взлома Parity продемонстрировали, насколько ограничены их возможности защиты - не было возможности обеспечить или разорвать контракты или даже вернуть украденные деньги; все, что они могли сделать, это взломать оставшиеся уязвимые контракты до того, как это сделает злоумышленник.

Может показаться, что это означает темное будущее.

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

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

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

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

Ethereum не выживет и не умрет из-за денег в нем. Он будет жить или умереть в зависимости от разработчиков, которые за него борются.

Лига белых шляп, объединившаяся для защиты уязвимых кошельков, сделала это не ради денег. Они сделали это, потому что верят в эту экосистему. Они хотят, чтобы Ethereum процветал. Они хотят, чтобы их видение будущего сбылось. И после всех спекуляций и спекуляций именно эти люди в конечном итоге будут вести сообщество в его будущее. Они принципиально объясняют, почему Эфириум в конечном итоге выиграет - или, если они откажутся от Эфириума, их отказ станет причиной его проигрыша.

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

Но эта атака не пошатнула сил строителей, работающих над этим материалом. Так что в этом смысле это временная неудача.

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

P.S. Если вы разработчик и хотите узнать больше о безопасности смарт-контрактов, это действительно хороший ресурс.

Исправление: в этой статье изначально говорилось, что разработчиком контракта был Гэвин Вуд, что неверно. Гэвин является основателем Parity и внес исправление в контракт, но не был первоначальным разработчиком. Также первоначально утверждалось, что дополнительные средства в размере 77 миллионов долларов были уязвимы, но это не учитывает все токены ERC20 (ICO), которые были уязвимы. Общая сумма на самом деле составляет 150 000 000 долларов США, если включить все токены ERC20. На момент написания этой статьи (21 июля, 16:00 по восточному стандартному времени) общая стоимость активов, сэкономленных белыми шляпами, составляла 179 704 659 долларов.