BNBChain跨链桥攻击事件已过去10天了,网上的分析和讨论也非常多,大家对漏洞的产生和修复非常关心。
但是,我们发现99%的开发者只理解漏洞产生是由于CosmosSDK中IAVL+树叶子节点的Hash算法存在漏洞,但并不知道攻击者到底是如何构造payload(inputdata)的,实现这样的攻击的难度到底有多大,选择很久之前的这个区块高度(110217401)到底是什么原因?
带着这些问题,SharkTeam高保真还原了黑客的漏洞挖掘过程和payload构造逻辑,让我们来一探究竟!
(资料图)
一、交易分析
txHash:0xebf83628ba893d35b496121fb8201666b8e09f3cbadf0e269162baa72efe3b8b
访问的合约是BNBChain中的CrossChain合约,地址是0x2000,调用的函数以及参数如下:
参数解析:
1. payload:有效负载,是跨链交易的包信息,其中包含了调用BNB转账的函数及其参数,长度为147bytes。
2. proof:跨链交易的包的Merkle证明,包含了IVAL树的范围证明以及MultiStore存储的简单Merkle树证明,长度为999bytes。
3. height:区块高度,读取跨链交易的来源链高度为height的区块中的appHash,实际是前一个区块的appHash,用于验证MultiStore存储的简单Merkle树证明。
4. packageSequence:跨链交易的包序列号,其特性类似于区块的Nonce。
5. channelId:跨链通道ID,指明跨链交易的来源链与目标链。这里,channelId=2表示从BC链到BSC链的跨链转账交易。
二、合约分析
CrossChain合约如下:
我们通过整个执行过程,将handlePackage函数分为6个关键函数,下面依次分析这6个关键函数的逻辑、参数以及返回值。
2.1 validateMerkleProof函数
该函数的功能就是验证Merkle证明,函数名称和参数定义如下:
传递的参数如下:
{
"appHash": "0x72cda827a83531ca0fd7ac917a6b65649719aab0836722caafe0603147a52318",
"storeName": "ibc",
"key": "0x00000100380200000000010dd85c",
"value": "0x000000000000000000000000000000000000000000000000000000000000000000f870a0424e4200000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000008ad3c21bcecceda100000094489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec94489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec846553f100",
"proof": "0x0a8d020a066961766c3a76120e00000100380200000000010dd85c1af201f0010aed010a2b0802100318b091c73422200c10f902d266c238a4ca9e26fa9bc36483cd3ebee4e263012f5e7f40c22ee4d20a4d0801100218b091c7342220e4fd47bffd1c06e67edad92b2bf9ca63631978676288a2aa99f95c459436ef632a20da657c1ffb86c684eb3e265361ef0fa4f9dfa670b45f9f91c5eb6ad84b21a4d112001a370a0e0000010038020000000000000002122011056c6919f02d966991c10721684a8d1542e44003f9ffb47032c18995d4ac7f18b091c7341a340a0e00000100380200000000010dd85c12202c3a561458f8527b002b5ec3cab2d308662798d6245d4588a4e6a80ebdfe30ac18010ad4050a0a6d756c746973746f726512036962631ac005be050abb050a110a066f7261636c6512070a0508b891c7340a0f0a046d61696e12070a0508b891c7340a350a08736c617368696e6712290a2708b891c7341220c8ccf341e6e695e7e1cb0ce4bf347eea0cc16947d8b4e934ec400b57c59d6f860a380a0b61746f6d69635f7377617012290a2708b891c734122042d4ecc9468f71a70288a95d46564bfcaf2c9f811051dcc5593dbef152976b010a110a0662726964676512070a0508b891c7340a300a0364657812290a2708b891c73412201773be443c27f61075cecdc050ce22eb4990c54679089e90afdc4e0e88182a230a2f0a02736312290a2708b891c7341220df7a0484b7244f76861b1642cfb7a61d923794bd2e076c8dbd05fc4ee29f3a670a330a06746f6b656e7312290a2708b891c734122064958c2f76fec1fa5d1828296e51264c259fa264f499724795a740f48fc4731b0a320a057374616b6512290a2708b891c734122015d2c302143bdf029d58fe381cc3b54cedf77ecb8834dfc5dc3e1555d68f19ab0a330a06706172616d7312290a2708b891c734122050abddcb7c115123a5a4247613ab39e6ba935a3d4f4b9123c4fedfa0895c040a0a300a0361636312290a2708b891c734122079fb5aecc4a9b87e56231103affa5e515a1bdf3d0366490a73e087980b7f1f260a0e0a0376616c12070a0508b891c7340a300a0369626312290a2708b891c7341220e09159530585455058cf1785f411ea44230f39334e6e0f6a3c54dbf069df2b620a300a03676f7612290a2708b891c7341220db85ddd37470983b14186e975a175dfb0bf301b43de685ced0aef18d28b4e0420a320a05706169727312290a2708b891c7341220a78b556bc9e73d86b4c63ceaf146db71b12ac80e4c10dd0ce6eb09c99b0c7cfe0a360a0974696d655f6c6f636b12290a2708b891c73412204775dbe01d41cab018c21ba5c2af94720e4d7119baf693670e70a40ba2a52143"
}
以上参数解读如下:
(1)appHash通过调用TendermintLightClient合约中的getAppHash函数来读取的高度为height的AppHash,getAppHash函数如下:
状态变量lightClientConsensusStates是由跨链中继器Relayer调用syncTendermintHeader函数同步每一个区块头部信息的时候进行更新的,其中保存了每一个BC链中区块的appHash。
AppHash是区块头部的一个字段,代表前一个区块执行完成后的应用状态散列值。
摘自:《区块链架构与实现:Cosmos详解》
由此可见,参数appHash是BC链高度为110217400的区块的AppHash(参数height为110217401),其值为0x72cda827a83531ca0fd7ac917a6b65649719aab0836722caafe0603147a52318
(2)参数storeName=”ibc”,是合约定义的常量,转换成十六进制为0x696263
(3)参数key是调用generateKey函数生成的,代码如下:
参数key的值长度为14bytes,由两部分组成:
前6 bytes由通道ID,即channelId生成,其值为0x000001003802,按顺序包含:
长度(/字节) | 字段 | 值 |
1 | 前缀 | 0x00 |
2 | 来源链ID | 0x0001 |
2 | 目标链ID | 0x0038 |
1 | 通道ID | 0x02 |
后8 bytes是包序列号packageSequence,即17684572=0x00000000010dd85c
所以,这里key=0x00000100380200000000010dd85c,length=14=0x0e
(4)参数payloadLocal就是交易InputData中的payload,长度为147bytes
(5)参数proofLocal就是交易InputData中的proof,长度为999bytes
validateMerkleProof函数的实现代码如下:
该函数调用了0x65地址的合约对proof进行验证,参数如下:
(1)input=0x00000000000000000000000000000000000000000000000000000000000005086962630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000100380200000000010dd85c0000000000000000000000000000000000000000000000000000000000000093000000000000000000000000000000000000000000000000000000000000000000f870a0424e4200000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000008ad3c21bcecceda100000094489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec94489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec846553f10072cda827a83531ca0fd7ac917a6b65649719aab0836722caafe0603147a523180a8d020a066961766c3a76120e00000100380200000000010dd85c1af201f0010aed010a2b0802100318b091c73422200c10f902d266c238a4ca9e26fa9bc36483cd3ebee4e263012f5e7f40c22ee4d20a4d0801100218b091c7342220e4fd47bffd1c06e67edad92b2bf9ca63631978676288a2aa99f95c459436ef632a20da657c1ffb86c684eb3e265361ef0fa4f9dfa670b45f9f91c5eb6ad84b21a4d112001a370a0e0000010038020000000000000002122011056c6919f02d966991c10721684a8d1542e44003f9ffb47032c18995d4ac7f18b091c7341a340a0e00000100380200000000010dd85c12202c3a561458f8527b002b5ec3cab2d308662798d6245d4588a4e6a80ebdfe30ac18010ad4050a0a6d756c746973746f726512036962631ac005be050abb050a110a066f7261636c6512070a0508b891c7340a0f0a046d61696e12070a0508b891c7340a350a08736c617368696e6712290a2708b891c7341220c8ccf341e6e695e7e1cb0ce4bf347eea0cc16947d8b4e934ec400b57c59d6f860a380a0b61746f6d69635f7377617012290a2708b891c734122042d4ecc9468f71a70288a95d46564bfcaf2c9f811051dcc5593dbef152976b010a110a0662726964676512070a0508b891c7340a300a0364657812290a2708b891c73412201773be443c27f61075cecdc050ce22eb4990c54679089e90afdc4e0e88182a230a2f0a02736312290a2708b891c7341220df7a0484b7244f76861b1642cfb7a61d923794bd2e076c8dbd05fc4ee29f3a670a330a06746f6b656e7312290a2708b891c734122064958c2f76fec1fa5d1828296e51264c259fa264f499724795a740f48fc4731b0a320a057374616b6512290a2708b891c734122015d2c302143bdf029d58fe381cc3b54cedf77ecb8834dfc5dc3e1555d68f19ab0a330a06706172616d7312290a2708b891c734122050abddcb7c115123a5a4247613ab39e6ba935a3d4f4b9123c4fedfa0895c040a0a300a0361636312290a2708b891c734122079fb5aecc4a9b87e56231103affa5e515a1bdf3d0366490a73e087980b7f1f260a0e0a0376616c12070a0508b891c7340a300a0369626312290a2708b891c7341220e09159530585455058cf1785f411ea44230f39334e6e0f6a3c54dbf069df2b620a300a03676f7612290a2708b891c7341220db85ddd37470983b14186e975a175dfb0bf301b43de685ced0aef18d28b4e0420a320a05706169727312290a2708b891c7341220a78b556bc9e73d86b4c63ceaf146db71b12ac80e4c10dd0ce6eb09c99b0c7cfe0a360a0974696d655f6c6f636b12290a2708b891c73412204775dbe01d41cab018c21ba5c2af94720e4d7119baf693670e70a40ba2a52143
(2)length=1260=0x508=32+1228
input的实际长度为1260bytes,但实际的参数长度是1228,之所以length=1260,是因为input的第一个字节中保存了实际参数的长度1228=0x508.
结果输出到result中,其值为0x01。最终,proof验证通过,返回true。
2.2 getSubmitter函数
状态变量submitters由跨链中继器Relayer调用syncTendermintHeader函数同步每一个区块头部信息的时候进行更新。其值为msg.sender,即中继器Relayer的地址:
0xb005741528b86f5952469d80a8614591e3c5b632。
2.3 decodePayloadHeader函数
payload=0x000000000000000000000000000000000000000000000000000000000000000000f870a0424e4200000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000008ad3c21bcecceda100000094489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec94489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec846553f100
其中,包括:
lpackageType=0x0,长度为1byte
lrelayFee=0x0,长度为32bytes
lpackage=0xf870a0424e4200000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000008ad3c21bcecceda100000094489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec94489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec846553f100,长度为114bytes
2.4 handleSynPackage函数
这里的参数如下:
lchannelId=0x02
lmsgBytes=0xf870a0424e4200000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000008ad3c21bcecceda100000094489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec94489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec846553f100
其中,handleTransferInSynPackage函数如下:
2.5 addReward函数
该函数的功能是在跨链业务完成后为中继器Relayer分发奖励金。
3公链分析
在合约中验证Merkle证明的时候调用了0x65地址处的预编译合约。
我们查看了VM中0x65处的预编译合约,如下:
预编译合约执行入口如下:
参数解析:
lp:地址0x65处的iavlMerkleProofValidate
linput:由validateMerkleProof函数传递的input,长度是32+1228=1260bytes,前32bytes是input实际参数的长度,即1228=0x508
input=0x00000000000000000000000000000000000000000000000000000000000005086962630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000100380200000000010dd85c0000000000000000000000000000000000000000000000000000000000000093000000000000000000000000000000000000000000000000000000000000000000f870a0424e4200000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000008ad3c21bcecceda100000094489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec94489a8756c18c0b8b24ec2a2b9ff3d4d447f79bec846553f10072cda827a83531ca0fd7ac917a6b65649719aab0836722caafe0603147a523180a8d020a066961766c3a76120e00000100380200000000010dd85c1af201f0010aed010a2b0802100318b091c73422200c10f902d266c238a4ca9e26fa9bc36483cd3ebee4e263012f5e7f40c22ee4d20a4d0801100218b091c7342220e4fd47bffd1c06e67edad92b2bf9ca63631978676288a2aa99f95c459436ef632a20da657c1ffb86c684eb3e265361ef0fa4f9dfa670b45f9f91c5eb6ad84b21a4d112001a370a0e0000010038020000000000000002122011056c6919f02d966991c10721684a8d1542e44003f9ffb47032c18995d4ac7f18b091c7341a340a0e00000100380200000000010dd85c12202c3a561458f8527b002b5ec3cab2d308662798d6245d4588a4e6a80ebdfe30ac18010ad4050a0a6d756c746973746f726512036962631ac005be050abb050a110a066f7261636c6512070a0508b891c7340a0f0a046d61696e12070a0508b891c7340a350a08736c617368696e6712290a2708b891c7341220c8ccf341e6e695e7e1cb0ce4bf347eea0cc16947d8b4e934ec400b57c59d6f860a380a0b61746f6d69635f7377617012290a2708b891c734122042d4ecc9468f71a70288a95d46564bfcaf2c9f811051dcc5593dbef152976b010a110a0662726964676512070a0508b891c7340a300a0364657812290a2708b891c73412201773be443c27f61075cecdc050ce22eb4990c54679089e90afdc4e0e88182a230a2f0a02736312290a2708b891c7341220df7a0484b7244f76861b1642cfb7a61d923794bd2e076c8dbd05fc4ee29f3a670a330a06746f6b656e7312290a2708b891c734122064958c2f76fec1fa5d1828296e51264c259fa264f499724795a740f48fc4731b0a320a057374616b6512290a2708b891c734122015d2c302143bdf029d58fe381cc3b54cedf77ecb8834dfc5dc3e1555d68f19ab0a330a06706172616d7312290a2708b891c734122050abddcb7c115123a5a4247613ab39e6ba935a3d4f4b9123c4fedfa0895c040a0a300a0361636312290a2708b891c734122079fb5aecc4a9b87e56231103affa5e515a1bdf3d0366490a73e087980b7f1f260a0e0a0376616c12070a0508b891c7340a300a0369626312290a2708b891c7341220e09159530585455058cf1785f411ea44230f39334e6e0f6a3c54dbf069df2b620a300a03676f7612290a2708b891c7341220db85ddd37470983b14186e975a175dfb0bf301b43de685ced0aef18d28b4e0420a320a05706169727312290a2708b891c7341220a78b556bc9e73d86b4c63ceaf146db71b12ac80e4c10dd0ce6eb09c99b0c7cfe0a360a0974696d655f6c6f636b12290a2708b891c73412204775dbe01d41cab018c21ba5c2af94720e4d7119baf693670e70a40ba2a52143
iavlMerkleProofValidate的Run方法如下:
解析出的kvmp结构如下:
然后,调用kvmp的validate方法:
其中prt和kp的结构如下:
调用prt的VerifyValue方法:
其中poz结构如下:
poz包含了2个Op,即IAVLValueOP和MultiStoreOp
调用poz的Verify方法,如下:
调用op中的Run方法。poz中有2个op,即IVALValueOp和MultiStoreOp。因此,这里先后调用了IVALValueOp和MultiStoreOp的Run方法,即对Merkle证明进行验证。
3.1 IVALValueOp验证
首先调用的是IVALValueOp的Run方法,如下:
这里的参数args只有一个元素args[0],是参数payload,长度为147字节。返回的是Merkle证明中的rootHash。
这里的op如下:
其中的Proof类型是RangeProof,结构定义如下:
RangeProof结构字段说明:
因此,proofInnerNode结构中的Left和Right必须有一个是nil,而另外一个不是nil。
以下面的IAVL+树为例:
说明:字母代表节点,数字代表key,公有8个叶子节点。
根据IAVL+树的特性,对于其中的任意中间节点node,满足条件:
node.key
比如,若要构造节点J的RangeProof,证明路径如下:
Key=2(节点J)时,PathToLeaf为{A, B,E}。其中,每一个节点的Left和Right如下:
lLeft=nil,A.Right=C.key
lB.Left=D.key, B.Right=nil
lE.Left=nil, E,Right=K.key(E.Left实际上是节点J)
这里Proof的值如下:
Proof中,LeftToPath有2个LeafToPath,0个InnerNodes,2个Leaves。同时还发现,LeftToPath[1]的Left和Right都不是nil。另外,经计算,发现LeftToPath[1].Right实际就是leaves[1],即参数传入的key,整个叶子的Hash值为:
0xda657c1ffb86c684eb3e265361ef0fa4f9dfa670b45f9f91c5eb6ad84b21a4d1
Run方法中调用了3个重要方法:
(1)RangeProof的ComputeRootHash方法,即根据proof计算Merkle证明的rootHash
其中pathWithLeaf的computeRootHash方法如下:
执行到这一步时,pin的值如下:
接下来调用proofInnerNode的Hash方法,如下:
这里的Hash方法是根据pin的Left和Right计算中间节点的Hash。根据上面介绍的,这里Left和Right必须一个是nil,另外一个不是nil。但实际代码中,在Left不是nil的时候,并没有要求Right是nil,即这种情况下的Right的值不参与Hash值的计算,也就对Hash没有影响。
这里的Hash实际是有pin.Left和leaves[0]计算出来的,pin的Right应该是nil的,但实际不是。但也因此,计算的Hash是正确的。
从整个proof,我们可以构造出相应的IAVL+树,如下:
LeftPath[1].Right应该是nil,实际是leaves[0],攻击者将其改为了leaves[1],由于Hash函数计算漏洞,这并不影响Hash的计算。
将计算的rootHash添加到proof的rootHash字段,并返回rootHash。
(2)RangeProof的Verify方法
该方法的功能是验证上一步中是否计算并设置了proof的rootHash字段,验证通过后,将proof的rootVerified设置为true。
(3)RangeProof的VerifyItem方法
该方法用于验证参数中提供的key和payload在proof的leaves中,即参数中的跨链包是MerkleProof的叶子节点,并且其ValueHash与MerkleProof中相应叶子节点的ValueHash相同。
IAVLValueOp验证了模块的MerkleProof,即验证了从特定功能模块(“ibc”)的IAVL+树根到目标叶子节点的Merkle证明。
3.2 MultiStoreOp验证
根据poz中op的顺序,在完成IAVLValueOp的验证后,就会进行MultiStoreOp的验证。
在执行完IAVLValueOp的Run方法后,其返回值为iavl proof中的rootHash,因此,此时args有原来的payload变为iavlRootHash。然后执行MultiStore的Run方法:
这里,op的如下:
这里执行的是MultiStoreOp的Proof的ComputeRootHash方法:
计算结构以及返回值是appHash。
关于代码中的si.Core.CommitID.Hash,其中si指Proof.StoreInfos中的“ibc”模块。
那么si.Core.CommitID.Hash就是“ibc”模块的IAVL+树根值,即IVALValueOp验证中返回的iavlRootHash。
这一步验证appHash实现了从AppHash到特定功能模块(“ibc”)的IAVL+树根的Merkle证明的验证。
3.3 小结
以上两部分,分别验证了从特定功能模块的IAVL+树根到目标叶子节点的Merkle证明(IAVLValueOp验证)以及从AppHash到特定功能模块的IAVL+树根的Merkle证明(MultiStoreOp验证)。最终,参数提供的证明验证通过,返回0x01,代表true。
4总结
此次安全事件发生的根本原因是BNBChain底层使用的CosmosSDK中IAVL+树叶子节点的Hash算法存在漏洞。攻击者完全可以从区块链中查询到完整的Merkle树,然后利用漏洞进行篡改,然后生成可以通过验证的MerkleRangeProof,从而实现伪造证明的目的。
叶子节点Hash漏洞并不是在指定的条件下才存在,理论上,任意的IAVL+树都可以伪造出MerkleRangeProof。在伪造证明时,明显越简单的IAVL+树,越容易伪造证明。IAVL+树叶子越多,结构越复杂,伪造证明也就越困难。
此次事件,攻击者选择高度为110217401的区块,原因是这个区块对应的IAVL+树构造更简单,更容易实现伪造证明。这也是攻击者在成功交易之前,有多笔失败交易的原因。
关于我们:SharkTeam的愿景是全面保护Web3世界的安全。团队成员分布在北京、南京、苏州、硅谷,由来自世界各地的经验丰富的安全专业人士和高级研究人员组成,精通区块链和智能合约的底层理论,提供包括智能合约审计、链上分析、应急响应等服务。已与区块链生态系统各个领域的关键参与者,如Polkadot、Moonbeam、polygon、OKC、Huobi Global、imToken、ChainIDE等建立长期合作关系。