Skip to main content

WTF Solidity 合约安全: S02. 选择器碰撞

我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity 合约安全”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。

推特:@0xAA_Science@WTFAcademy_

社区:Discord微信群官网 wtf.academy

所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity


这一讲,我们将介绍选择器碰撞攻击,它是导致跨链桥 Poly Network 被黑的原因之一。在2021年8月,Poly Network在ETH,BSC,和Polygon上的跨链桥合约被盗,损失高达6.11亿美元(总结)。这是2021年最大的区块链黑客事件,也是历史被盗金额榜单上第2名,仅次于 Ronin 桥黑客事件。

选择器碰撞

以太坊智能合约中,函数选择器是函数签名 "<function name>(<function input types>)" 的哈希值的前4个字节(8位十六进制)。当用户调用合约的函数时,calldata的前4字节就是目标函数的选择器,决定了调用哪个函数。如果你不了解它,可以阅读WTF Solidity极简教程第29讲:函数选择器

由于函数选择器只有4字节,非常短,很容易被碰撞出来:即我们很容易找到两个不同的函数,但是他们有着相同的函数选择器。比如transferFrom(address,address,uint256)gasprice_bit_ether(int128)有着相同的选择器:0x23b872dd。当然你也可以写个脚本暴力破解。

大家可以用这两个网站来查同一个选择器对应的不同函数:

  1. https://www.4byte.directory/
  2. https://sig.eth.samczsun.com/

你也可以使用下面的Power Clash工具进行暴力破解:

  1. PowerClash: https://github.com/AmazingAng/power-clash

相比之下,钱包的公钥有64字节,被碰撞出来的概率几乎为0,非常安全。

0xAA 解决斯芬克斯之谜

以太坊的人得罪了天神,天神震怒。天后赫拉为了惩罚以太坊的人,在以太坊的峭崖上降下一个名叫斯芬克斯的人面狮身的女妖。她向每一个路过悬崖的以太坊用户提出一个谜语:“什么东西在早晨用四只脚走路,中午两只脚走路,晚间三只脚走路,在一切生物中这是唯一的用不同数目的脚走路的生物。脚最多的时候,正是速度和力量最小的时候。”对于这个奥妙费解的谜语,凡猜中者即可活命,凡猜不中者一律被吃掉。过路的人全被斯芬克斯吃了,以太坊用户陷入恐惧之中。斯芬克斯用选择器0x10cd2dc7来验证答案是否正确。

有一天上午,俄狄浦斯路过此地,会见了女妖,并猜中了这神秘奥妙之谜。他说:“这是"function man()"啊!在生命的早晨,他是个孩子,用两条腿和两只手爬行;到了生命的中午,他变成壮年,只用两条腿走路;到了生命的傍晚,他年老体衰,必须借助拐杖走路,所以被称为三只脚。”谜语被猜中后,俄狄浦斯得以生还。

那一天下午,0xAA路过此地,会见了女妖,并猜中了这神秘奥妙之谜。他说:“这是"function peopleLduohW(uint256)"啊!在生命的早晨,他是个孩子,用两条腿和两只手爬行;到了生命的中午,他变成壮年,只用两条腿走路;到了生命的傍晚,他年老体衰,必须借助拐杖走路,所以被称为三只脚。”谜语再次被猜中后,斯芬克斯气急败坏,脚下一打滑就从巍峨的峭崖上掉下去摔死了。

漏洞合约例子

漏洞合约

下面我们来看一下有漏洞的合约例子。SelectorClash合约有1个状态变量 solved,初始化为false,攻击者需要将它改为true。合约主要有2个函数,函数名沿用自 Poly Network 漏洞合约。

  1. putCurEpochConPubKeyBytes() :攻击者调用这个函数后,就可以将solved改为true,完成攻击。但是这个函数检查msg.sender == address(this),因此调用者必须为合约本身,我们需要看下其他函数。

  2. executeCrossChainTx() :通过它可以调用合约内的函数,但是函数参数的类型和目标函数不太一样:目标函数的参数为(bytes),而这里调用的函数参数为(bytes,bytes,uint64)

contract SelectorClash {
bool public solved; // 攻击是否成功

// 攻击者需要调用这个函数,但是调用者 msg.sender 必须是本合约。
function putCurEpochConPubKeyBytes(bytes memory _bytes) public {
require(msg.sender == address(this), "Not Owner");
solved = true;
}

// 有漏洞,攻击者可以通过改变 _method 变量碰撞函数选择器,调用目标函数并完成攻击。
function executeCrossChainTx(bytes memory _method, bytes memory _bytes, bytes memory _bytes1, uint64 _num) public returns(bool success){
(success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_bytes, _bytes1, _num)));
}
}

攻击方法

我们的目标是利用executeCrossChainTx()函数调用合约中的putCurEpochConPubKeyBytes(),目标函数的选择器为:0x41973cd9。观察到executeCrossChainTx()中是利用_method参数和"(bytes,bytes,uint64)"作为函数签名计算的选择器。因此,我们只需要选择恰当的_method,让这里算出的选择器等于0x41973cd9,通过选择器碰撞调用目标函数。

Poly Network黑客事件中,黑客碰撞出的_methodf1121318093,即f1121318093(bytes,bytes,uint64)的哈希前4位也是0x41973cd9,可以成功的调用函数。接下来我们要做的就是将f1121318093转换为bytes类型:0x6631313231333138303933,然后作为参数输入到executeCrossChainTx()中。executeCrossChainTx()函数另3个参数不重要,填 0x, 0x, 0 就可以。

Remix演示

  1. 部署SelectorClash合约。
  2. 调用executeCrossChainTx(),参数填0x66313132313331383039330x0x0,发起攻击。
  3. 查看solved变量的值,被修改为true,攻击成功。

总结

这一讲,我们介绍了选择器碰撞攻击,它是导致跨链桥 Poly Network 被黑 6.1 亿美金的的原因之一。这个攻击告诉了我们:

  1. 函数选择器很容易被碰撞,即使改变参数类型,依然能构造出具有相同选择器的函数。

  2. 管理好合约函数的权限,确保拥有特殊权限的合约的函数不能被用户调用。