# Capture the Ether https://capturetheether.com/ ## [Guess the secret number](https://capturetheether.com/challenges/lotteries/guess-the-secret-number/) - 作法:基於`uint8 n`範圍為0~255,直接用brute force方法破解即可。 - 修正:`n`範圍不能太小 ```solidity 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); } } } ``` ## [Guess the random number](https://capturetheether.com/challenges/lotteries/guess-the-random-number/) - 作法:根據[Layout of State Variables in Storage](https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html),`answer`可在鏈上直接查到。 - 修正: - 別事先將機密資訊寫在智能合約中 - 別使用`block.blockhash`、`block.number`、`block.timestamp`作為產生隨機數的inputs ```javascript const number = BigNumber.from( await contract.provider.getStorageAt(contract.address, 0) ); ``` ```solidity 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); } } } ``` ## [Guess the new number](https://capturetheether.com/challenges/lotteries/guess-the-new-number/) - 作法:藉由撰寫一份新的智能合約,仿照對象合約code計算出`answer`,再呼叫其`guess()`即可 - 修正:別使用`block.blockhash`、`block.number`、`block.timestamp`作為產生隨機數的inputs ```solidity function attack() external payable { require(address(this).balance >= 1 ether, "not enough funds"); uint8 answer = uint8(uint256( keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)) )); challenge.guess{value: 1 ether}(answer); require(challenge.isComplete(), "challenge not completed"); msg.sender.transfer(address(this).balance); } ``` ```solidity 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) { tx.origin.transfer(2 ether); } } } ``` ## [Predict the future](https://capturetheether.com/challenges/lotteries/predict-the-future/) - 作法: - 基於`answer`的範圍僅限 0~9,可藉由撰寫一份新的智能合約,呼叫`lockInGuess()`隨意設定一個數字(e.g., `0`),再多次嘗試呼叫`attack()`直到答案符合即可。 - 若`answer`不符合將`revert`而還原合約state,因此不會受任何影響 - 修正: - `answer`範圍不能太小 - 別使用`block.blockhash`、`block.number`、`block.timestamp`作為產生隨機數的inputs ```solidity function lockInGuess(uint8 n) external payable { challenge.lockInGuess{value: 1 ether}(n); } function attack() external payable { challenge.settle(); require(challenge.isComplete(), "challenge not completed"); tx.origin.transfer(address(this).balance); } receive() external payable {} ``` ```solidity 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); } } } ``` ## [Predict the block hash](https://capturetheether.com/challenges/lotteries/predict-the-block-hash/) - 基於效能原因,`blockhash`函數只回傳最後256個blocks的實際區塊hash值。 - `blockhash(uint blockNumber) returns (bytes32)`:僅適用於最近的256個blocks,不包括當前塊。 - 過了256 blocks之後會回傳`0x00000000000000000000000000000000000000000000000000000000000000000` - 等待257個blocks以呼叫`settle()`操作。 ```solidity 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); } } } ``` ## [Token sale](https://capturetheether.com/challenges/math/token-sale/) - 作法:利用arithmetic overflow的方式破解 - 過程:呼叫T.buy(2<sup>256</sup> / 10<sup>18</sup>) &rarr; 檢查`msg.value`是否等於 (2<sup>256</sup> / 10<sup>18</sup>) * 10<sup>18</sup> = 2<sup>256</sup> = 0) &rarr; 成功饒過require statement &rarr; 使得自己的`balanceOf`增加 &rarr; 呼叫T.sell(1)取得ether - 修正:T.buy()內部須補上overflow & underflow的檢查 ```solidity 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); } } ``` ## [Token Whale](https://capturetheether.com/challenges/math/token-whale/) - 作法: 1. 先用刪去法,把無誤的code排除。 2. 排除後發現`transferFrom()` & `_transfer()`寫法是有問題的,可往此方向破解。 - 過程: 1. 產生另一個新帳號`newPlayer` 2. `player`呼叫`approve`(`newPlayer`, 2<sup>256</sup>-1) &rarr; `transferFrom`[req3] passed 3. newPlayer呼叫`transferFrom(player, player, 1)` &rarr; `transferFrom`[req1+req2] passed 4. 進入`_transfer(player, 1)` &rarr; `msg.sender == newPlayer` &rarr; `balanceOf[newPlayer]` - 1 = 0 - 1 = 2<sup>256</sup>-1;`newPlayer`呼叫`transfer(player, 1000000)` &rarr; `isComplete = true` - 修正: 1. `transferFrom()`內部的`msg.sender`應為`to`,兩者不應混用 2. `_transfer()`參數應為`from, to, value`,內部也不應使用`msg.sender` ```solidity 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); } } ``` ## [Retirement fund](https://capturetheether.com/challenges/math/retirement-fund/) - 作法: 1. 先用刪去法,把無誤的code排除。 2. 排除後剩下`collectPenalty()`是可能的切入點,然而`collectPenalty()`未有參數可供輸入,因此得用其他方式破解。例如強制發送ethers,以控制address(this).balance之值。 - 過程: 1. 撰寫一份新合約並傳送1 wei進去 2. 呼叫`selfdestruct(RF)`,強制使得`address(this).balance = 1 ether + 1 wei` &rarr; `withdrawn` = 2<sup>256</sup>-1 3. 通過`require(withdrawn > 0)` &rarr; `beneficiary`接收來自RF合約中的全部ethers - 修正:別使用address(this).balance作為判斷依據 - 補充:以state variable記錄的`etherBalance`與`address(this).balance`有可能發生不同步的狀況,使得檢查失靈並造成漏洞。 ```solidity 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); } } ``` ## [Mapping](https://capturetheether.com/challenges/math/mapping/) - 作法: 1. 利用`set(key, value)`嘗試修改isComplete的值。 - 每份合約共有2<sup>256</sup>slots的storage儲存空間 - dynamic array: map[] in slot#1 = array length - dynamic array: elemSlot# = hash(1) + array index * elemSize - dynamic array: elemSize = 1 (uint256 / each element) - 過程: 1. 先將`map[]`全展開 &rarr; `set`(2<sup>256</sup>-2, 0) &rarr; `map.length` = 2<sup>256</sup>-1 2. elemSlot# = `keccak256(1)` + index &rarr; 使之overflow繞回#0 3. 因此 index = 2<sup>256</sup> - `keccak256(1)` = 2<sup>256</sup> - 80084422....44778998 &rarr; elemSlot# = 0 4. isCompleteOffset = index &rarr; 呼叫`set(index, 1)` &rarr; `isComplete` = true - 修正:別將更改state變數的權限設為public,尤其是dynamic array與mapping。 ```solidity 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]; } } ``` ![](https://i.imgur.com/IJbHINm.png) ## [Donation](https://capturetheether.com/challenges/math/donation/) - 作法: 1. 當中的`donate()`寫法有誤:scale = 10^36 & `uint256 etherAmount`的單位不是ether(10^18),使得`msg.value`很小的情況下能通過`require()`的檢查 2. 此外`Donation donation;`未標註`storage` or `memory`,則預設為`storage`。此語法意味著`donation`其實是指向了的slot#0 (i.e., Donation[] array length),而`donation.timestamp`與`donation.etherAmount`各對應slot#0與slot#1 (i.e., address owner) 的位置。 3. 基於上述兩點,因而產生了合約漏洞 &rarr; 可利用`donate()`嘗試修改owner的值 - 過程: 1. 產生另一個新帳號並呼叫`donate(uint(newAddress)/scale)` 2. 上述執行後,使得`donation.etherAmount`=`newAddress` &rarr; `owner`=`newAddress` &rarr; 呼叫`withdraw()`即可 - 修正: 1. `uint256 scale = 10**18 * 1 ether;`改為`uint256 scale = 10**18;` 2. `require(msg.value == etherAmount / scale);`改為`etherAmount * scale` 3. `Donation donation;`改為`Donation memory donation;` ```solidity 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); } } ``` ![](https://i.imgur.com/L9aCKvk.png) ![](https://i.imgur.com/MdfBFEZ.png) ![](https://i.imgur.com/bqfJ6Se.png) ## [Fifty years](https://capturetheether.com/challenges/math/fifty-years/) - 作法: 1. 在`upsert()`的else分支,發現再次使用了未初始化的storage pointer。`contribution.amount` & `contribution.unlockTimestamp`分別覆寫了`q.length` & `head`。 2. 此外,因為`queue.push(contribution);`的關係,`contribution.amount`、也就是`q.length`遭`msg.value`覆寫之後,`q.length`還會再+1 (`q.push`函數中的內部指令順序:q.length首先遞增,然後再複製queue entry)。 3. 利用`queue[queue.length - 1].unlockTimestamp + 1 days`的overflow,使其值變為0,這樣便可以在timestamp = 0的情況下通過`require()`之檢查。 - 過程: 1. 呼叫`upsert`(1, 2<sup>256</sup>-86400) & msg.value = 1 wei (若呼叫`upsert`未設定`msg.value`=`q.length`的話,`q.length`將被視為0、也就是被視為empty,所有的值都讀不到了)。 2. 呼叫`upsert`(2, 0) & `msg.value` = 2 wei,即可繞過`require(timestamp...)`,使得`head` = 0。 3. 還不能呼叫`withdraw(2)`,合約裡的`etherBalance` = 1 ether + 1 wei + 2 wei,而`uint256 total` = 1 ether + 2 wei + 3 wei (多出2 wei),因此得先用`selfdestruct`強制發送2 wei到此合約,然後再呼叫`withdraw(2)`即可。 - 修正:避免使用未初始化的storage pointer、timestamp要檢查overflow。 ```solidity 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); } } ``` <!-- ![](https://i.imgur.com/ADRD9xy.png) ![](https://i.imgur.com/wmqmj0L.png) ![](https://i.imgur.com/vORNDXY.png) ![](https://i.imgur.com/gGTjGq2.png) ![](https://i.imgur.com/pgi2QIr.png) --> ## [Fuzzy identity](https://capturetheether.com/challenges/accounts/fuzzy-identity/) 利用pre-compute合約地址 + brute force的方式破解 ```solidity 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; } } ``` ## [Public key](https://capturetheether.com/challenges/accounts/public-key/) - 由於交易是由帳戶簽署的,因此它們帶有簽名,並且該簽名需要包含一個公鑰,供所有人驗證交易是否來自該帳戶。或者在ECDSA的情況下,公鑰實際上不是簽名的一部分,但可以從訊息和簽名中recover。 - 幸運的是,owner帳戶只有一個轉出交易。我們可以使用一個節點來獲取它的數據,重新計算message hash,並recover地址的公鑰。 - ECDSA 簽名由 (r,s,v) 值組成,以太坊根據 EIP-155 對序列化tx hash進行簽名,即 keccak256(rlp(nonce, gasprice, startgas, to, value, data, chainid, 0, 0))。 - 對recoverPublicKey的另一個呼叫會產生未壓縮的公鑰,我們可以提交它來解決這個挑戰。 - 一個有趣的結果是,如果你從不自你的帳戶發送交易,那麼在區塊鏈上唯一可見的就只有地址(一個 sha256 hash)。這使得從未發送交易的帳戶具有量子安全性。 - 補充:account address: 其public key的keccak256 hash值之最後20個bytes - 補充:contract address: 由0xff prefix, deployerAddress, salt, 合約creationCode + constructorArgs組成bytecode的keccak256 hash值之最後20個bytes - 補充:r,s,v是交易簽名的值,可以用作獲取任何以太坊帳戶的公鑰。r,s是ECDSA簽名的輸出,v是recovery ID。 ```solidity contract PublicKeyChallenge { address owner = 0x92b28647ae1f3264661f72fb2eb9625a89d88a31; bool public isComplete; function authenticate(bytes publicKey) public { require(address(keccak256(publicKey)) == owner); isComplete = true; } } ``` ![](https://i.imgur.com/z10eDaV.png) ![](https://i.imgur.com/EnkmbPA.png) ![](https://i.imgur.com/4QV2aSY.png) ![](https://i.imgur.com/hnOSi65.png) ## [Account takeover](https://capturetheether.com/challenges/accounts/account-takeover/) - 這次我們需要檢索實際的私鑰來模擬來自所有者合約的交易 - 我們唯一的線索是owner的地址。檢查帳戶交易後,可能會注意到有兩筆交易在其簽名中使用相同的r值(ECDSA中的k值)。 - 關於ECDSA,需要了解的是k值應該是隨機選擇的,不能重複使用。兩次使用相同的k值來簽署不同的訊息[允許重新計算私鑰](https://bitcoin.stackexchange.com/questions/35848/recovering-private-key-when-someone-uses-the-same-k-twice-in-ecdsa-signatures)。 ```solidity contract AccountTakeoverChallenge { address owner = 0x6B477781b0e68031109f21887e6B5afEAaEB002b; bool public isComplete; function authenticate() public { require(msg.sender == owner); isComplete = true; } } ``` ![](https://i.imgur.com/LwPlwV7.png) <!-- ## [Token bank](https://capturetheether.com/challenges/miscellaneous/token-bank/) - 通常的 ERC-20 和 ERC-223 token標準之間的區別在於,後者透過呼叫接收者的tokenFallback函數,來通知接收者這個transfer,以防它是一個合約。 ```solidity 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; } } ``` ## Reference - https://cmichel.io/capture-the-ether-solutions/ - https://www.ctfiot.com/37245.html -->