Tags: blockchain evm
Rating:
# Immutable Writeup
### LakeCTF 2022 - blockchain 372 - 17 solves
> Code is law, and whatever's on the blockchain can never be changed. `nc chall.polygl0ts.ch 4700` [immutable.py](immutable.py)
#### Analysis
Python script is given. The script has three menus: `audit()`, `rugpull()` and `exit()`. To gain flag, I must execute below control flow.
1. `audit()`:
- Supply contract address `addr`.
- Check given address is a contract, not [EOA](https://ethereum.org/en/whitepaper/#ethereum-accounts) by checking code size.
- Check `target(addr)` is **NOT IN** contract's bytecode. `target(addr)` is known to everyone, and is the hash of `addr` with padding.
- After all check is passed, it will return a `proof = auth(addr)`. `proof` can only be generated by the script because it depends on secret value `KEY`.
2. `rugpull()`:
- Supply contract address `addr`.
- Supply `proof` which was obtained using `audit()`
- Check `target(addr)` is **IN** contract's bytecode. If it is in, give flag. To get `proof`, I must pass `audit()`, and it already checkd `target(addr)` is **NOT IN** contract's bytecode.
Therefore the objective of the challenge is to update a code of a deployed contract. This update logic must be done between `audit()` and `rugpull()`.
### Contract Creation/Destruction Internals
Let me gather information of contract creation and destruction.
1. [`CREATE`](https://ethervm.io/#F0) : `create(v, p, n)` creates a new contract with code at memory `p` to `p + n` and send `v` wei and return new address computed by `keccak256(msg.sender ++ nonce)[12:]`. [Ref](https://github.com/ethereum/go-ethereum/blob/2b44ef5f93cc7479a77890917a29684b56e9167a/crypto/crypto.go#L107). Nonce is incremented when contract creation.
2. [`CREATE2`](https://ethervm.io/#F5) : `create2(v, p, n, s)` creates a new contract with code at memory `p` to `p + n` and send `v` wei and return new address commputed by `keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:]`. [Ref](https://github.com/ethereum/go-ethereum/blob/2b44ef5f93cc7479a77890917a29684b56e9167a/core/vm/evm.go#L503). Nonce is incremented when contract creation. To redeploy to same address using `create2`, the contract must be self destructed or never been deployed.
3. [`SELFDESTRUCT`](https://ethervm.io/#FF): `selfdestruct(address(addr))` destroys the contract and sends all funds to addr. It also resets the nonce.
### Exploit Scenario
1. Deploy [Factory](contracts/Factory.sol) contract.
2. Deploy [Solution](contracts/Solution.sol) contract using `CREATE2`, using `salt` as `pcw109550` at [Factory](contracts/Factory.sol) contract.
3. Deploy [Contract1](contracts/Contract1.sol) contract using `CREATE`, at [Solution](contracts/Solution.sol) contract.
- [Contract1](contracts/Contract1.sol) contract address will be the address which bytecode becomes mutable.
4. Call `audit()`
- Give [challenge script](immutable.py), [Contract1](contracts/Contract1.sol) contract address `addr` and get `proof` of it. `target(addr)` is not in [Contract1](contracts/Contract1.sol) contract's bytecode so possible.
5. `SELFDESTRUCT` [Contract1](contracts/Contract1.sol) contract and [Solution](contracts/Solution.sol) contract.
- [Solution](contracts/Solution.sol) contract nonce is reset.
6. Redeploy [Solution](contracts/Solution.sol) contract using `CREATE2`, using `salt` as `pcw109550` at [Factory](contracts/Factory.sol) contract. Contract address is not changed because I used the same `salt`.
7. Deploy [Contract2](contracts/Contract2.sol) contract using `CREATE`, at [Solution](contracts/Solution.sol) contract.
- [Contract2](contracts/Contract2.sol) contract address will be equal to `addr` because [Solution](contracts/Solution.sol) contract was self destructed, and its nonce was reset. Nonce and parent contract address not changed so possible.
- Make `target(addr)` be the bytecode of [Contract2](contracts/Contract2.sol) contract. This is because to get flag while executing `rugpull()` in challenge script.
7. Call `rugpull()`
- Give [challenge script](immutable.py), [Contract2](contracts/Contract2.sol) contract address `addr` and `proof` obtained at step 4.
- At this point, `target(addr)` is included in the bytecode of contract `addr`, so I get flag.
### Implementation
I first tested the upper scenario using truffle test, [TestSolution.sol](test/TestSolution.sol) using dummy `target(addr)` value. After that, I wrapped everything in [solution.js](test/solution.js) ans wrapped it again using [pwntools](https://github.com/Gallopsled/pwntools), implemented at [solve.py](solve.py). It first boots up truffle node, and runs exploit script.
I get flag:
```
EPFL{https://youtu.be/ZgWkdQDBqiQ}
```
Full exploit code: [solve.py](solve.py) requiring [truffle-config.js](truffle-config.js)
Exploit test: [TestSolution.sol](test/TestSolution.sol)
Python snippet dependency: [requirements.txt](requirements.txt)
Problem src: [immutable.py](immutable.py)
Modified problem src: [immutable_local.py](immutable_local.py): RPC tweaked to truffle node.