Skip to content

Commit

Permalink
Missing update
Browse files Browse the repository at this point in the history
  • Loading branch information
0xAWM committed Dec 4, 2023
1 parent 9f9ab5b commit de7b1b1
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 32 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
363 changes: 331 additions & 32 deletions solutions/missing/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -116,6 +416,5 @@ function uint8ArrayToHex(bytes) {
uint8ArrayToHex(multihash.fromB58String(encoded))
'12207a0964c9fb7e33d9e5fb6dc6c17d84280dbbfabeb4429f2206529aca0a651222'
```

```

0 comments on commit de7b1b1

Please sign in to comment.