区块链安全

math类型ctf-05

思路分析

1 pragma solidity ^0.4.21;
2
3 contract DonationChallenge {
4     struct Donation {
5         uint256 timestamp;
6         uint256 etherAmount;
7     }
8     Donation[] public donations;
9
10    address public owner;
11
12    function DonationChallenge() public payable {
13        require(msg.value == 1 ether);
14        
15        owner = msg.sender;
16    }
17    
18    function isComplete() public view returns (bool) {
19        return address(this).balance == 0;
20    }
21
22    function donate(uint256 etherAmount) public payable {
23        // amount is in ether, but msg.value is in wei
24        uint256 scale = 10**18 * 1 ether;
25        require(msg.value == etherAmount / scale);
26
27        Donation donation;
28        donation.timestamp = now;
29        donation.etherAmount = etherAmount;
30
31        donations.push(donation);
32    }
33
34    function withdraw() public {
35        require(msg.sender == owner);
36        
37        msg.sender.transfer(address(this).balance);
38    }
39 }

首先看到 isComplete() 的条件,是这个 DonationChallenge 合约的余额为 0 ether。但是在合约构造器函数 DonationChallenge() 中这个合约已经被转入了 1 ether。所以我们要做到的就是把这 1 ether 转出。

所以在全部代码中寻找 transfer 函数,发现只有第 37 行这一个将 DonationChallenge 合约的余额转给 msg.sender 的转账语句。但是要满足 msg.sender == owner,目前 owner 没有被赋值过,也没有关于这个变量的赋值函数,所以 owner 为 0x0。

所以很自然的联想到,这道题的思路就是要把 owner 的这个 slot 覆盖,值为传进去的 value。而这道题的难点就是数据结构复杂。

解题过程

storage 里面的存储布局应该是如下的:

这样看来,因为 timestamp 地方传入的永远是 now,我们无法操作。只有 etherAmount 的地方可控。但是跟上一题不一样的是,因为是通过 push 给动态数组 Donations 增加的元素,而不是通过指定 Donations[index] = value。所以无法通过 index 来改变 slot 的位置。也就是无法将 slot 地址的值上溢为 1。

这样的话,似乎我们是无法覆盖 slot1 的值的。但是此刻就需要利用 Solidity 低版本的一个特性了:

http://snowming.me/2021/09/27/320/

因为结构体在函数内非显式地初始化的时候会使用storage存储而不是memory,所以就可以达到变量覆盖的效果,关于这我也专门写过相关的文章,Solidity中存储方式错误使用所导致的变量覆盖,个人感觉写的还算清楚,这也是solidity的一个bug,官方是准备在0.5.0版本修复,不过看来是遥遥无期了

https://www.freebuf.com/articles/blockchain-articles/175237.html

所以其实上面我还犯了一个错误,就是结构体实例在函数中初始化的,其实是局部变量,不是状态变量。所以不应该存在 storage 里面。但又因为 Solidity 的存储方式的错误,导致会覆盖掉状态变量的 slot。

所以现在是这样一个情况。etherAmount 只要传入我们的地址,就能让 owner 变成我们的地址。刚好 address 也是 32 个字节,跟 uint256 一样。

22    function donate(uint256 etherAmount) public payable {
23        // amount is in ether, but msg.value is in wei
24        uint256 scale = 10**18 * 1 ether;
25        require(msg.value == etherAmount / scale);
26
27        Donation donation;
28        donation.timestamp = now;
29        donation.etherAmount = etherAmount;
30
31        donations.push(donation);
32    }

要实现这个调用,还得注意 require 条件:msg.value == etherAmount/scale,所以来计算一下 msg.value。
我的 msg.sender == 0x6B477781b0e68031109f21887e6B5afEAaEB002b

uint256 scale = 10**18 * 1 ether;
msg.value = etherAmount / scale; //wei
result = msg.value/10**18; //wei to ether


msg.value = (etherAmount/10**18)/scale = 612455775880 wei

之后去调用 donate() 函数:

发现 owner 已经被覆盖成功:

然后调用 withdraw() 函数取出钱:https://ropsten.etherscan.io/tx/0x3b358d371e469146f1768584b6811e9b4080afe2428527b24e21778eb27f06b4

验证发现通关成功:

总结:

局部变量覆盖状态变量