##                       ##

########           ########

############   ############

 ###########   ########### 

   #########   #########   

"@_    #####   #####    _@"

#######             #######

############   ############

############   ############

############   ############

######    "#   #"    ######

 #####               ##### 

  #####             #####  

    ####           ####    

       '####   ####'       

D
O

N
O
T

F
E
E
D

T
H
E

B
U
G
S

Solid Reverse

[CryptoVerse, 2023]

category: smart-contract

by J4X

Challenge

We are presented with one Solidity contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ReverseMe {
    uint goal = 0x57e4e375661c72654c31645f78455d19;
    function magic1(uint x, uint n) public pure returns (uint) {
        // Something magic
        uint m = (1 << n) - 1;
        return x & m;
    }
    function magic2(uint x) public pure returns (uint) {
        // Something else magic
        uint i = 0;
        while ((x >>= 1) > 0) {
            i += 1;
        }
        return i;
    }
    function checkflag(bytes16 flag, bytes16 y) public view returns (bool) {
        return (uint128(flag) ^ uint128(y) == goal);
    }
    modifier checker(bytes16 key) {
        require(bytes8(key) == 0x3492800100670155, "Wrong key!");
        require(uint64(uint128(key)) == uint32(uint128(key)), "Wrong key!");
        require(magic1(uint128(key), 16) == 0x1964, "Wrong key!");
        require(magic2(uint64(uint128(key))) == 16, "Wrong key!");
        _;
    }
    function unlock(bytes16 key, bytes16 flag) public view checker(key) {
        // Main function
        require(checkflag(flag, key), "Flag is wrong!");
    }
}

Our goal is to find a key that fits the requires and xor'd to the goal yields us the flag.

Solution

We in total have 4 requires which we can use to guess the key.

require(bytes8(key) == 0x3492800100670155, "Wrong key!");

This is pretty easy, it just tells us that the first 8 bytes of our key have to be this.

So our current key is:

0x3492800100670155XXXXXXXXXXXXXXXX

require(uint64(uint128(key)) == uint32(uint128(key)), "Wrong key!");

This is also pretty straight forward, we cast our 16 byte key to 8bytes and 4 bytes and want the results to be the same. This means that the bytes 7-4 need to be zero so it doesn't matter if we cast to 4 or 8 byte. Our key now is:

0x349280010067015500000000XXXXXXXX

require(magic1(uint128(key), 16) == 0x1964, "Wrong key!");

Now we get to use the first magic function:

function magic1(uint x, uint n) public pure returns (uint) {
    // Something magic
    uint m = (1 << n) - 1;
    return x & m;
}

What this does for us is generate a value that is n bits long and all zeros, and then ands our x with it, which just results in the first n bits of x. This means that our last 2 bytes are 0x1964. Our key now is:

0x349280010067015500000000XXXX1964

require(magic2(uint64(uint128(key))) == 16, "Wrong key!");

Now we use our second magic function:

function magic2(uint x) public pure returns (uint) {
    // Something else magic
    uint i = 0;
    while ((x >>= 1) > 0) {
        i += 1;
    }
    return i;
}

This just shifts our x by one to the left each step and increases i as long as x is not 0. As this should be 16, we are only using the last 8 bytes and the i starts counting after the first shift, this means what the 33rd bit is 1 and then there are only zeroes. This results in the final key:

0x34928001006701550000000000011964

XOR

Now we just need to xor this to the goal and get the flag:

0x34928001006701550000000000011964
^
0x57e4e375661c72654c31645f78455d19
=
0x63766374667b73304c31645f7844447d

This yields us the flag: cvctf{s0L1d_xDD}

/writeups/ $

$