# 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>) → 檢查`msg.value`是否等於 (2<sup>256</sup> / 10<sup>18</sup>) * 10<sup>18</sup> = 2<sup>256</sup> = 0) → 成功饒過require statement → 使得自己的`balanceOf`增加 → 呼叫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) → `transferFrom`[req3] passed
3. newPlayer呼叫`transferFrom(player, player, 1)` → `transferFrom`[req1+req2] passed
4. 進入`_transfer(player, 1)` → `msg.sender == newPlayer` → `balanceOf[newPlayer]` - 1 = 0 - 1 = 2<sup>256</sup>-1;`newPlayer`呼叫`transfer(player, 1000000)` → `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` → `withdrawn` = 2<sup>256</sup>-1
3. 通過`require(withdrawn > 0)` → `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[]`全展開 → `set`(2<sup>256</sup>-2, 0) → `map.length` = 2<sup>256</sup>-1
2. elemSlot# = `keccak256(1)` + index → 使之overflow繞回#0
3. 因此 index = 2<sup>256</sup> - `keccak256(1)` = 2<sup>256</sup> - 80084422....44778998 → elemSlot# = 0
4. isCompleteOffset = index → 呼叫`set(index, 1)` → `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. 基於上述兩點,因而產生了合約漏洞 → 可利用`donate()`嘗試修改owner的值
- 過程:
1. 產生另一個新帳號並呼叫`donate(uint(newAddress)/scale)`
2. 上述執行後,使得`donation.etherAmount`=`newAddress` → `owner`=`newAddress` → 呼叫`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 -->