Published on

BSidesTLV 2022 CTF – Code is Law

Authors

Code is Law 1 & 2 (Blockchain, 150 + 250)

Download the zip file and follow the instructions in the README.md file to solve the solidity challenge.

Attachments: code-is-law-1.zip code-is-law-2.zip

In Code is Law 1 and Code is Law 2, we can get the flag when the address of msg.sender of ChallengeToken.did_i_win() holds ChallengeToken.

function did_i_win() public view returns (string memory) {
    if (balanceOf(msg.sender) == 0) {
        revert("you shall not pass");
    }

    return "BSidesTLV2022{PLACEHOLDER}";
}

We first take a look at the ChallengeToken in Code is Law 1:

function can_i_haz_token(address receiver) public {
    require(
        receiver == calculateAddressOfTheFirstContractDeployedBy(tx.origin),
        "receiver is ineligible for a token because they are not the first contract deployed by the EOA who initiated this transaction"
    );
    require(
        getContractCodeHash(receiver) == onlyICanHazTokenContractCodeHash,
        "receiver is ineligible for a token because their codehash does not match the specific contract codehash required"
    );

    if (balanceOf(receiver) == 0) {
        _mint(receiver, 1);
    }
}

We can only get new tokens from can_i_haz_token. However, the receiver must meet these conditions:

  • The receiver is on the address of the first contract deployed by the tx.origin
  • The code hash of the receiver equals the onlyICanHazTokenContractCodeHash

What about let OnlyICanHazToken obtain a token and then transfer it to us? Well, selfdestruct can only transfer ethers, not tokens.

contract OnlyICanHazToken {
    function bye() public {
        selfdestruct(payable(msg.sender));
    }
}

Going back to ChallengeToken, I tried but failed to find any weakness in getContractCodeHash and calculateAddressOfTheFirstContractDeployedBy. ERC20, the parent contract of ChallengeToken, is even less likely to have problems. (╥ω╥)

function getContractCodeHash(address contractAddress)
    private
    view
    returns (bytes32 callerContractCodeHash)
{
    assembly {
        callerContractCodeHash := extcodehash(contractAddress)
    }
}

// Copied from https://ethereum.stackexchange.com/a/87840
function calculateAddressOfTheFirstContractDeployedBy(address deployer)
    private
    pure
    returns (address _address)
{
    bytes32 hash = keccak256(
        abi.encodePacked(bytes1(0xd6), bytes1(0x94), deployer, bytes1(0x80))
    );

    assembly {
        mstore(0, hash)
        _address := mload(0)
    }
}

Finally, I turned my attention to the development environment Hardhat. HardHat has a method hardhat_setStorageAt1 which can modify contract storage. So, we can directly modify our balance to hold tokens!

ChallengeToken inherits from ERC20. There is a mapping variable _balances which is used to store the balance of each account. The storage location of _balances ends up being the slot 0 after applying the storage layout rules. So, the entry for address A is stored at keccak256(A | 0) where | is concatenation.

it('Should return the winning flag', async function () {
  challengeToken = await ethers.getContractAt(
    'ChallengeToken',
    '0x73511669fd4dE447feD18BB79bAFeAC93aB7F31f'
  )

  let [player] = await ethers.getSigners()
  let playerHash = await ethers.utils.solidityKeccak256(['uint256', 'uint'], [player.address, 0])
  await ethers.provider.send('hardhat_setStorageAt', [
    challengeToken.address,
    playerHash,
    ethers.utils.hexZeroPad(ethers.utils.hexlify(1), 32),
  ]) // modify the balance

  const returnedFlag = await challengeToken.did_i_win()
  console.log(`\tThe returned flag is: "${returnedFlag}"`)
})

Compared to Code is Law 1, ChallengeToken in Code is Law 2 only updates the rules of minting tokens and has banned approve. So, the above method can still be applicable =)

Flag

BSidesTLV2022{c0nstUct!v3_m@g!3_ind3ed} # Code is Law 1
BSidesTLV2022{W!L3_M@g!3_in_the_w3rld} # Code is Law 2

Footnotes

  1. Local ERC20 Balance Manipulation (with HardHat) ↩︎