diff --git a/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 1.png b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 1.png new file mode 100644 index 0000000..df1169d Binary files /dev/null and b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 1.png differ diff --git a/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 2.png b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 2.png new file mode 100644 index 0000000..15fe632 Binary files /dev/null and b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 2.png differ diff --git a/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 3.png b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 3.png new file mode 100644 index 0000000..4142847 Binary files /dev/null and b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 3.png differ diff --git a/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 4.png b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 4.png new file mode 100644 index 0000000..e30dd77 Binary files /dev/null and b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled 4.png differ diff --git a/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled.png b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled.png new file mode 100644 index 0000000..7cd87fd Binary files /dev/null and b/solutions/missing/Missing WriteUp 12a8fb151cdb4a74820bd3903b986b70/Untitled.png differ diff --git a/solutions/missing/README.md b/solutions/missing/README.md index fe7f9ab..8a6678f 100644 --- a/solutions/missing/README.md +++ b/solutions/missing/README.md @@ -1,48 +1,199 @@ -```solidity -contract A { - bool public isSolved; +# Missing WriteUp - function execute_44g58pv() public { - assembly { - let s := gas() - pop(call(s, caller(), 0, 0, 0, 0, 0x20)) - for { let i := exp(0x02, 0x02) } slt(i, extcodesize(caller())) { i := add(i, 0x02) } { - let l := mload(shr(0x02, call(gas(), caller(), 0, 0, 0, 0x20, 0x20))) - switch lt(l, mload(0x20)) - case 0 { - mstore(mul(0x20, 0x02), l) - mstore(0, keccak256(0x20, mul(0x20, 0x02))) +## Solution + +### Decompile + +decompile result by `dedaub` + +[https://library.dedaub.com/decompile?md5=f2af5c1436a5b93ce2be4b992d50920d](https://library.dedaub.com/decompile?md5=f2af5c1436a5b93ce2be4b992d50920d) + +```coffeescript +function __function_selector__(bytes4 function_selector) public payable { + MEM[64] = 128; + require(!msg.value); + if (msg.data.length >= 4) { + if (!(function_selector >> 224)) { + v0 = msg.sender.call().gas(msg.gas); + v1 = v2 = 4; + while (v1 < msg.sender.code.size) { <- if we can make a little contract, we can make skip this loop + v3 = msg.sender.call().gas(msg.gas); + if (MEM[v3 >> 2] < MEM[32]) { + MEM[0] = keccak256(keccak256(v4, MEM[v3 >> 2])); } - default { mstore(0, keccak256(0x0, mul(0x20, 0x02))) } + v1 += 2; } - if eq(mload(0), 0x58898ce26697138ee057566d1fcfbcf1384f625d0aa51e80fbc9c44e6cd8a658) { sstore(0, 1) } - if gt(sub(s, gas()), 0x60ff) { sstore(0, 0) } + if (!(MEM[0] - 0x58898ce26697138ee057566d1fcfbcf1384f625d0aa51e80fbc9c44e6cd8a658)) { + _isSolved = 1; + } + if (msg.gas - msg.gas > 24831) { <- must be a decomplie error + _isSolved = 0; + } + exit; + } else if (0x64d98f6e == function_selector >> 224) { + isSolved(); } } + (); } ``` +decompile result by `panoramix` -Challenge Desc: - -MerkleTree + EIP1153(TLOAD/TSTORE) + Bytecode + MinimizeContract -https://eips.ethereum.org/EIPS/eip-1153 +```coffeescript +def _fallback() payable: # default function + mem[64] = 96 + call caller with: + gas gas_remaining wei + mem[0] = ext_call.return_data[0] + idx = 4 + while idx <′ ext_code.size(caller): <=== if we can make a little contract, we can make skip this loop + call caller with: + gas gas_remaining wei + mem[32] = ext_call.return_data[0] + if mem[Mask(254, 0, ext_call.success) * 0.25] < ext_call.return_data[0]: + mem[0] = sha3(mem[0], ext_call.return_data[0]) + else: + mem[64] = mem[Mask(254, 0, ext_call.success) * 0.25] + mem[0] = sha3(ext_call.return_data[0], mem[64]) + idx = idx + 2 + continue + if not mem[0] - 0x58898ce26697138ee057566d1fcfbcf1384f625d0aa51e80fbc9c44e6cd8a658: + uint256(stor0) = 1 -Deploy: -``` -anvil --hardfork cancun -forge create src/Chaotic\ Merkel/Challenge.sol:ChallengeChaoticMerkel --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +end here. So must be a decompile error because miss the gas check ``` -Quick Solution: +heimdall : No valid decompile result. + +There are obvious problems with the output of the mainstream decompiler tools, but we can see the general outline. + +Our contract needs to return a specific value, but the length obviously exceeds 4: + +opcode: return + retoffset + retsize + (memory op to store value) + +If we can't make a contract with a length less than or equal to 4, then keccak is inevitable. + +After a lot of Google/Etherscan/Bigquery searches and brute-force cracking with no results, you'll always think something must be wrong! + +As ****bentobox19**** from DefiHackLabs said :”In most bruteforcing problem I've seen, you always have to do anything but bruteforcing 😛” + +### Disassemble + +Take a deeper dive with `bytegraph`! + +[https://bytegraph.xyz/bytecode/3217d114f613f626e62e2170b59f8c50/graph](https://bytegraph.xyz/bytecode/3217d114f613f626e62e2170b59f8c50/graph) + +![Untitled](Missing%20WriteUp%2012a8fb151cdb4a74820bd3903b986b70/Untitled.png) + +![Untitled](Missing%20WriteUp%2012a8fb151cdb4a74820bd3903b986b70/Untitled%201.png) + +![Untitled](Missing%20WriteUp%2012a8fb151cdb4a74820bd3903b986b70/Untitled%202.png) + +look at opcode 0x84 which caused keccak to different branch. After analysis, DUP4 is actually 0x20 + +Combined with the results from panoramix, we can tell that the challenger contract keeps requesting the user contract and doing keccak on its return value(with 0x20 size) overlay. And keccak input size is always 0x40. The purpose of LT is to arrange the return value with the existing value. + +## Keccak preimage + +Are you a little familiar? In fact, this is doing a root node check of Merkle Tree. + +But where did the damn `0xAWM` plant this tree? + +You may or may not think of IPFS, now we pretend that we haven't thought of it yet, so we decided to look back at the challenge and find some other information. + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +contract Challenge { + address public r; + + constructor(address) { + bytes memory code = + hex"608060405234801561001057600080fd5b5061011d806100206000396000f3fe6080604052348015600f57600080fd5b5060043610602d5760003560e01c8015603257806364d98f6e14603a575b600080fd5b6038605a565b005b60005460469060ff1681565b604051901515815260200160405180910390f35b5a602060006020818283843388f15060045b333b81121560a6578283838485335af160021c51835181108015609357604084208452609d565b6040828152852084525b5050600201606c565b507f58898ce26697138ee057566d1fcfbcf1384f625d0aa51e80fbc9c44e6cd8a65881510360d357600181555b6160ff5a8403111560e2578081555b50505056fea26469706673582212207a0964c9fb7e33d9e5fb6dc6c17d84280dbbfabeb4429f2206529aca0a65122264736f6c63430008170033"; + assembly { + sstore(r.slot, create(0, add(code, 0x20), mload(code))) + } + } + + function isSolved() public returns (bool) { + return Challenge(r).isSolved(); + } +} ``` -mv script/exploit/ChaoticMerkel.t.solution script/exploit/ChaoticMerkel.t.sol -forge create script/exploit/ChaoticMerkel.t.sol:Solve --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --use=./script/exploit/solc-osx -cast send 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "test()" --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --gas-limit=100000 -mv script/exploit/ChaoticMerkel.t.sol script/exploit/ChaoticMerkel.t.solution +Ever notice anything strange? + +```solidity +function isSolved() public returns (bool) { + return Challenge(r).isSolved(); + } ``` +The challenge is just a wrapper! Obviously, `r` is enough to judge `isSolved`. + +Why did he do this? He must be trying to hide some clues that he doesn't want to be discovered. He wraps the inner contract in order to hide **the modified metadata** + +When we first directly decompile the runtimecode of the inner contract, we have missed this clue. + +[https://playground.sourcify.dev/](https://playground.sourcify.dev/) + +![Untitled](Missing%20WriteUp%2012a8fb151cdb4a74820bd3903b986b70/Untitled%203.png) + +Aha. Unexpected thing happened. + +Look what we found: + +[https://ipfs.io/ipfs/QmWZ2kq9CfgKHkXYghaeKM2mcYZxsZP2ibrgXkPK6ivjk5](https://ipfs.io/ipfs/QmWZ2kq9CfgKHkXYghaeKM2mcYZxsZP2ibrgXkPK6ivjk5) + +![Untitled](Missing%20WriteUp%2012a8fb151cdb4a74820bd3903b986b70/Untitled%204.png) + +Such a large amount of hash value is simply a treasure! + +And… `0x58898ce26697138ee057566d1fcfbcf1384f625d0aa51e80fbc9c44e6cd8a658` is really inside. + +Wait…… Looks like we're gonna have to organize the tree ourselves: + +```python +import itertools +import random +from web3 import Web3 + +nodes = [] +def keccak256(x, y): + if len(x) == 66: + x = x[2:] + if len(y) == 66: + y = y[2:] + if x < y: + b = bytes.fromhex(x) + bytes.fromhex(y) + else: + b = bytes.fromhex(y) + bytes.fromhex(x) + return Web3.keccak(b).hex() + +nodes = random.sample(nodes, len(nodes)) # This is what is being used by 0xAWM to make difficulties + +# for i in nodes: +# print(i) + +def get_root(nodes): + fathers = [] + while True: + print() + for i, j in itertools.combinations(nodes, 2): + k = keccak256(i, j) + if k in nodes: + fathers.append(k) + print(f"keccak256({i},{j}) = {k}") + nodes = fathers + if len(fathers) == 1: + return fathers[0] + fathers = [] + +print(get_root(nodes)) + +``` ``` Tree @@ -79,8 +230,157 @@ Tree └─ 01969e3b34fb58274a33cdf37bab8173c2681aaefd1010ae9c6076e45424ac65 ``` -ipfs: +## Gogogo + +Then we construct a minimum proxy contract to complete the challenge. + +We will try to use sstore/sload to determine different return values, but it will exceed gas. + +If you're familiar with EIP1153, you can easily replace it with a TSTORE/TLOAD to solve this challenge. Otherwise, using `gasleft()` will consume a lot of time to debug. + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import {Test, console2} from "forge-std/Test.sol"; +import "../../src/Missing/Challenge.sol"; + +contract Child { + constructor() { + (, bytes memory b) = msg.sender.call("1"); + address impl = abi.decode(b, (address)); + assembly { + tstore(0, impl) + mstore(0, hex"6021805f5f5f5f5c5af4f3") + return(0, 11) + } + /* + PUSH1 0x21 + DUP1 + PUSH0 + PUSH0 + PUSH0 + PUSH0 + TLOAD + GAS + DELEGATECALL + RETURN + */ + } +} + +contract Solve is Test { + address slot0; + address immutable me = address(this); + uint256 times = 0; + // ChallengeChaoticMerkel immutable c = new ChallengeChaoticMerkel(); + ChallengeChaoticMerkel immutable c = ChallengeChaoticMerkel(0x5FbDB2315678afecb367f032d93F642f64180aa3); + + function test() public { + c.isSolved(); + address child = address(new Child()); + child.call(abi.encodeWithSignature("1")); + c.isSolved(); + } + + fallback() external payable { + if (me == address(this)) { + assembly { + mstore(0, address()) + return(0, 0x20) + } + } + assembly { + let t := tload(1) + if eq(t, 0) { + tstore(1, 2) + pop(call(gas(), 0xa16E02E87b7454126E5E10d957A927A7F5B5d2be, 0, 0, 4, 0, 0)) // challenge address + return(0, 0) + } + if eq(t, 2) { + mstore(1, hex"803511394c6f5151454880286edf5b3bf23c886f7e752818c91e5379c8edba5a") + tstore(1, 3) + return(0, 33) + } + if eq(t, 3) { + mstore(1, hex"fcdd6194e30d12c1f1a621e13862cb9c19160a96e16358b9140650b3dd54c98c") + tstore(1, 4) + return(0, 33) + } + if eq(t, 4) { + mstore(1, hex"730239b21a857fa6df9e821db8779a908f5eaf9cc1291dd46bc2b0659a2a00d1") + tstore(1, 5) + return(0, 33) + } + if eq(t, 5) { + tstore(1, 6) + mstore(1, hex"c30e7c0611d9b5dfdd41146cd1c147c6d392cd9ba5ac6702a7d747e3b9eaf987") + return(0, 33) + } + if eq(t, 6) { + mstore(1, hex"e4675068ce90f260cb4fcf35a183cbc76171f5a54e8f0227199878fbe7d92420") + return(0, 33) + } + } + } +} ``` + +## Idea and Info for Challenge + +### Challenge Desc: + +MerkleTree + EIP1153(TLOAD/TSTORE) + Bytecode + MinimizeContract +[https://eips.ethereum.org/EIPS/eip-1153](https://eips.ethereum.org/EIPS/eip-1153) + +### Source Code + +```solidity +contract A { + bool public isSolved; + function execute_44g58pv() public { + assembly { + let s := gas() + pop(call(s, caller(), 0, 0, 0, 0, 0x20)) + for { let i := exp(0x02, 0x02) } slt(i, extcodesize(caller())) { i := add(i, 0x02) } { + let l := mload(shr(0x02, call(gas(), caller(), 0, 0, 0, 0x20, 0x20))) + switch lt(l, mload(0x20)) + case 0 { + mstore(mul(0x20, 0x02), l) + mstore(0, keccak256(0x20, mul(0x20, 0x02))) + } + default { mstore(0, keccak256(0x0, mul(0x20, 0x02))) } + } + if eq(mload(0), 0x58898ce26697138ee057566d1fcfbcf1384f625d0aa51e80fbc9c44e6cd8a658) { sstore(0, 1) } + if gt(sub(s, gas()), 0x60ff) { sstore(0, 0) } + } + } +} +``` + +### Deploy: + +```bash +anvil --hardfork cancun +forge create src/Chaotic\\ Merkel/Challenge.sol:ChallengeMissing --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + +``` + +### Quick Solution: + +```bash +mv script/exploit/Missing.t.solution script/exploit/Missing.t.sol +forge create script/exploit/Missing.t.sol:Solve --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --use=./script/exploit/solc-osx +cast send 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 "test()" --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --gas-limit=100000 + +restore: +mv script/exploit/Missing.t.sol script/exploit/Missing.t.solution + +``` + +### ipfs hash converter + +```jsx npm install multihashes function hexToUint8Array(hexString) { @@ -116,6 +416,5 @@ function uint8ArrayToHex(bytes) { uint8ArrayToHex(multihash.fromB58String(encoded)) '12207a0964c9fb7e33d9e5fb6dc6c17d84280dbbfabeb4429f2206529aca0a651222' -``` - +``` \ No newline at end of file