So, this is gonna be a writeup for one of my CTF challenges at CipherShastra. If you have not tried it, I suggest you to give it a try, before reading the writeup.
Chall: Sherlock | Difficulty: Easy
Description
The challenge description says “Can You Find My Password Hash?”. So, the first point to note down here is we have to look out for hash and not a string, because there are some rabbit holes or fake flags setup intentionally, just to distract. Now the question, “Who’s Password hash?”, the second thing to note down here is the Author’s Name: Razzor
What else do we have?
With that, we have given a Rinkeby address and a pseudo Contract code, where some values are REDACTED. So, do we need to find those values? We don’t know that yet. Need to explore everything that we have
Transaction Analysis
Let’s go to Etherscan, and do some transaction analysis. Starting with the transaction for Contract Creation. View the input data as UTF-8
We got some strings, but seems like they are here just to troll us. But, there is one which looks interesting as Razzor{Y0u_F0uNd-M3$%#}. So, is it the flag? Don’t know yet, but as the description asks for a hash, and not a string. This could be possibly another Fake Flag, and if it’s a hash, that we are supposed to find then It can’t be decoded from the input data. So, we need to enumerate more.
Continuing our, transaction analysis. The other transactions too have some strings, but this time, these look like some usernames.
But, then we find a transaction as:
The input data contains a string as razzor which is the Author’s name and which gives us some clue to our directionless journey that there is a user as razzor and we may supposed to find it’s password hash.
What Next?
So, the next piece of information can be obtained by understanding the contract code. We have some sort of pseudo code available which contains bunch of data types as: uint16, uint32, uint256, bool, address, bytes32 and constants, Array of Struct type, and a Mapping. What about the values stored in them? They might be containing the actual hash, right?
Interestingly, the visibility of most of them is kept as Private. What does this mean? We can’t read them?
The private visibility just defines, that it is not accessible outside the scope of the contract in which it is defined. It has nothing to do with implementing a restriction to read it. Everything is Public in a Permission less Blockchain.
Understanding the Storage Slot
Every contract has it’s own storage to store the data. Every storage has 2²⁵⁶ slots, and each slot can hold up to 32 bytes. The Statically-sized variables are stored sequentially in the slots, in the same order as they are declared in the contract. Variables also share a slot, if there is enough space to hold them. If a variable doesn’t fit in the remaining space, it is stored in the next slot. The data is stored from right to left in a slot.
Structs always start with a new slot, and occupy the whole slot. Mapping and Dynamic Arrays occupy the storage slot in the same order as they are declared but due to their unpredictable size, the elements are stored in a different storage slot. We will be coming back to this point later.
Constants don’t take up a storage slot, instead they are attached to contract byte code.(If you decompile the bytecode, you will find a constant value as Razzor{Y0u_F0uNd-M3$%#} which we found earlier in the contract creation transaction data)
Number of bytes required by different data types:
Now, Let’s try to mark slots for our understanding
| - - - - - - - -Slot 0 - - - - - - - -|
uint256 public var256_1 = 1337;| - - - - - - - -Slot 1 - - - - - - - -|
bool public bool_1 = false;
bool public bool_2 = false;
bool public bool_3 = true;
uint16 public var16_1 = 32;
uint16 private var16_2 = 64;
address public contractAdd = address(this);| - - - - - - - -Slot 2 - - - - - - - -|
uint256 private var256_2 = 3445;| - - - - - - - -Slot 3 - - - - - - - -|
uint256 private var256_3 = 6677;| - - - - - - - -Slot 4 - - - - - - - -|
bytes32 private iGotThePassword;| - - - - - - - -Slot 5 - - - - - - - -|
bytes32 private actuallPass;| - - - - - - - -Slot 6 - - - - - - - -|
bytes32 private definitelyThePass;| - - - - - - - -Slot 7 - - - - - - - -|
uint256 public var256_4 = 7788;| - - - - - - - -Slot 8 - - - - - - - -|
uint16 public var16_3 = 69;
uint16 private var16_4 = 7;
bool private _Pass = true;
bool private _The = true;
bool private _Password = false;
address private owner;
uint16 private counter;| - - - - - - - -Constants - - - - - - |
bytes32 public constant thePassword
bytes32 private constant ohNoNoNoNoNo| - - - - - - Slot 9–12 - - - - - - - -|
bytes32[4] private passHashes;struct Passwords {
bytes32 name;
uint256 secretKey;
bytes32 password;
}| - - - - - - - -Slot 13 - - - - - - - -|
Passwords[] private passwords;| - - - - - - - -Slot 14 - - - - - - - -|
mapping (uint256 => Passwords) private destiny;
Slot 0: uint256 which occupies the whole 32 bytes
Slot 1: 3 * bool + 2 * uint16 + 1 * address = 3+4+20 = 27 bytes. Now the remaining space left in this slot is 32–27 = 5 bytes, which can’t hold the next uint256 bytes data, so it is stored in the next slot.
Likewise, we can mark all the slots.
Accessing the storage slots
Open your truffle console
truffle console --network rinkeby
We will be using web3.eth package to interact with our smart contract and getStorageAt function to access the storage at specific position or in other words a specific storage slot
Accessing Statically-Sized Variables:
Static Sized Variables can be simply accessed with web3.eth.getStorageAt(contractAddress, slotNumber)
truffle(rinkeby)> addr = "
"
'0x3a6CAE3af284C82934174F693151842Bc71b02b2'truffle(rinkeby)> web3.eth.getStorageAt(addr,0)
'0x0000000000000000000000000000000000000000000000000000000000000539'truffle(rinkeby)> parseInt("0x0539", 16)
1337
Similarly, let’s access the slot 1:
truffle(rinkeby)> web3.eth.getStorageAt(addr,1)
‘0x00000000003a6cae3af284c82934174f693151842bc71b02b200400020010000’
As the data is stored from the right to left, let’s break it down
00: 1 byte bool value(false)
00: 1 byte bool value(false)
01: 1 byte bool value(true)
0020: 2 bytes uint16 value(32 in decimal)
0040: 2 bytes uint16 value(64 in decimal)
3a6cae3af284c82934174f693151842bc71b02b2: 20 bytes address
Likewise, we can access other slots, and find out the values stored in them.
Let’s try to access seemingly interesting bytes32 values stored in slots 4,5,6
truffle(rinkeby)> web3.eth.getStorageAt(addr,4)
‘0x7930755f6730745f703473735730526400000000000000000000000000000000’truffle(rinkeby)> web3.utils.toAscii(‘0x7930755f6730745f703473735730526400000000000000000000000000000000’)
‘y0u_g0t_p4ssW0Rd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00’truffle(rinkeby)> web3.eth.getStorageAt(addr,5)
‘0x61633175614c2d50347353000000000000000000000000000000000000000000’truffle(rinkeby)> web3.utils.toAscii(‘0x61633175614c2d50347353000000000000000000000000000000000000000000’)
‘ac1uaL-P4sS\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00’truffle(rinkeby)> web3.eth.getStorageAt(addr,6)
‘0x5930755f53755233000000000000000000000000000000000000000000000000’truffle(rinkeby)>
web3.utils.toAscii(‘0x5930755f53755233000000000000000000000000000000000000000000000000’)
‘Y0u_SuR3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00’
We got some interesting strings. If you remember, we found them earlier in the transaction of contract creation. Again this is not we are looking for, because we are supposed to find Author’s password hash.
Slot 9–12 contains an array of 4 bytes32 elements as passHashes. After trying to access them, you will find out that they all are empty.
truffle(rinkeby)> web3.eth.getStorageAt(addr, 9)
'0x0000000000000000000000000000000000000000000000000000000000000000'truffle(rinkeby)> web3.eth.getStorageAt(addr, 10)
'0x0000000000000000000000000000000000000000000000000000000000000000'truffle(rinkeby)> web3.eth.getStorageAt(addr, 11)
'0x0000000000000000000000000000000000000000000000000000000000000000'truffle(rinkeby)> web3.eth.getStorageAt(addr, 12)
'0x0000000000000000000000000000000000000000000000000000000000000000'
So we are now left with a dynamic array of type Struct and a Mapping.
About to reach the Destiny
The struct declaration contains, a name, secretKey and password. This might be it? Hopefully, we find our user Razzor here, and it’s password hash. Let’s do that!
Accessing Dynamic Array:
If we try to access the slot 13, it will give us the size of the dynamic array passwords. To access the elements, we have to access the storage at
keccak256(s), where s is the slot number
truffle(rinkeby)> web3.utils.soliditySha3({type: "uint", value: 13})
'0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb5'truffle(rinkeby)> web3.eth.getStorageAt(addr, '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb5')
'0x736865726c6f636b000000000000000000000000000000000000000000000000'truffle(rinkeby)> web3.utils.toAscii('0x736865726c6f636b000000000000000000000000000000000000000000000000')
'sherlock\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'truffle(rinkeby)> web3.eth.getStorageAt(addr, '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb6')
'0x000000000000000000000000000000000000000000000000000000000000848f'truffle(rinkeby)> web3.eth.getStorageAt(addr, '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb7')
'0x738e58e5a6aacbcf070d643ca922d8570351454244da967af8e88dd6978a8b4d'truffle(rinkeby)> web3.eth.getStorageAt(addr, '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb8')
'0x776174736f6e0000000000000000000000000000000000000000000000000000'truffle(rinkeby)> web3.utils.toAscii('0x776174736f6e0000000000000000000000000000000000000000000000000000')
'watson\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
By accessing the storage slot at keccak256(13), we found the very first element of type Struct, and the first item of the element is username as sherlock, if we increment the storage with 1, we can get the secretKey and further incrementing it, we get password hash of this user. Great!
(further incrementing will lead us to next elements)
So, now we have to iterate through all the usernames until we find Razzor. Unfortunately, Razzor is not here!
Destiny
We are finally left with the mapping called as destiny.
To get the element value of a Mapping, the storage slot is calculated as
keccak256(k . s)
where k is the mapping key and s is the storage slot of mapping and (.) is the concatenation of both.
Let’s do that!
truffle(rinkeby)> web3.utils.soliditySha3({type: "uint", value: 0}, {type: "uint", value: 14})
'0xe710864318d4a32f37d6ce54cb3fadbef648dd12d8dbdf53973564d56b7f881c'truffle(rinkeby)> web3.eth.getStorageAt(addr, '0xe710864318d4a32f37d6ce54cb3fadbef648dd12d8dbdf53973564d56b7f881c')
'0x687564736f6e0000000000000000000000000000000000000000000000000000'truffle(rinkeby)> web3.utils.toAscii('0x687564736f6e0000000000000000000000000000000000000000000000000000')
'hudson\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'truffle(rinkeby)> web3.eth.getStorageAt(addr, '0xe710864318d4a32f37d6ce54cb3fadbef648dd12d8dbdf53973564d56b7f881d')
'0x0000000000000000000000000000000000000000000000000000000000002775'truffle(rinkeby)> web3.eth.getStorageAt(addr, '0xe710864318d4a32f37d6ce54cb3fadbef648dd12d8dbdf53973564d56b7f881e')
'0x72f2b2b65299526b6287b9c3f8031fc16e5ccb514c61c6b0be54853be7f5fbab'
The first key(0) revealed a user as hudson. Doing the same for key 1
web3.utils.soliditySha3({type: "uint", value: 1}, {type: "uint", value: 14})
'0xa7c5ba7114a813b50159add3a36832908dc83db71d0b9a24c2ad0f83be958207'web3.eth.getStorageAt(addr, '0xa7c5ba7114a813b50159add3a36832908dc83db71d0b9a24c2ad0f83be958207')
'0x72617a7a6f720000000000000000000000000000000000000000000000000000'truffle(rinkeby)> web3.utils.toAscii('0x72617a7a6f720000000000000000000000000000000000000000000000000000')
'razzor\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'truffle(rinkeby)> web3.eth.getStorageAt(addr, '0xa7c5ba7114a813b50159add3a36832908dc83db71d0b9a24c2ad0f83be958208')
'0x0000000000000000000000000000000000000000000000000000000000000001'truffle(rinkeby)> web3.eth.getStorageAt(addr, '0xa7c5ba7114a813b50159add3a36832908dc83db71d0b9a24c2ad0f83be958209')
'0xcd7bfc1df0a853a60c6d1fddc112e54eb4e1c7c25144889e2263e4334da67945'
Finally, we found the user Razzor and it’s password hash as
“0xcd7bfc1df0a853a60c6d1fddc112e54eb4e1c7c25144889e2263e4334da67945”
which is a keccak256 hash for
Razzor{Pr1v4T3–1s_N0t-S0_pr1V4t3_shhhhh}
We have completed our journey. Is this it?
Bonus: Try accessing the values for key 2, and let me know what you found.
Key Takeaways:
1: Everything is Public in a Permission less Blockchain
2: The private visibility just defines, that it is not accessible outside the scope of the contract in which it is defined. It has nothing to do with implementing a restriction to read it.
3: Hash your passwords and sensitive information before putting them on blockchain
References:
https://web3js.readthedocs.io/en/v1.3.4/web3-eth.html#getstorageat
Connect With Me:
Twitter: https://twitter.com/razzor_tweet
Linkedin: https://linkedin.com/in/razzor