21 Days, auditing?
Day 01 - Everything starts here
Today will officially be my first day on my journey into becoming a full time smart contract auditor.
It is not my actual first day, maybe the 5th or 6th, but this first days served for a couple of things.
Trainning my algortihm
The other day I saw the importance of sinknig yourself into the topic you are trying to learn. In this case, Twitter is the way I started to embed myself into the world of smart contract auditing, following a bunch of well-known auditors and people in the space.
This was the first step into the rabbit hole, but I needed to do more. I needed to start reading the articles and papers that these people were sharing, and that’s where the twitter algorithm comes in.
I don’t need to search much to discover new cool blogs or useful websites, the algorithm does it for me. I just need to keep reading and liking the content I find interesting.
Getting my tools ready
In this first five or so days, I was ablt to quickly create what I think are two useful tools, not only for me, but also for the community.
Useful websites
-
sec-eth.xyz: A websites that compiles all the security related resources I have found so far. It is a work in progress, but I think it is already useful.
-
filter4rena: This is quite a useful tool for someone starting out auditing code4rena. It is a simple filter that allows you to filter vulnerabilities by severity or fuzzy search for a specific vulnerability.
Dev stuff
In terms of my dev environment, I have been using VSCode with the Solidity extension. I’m a Foundry enjoyer so that’s my testing framework of choice. Also a bunch of extensions to make my life easier, like the solidity visual developer one from ConsenSys Diligence and a couple more.
Engage
Another crucial part of this journey is to engage with the community. Talk to people on Twitter, join the discord channels, and participate in the conversations, you will not only learn a shit ton of new things but also make cool connections.
Wrapping up
This post was kinda lenghty, next ones won’t be like this. I will try to make this posts as useful as possible for a newcomer, therefore, they will include new learnings and technical stuff I find cool to share.
I have some experience coding, but I consider myself quite a junior in Solidity, so if you are in the same situation, welcome and have fun learning with me.
Consider giving a follow @mariodev__. See you soon mate, and keep hacking!
Day 02 - People don’t read standards
Today I went on and after doing some Ethernaut challenges, I decided to try and go through open challenges on Code4rena.
Prepared my environment, opened filter4rena, and started looking for some vulns.
First finding
When starting to look at the contracts I quickly found this line of code that caught my attention:
ERC20(token).transfer(msg.sender, amount);
Even though this was my first ever real audit, I have been reading a lot of blog posts and common vulnerabilities, so I knew that this line of code was considered a vulnerability because it doesn’t check the return value of the transfer
function, and some tokens do not revert
when the transfer fails but instead return false
.
It is really easy to fix, just use the safeTransfer
function from openzeppelin lib:
ERC20(token).safeTransfer(msg.sender, amount);
Same thing happens with the transferFrom
function.
This is now considered a Low severity vulnerability, but not so long ago it was considered a Medium severity vulnerability, see this audit report from @cmichelio
Familiarize with standards
Did you know that $USDT doesn’t follow the ERC20 standard? It is a bit weird, but it is true. Therefore, using $USDT as the token
parameter, will result in a revert
.
The catch
Hehe, you might be thinking: “Damn, first day and I already found a vulnerability, this guy knows what he is doing”
The real thing is that no, I have no Idea what I am doing, I’m just lurking around and reading stuff, but I’m still not able to craft a proper POC so my scope is really on QA and Low severity vulnerabilities.
In fact (here is the catch) this is such a simple and easy to spot vulnerability that the automatic 4nalyzer from Code4rena already found it, not only this one but even TWO MEDIUM SEVERITY VULNERABILITIES, and they immediately become common knowledge and are not accountable for reward.
Pretty crappy that this happened while I was just about to submit my first low severity.
Some people even think that the 4nalyzer could be next top auditor 😅
Day 03 - Auditing heuristics
I found this cool repo on Github that has a list of heuristics for auditing smart contracts. I thought it would be a good idea to go through the ones I found interesting.
Code asymmetries
In many projects, there should be some symmetries for different functions.
For instance, a withdraw
function should (usually) undo all the state changes of a deposit
function and a delete
function should undo all the state changes of the corresponding add
function.
Asymmetries in these function pairs (e.g., forgetting to unset a field or to subtract from a value) can often lead to undesired behavior.
This one might seem obvious, and it is, but the idea of this repo doesn’t seem to be to give you the key to the audit kingdom, but to give you a list of things to look for on every project, because they are things that might be overlooked and are actually quite common.
Off-by-one errors
Off-by-one errors are very common (not only in smart contracts), so it always makes sense to think about the boundaries. Is <=
correct in this context or should <
be used? Should a variable be set to the length of a list or the length - 1
? Should an iteration start at 1
or 0
?
I liked this one in particular for almost the same reason as the first one, it is something so easily overlooked, and a mistake that even the most experienced programmers make, so even if you don’t find any particular vulnerability, you can still help the developer by pointing out these things.
Not following EIPs / Standards
Ethereum Improvement Proposals and in general standards (like RFCs) often have very detailed instructions on how different functions/implementations should behave in different scenarios. Not following these is very bad for composability (after all, the reason for these standards is that you can rely on a certain behavior) and it can also result in vulnerabilities. In my experience, there are often slight details (e.g., not reverting when one should revert or not following the specified rounding behavior) that are ignored by implementers.
Well, this one may be familiar if you read my previous post ;)
Behavior when src == dst
In a lot of smart contracts, there are functions where you have to specify a source (or sender) and a destination (or recipient). Sometimes, the programmer did not think about what happens when these (user-specified) parameters are equal, which can result in undesired behavior. For instance, the balance of the source and destination may be cached in the beginning, in which case you can inflate your balance by specifying yourself as the source and the destination.
I just finished watching an interview 100proof - Receiving a 150k Bug Bounty, Web3 Bounty Hunting and Smart Contract Auditing and while writing this post and listening to it in the background 100proof mentioned this exact same thing.
This just assures me that this is a very good thing to test for, maybe not specifically for finding a vulnerability, but to make sure that the code is behaving as expected.
Wrapping up
I think this is a very good list of things to look for when auditing a smart contract, and I will definitely be using it in the future. I hope you found it useful as well.
I intended to write some code today, but I had some work to do on my job, so I didn’t get to it. I will try to write some code asap and show some POCs, this will also serve as a good way to improve my POC skills.
Day 04 🔥 - Calldata & foundry
The other day, I stumbled upon a tweet from @DeGatchi promoting his latest blog post about Reversing the EVM: Raw Calldata and I thought it was so helpful that I decided to write a post about it, not only to share it with you (rando reading this) but also to have a reference for myself.
It includes a foundry
CLI mini-masterclass (if that’s a thing).
Calldata 101
Super brief ELI5 explanation of calldata:
Calldata
is the encoded data passed to a funtion in the form of bytes, and there are two types, dynamic and static.
Setting up the playground
I recently learned how powerful foundry can be, not only for testing but also because of its different CLI tools.
- Start by running
anvil
to launch a local node. - I recommend exporting a couple private keys and addresses to your environment variables.
export PK1=0x00000000
export PK2=0x00000000
export addr1=0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
export addr2=0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
- Leave the node running and open a new terminal window to run
forge init <project-name>
. - Finally
cd
into<project-name>
and open your fav code editor.
Encoding calldata
Imagine we have this contract:
interface IDay04 {
function transfer(uint256[] memory ids, address to) external;
}
contract Day04 {
function checkCalldata(uint256[] memory ids, address to) external pure returns (bytes memory) {
return abi.encodeWithSelector(IDay04.transfer.selector, ids, to);
}
}
Let’s deploy the contract with the following command:
forge create --private-key $PK1 --rpc-url http://127.0.0.1:8545 src/Day04.sol:Day04
You will now see some stuff in the anvil
terminal window, that’s the transaction details for the contract creation, take the contract address and run:
cast send <contract-address> "checkCalldata(uint256[],address)" "[1,2,3]" $addr1 --from $addr2
This is the way we send a transaction to the contract in our local node.
To send a transaction to a real world contract (testnet or mainnet) you will need to specify a couple more parameters (maybe next post), but for now this is enough.
cast tx <tx-hash>
This will let us see the transaction details.
In the input
field you’ll find something like this (depending on the parameters you used, you may see slightly different values):
0x8229ffb6
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000009965507d1a55bcc2695c58ba16fb37d819b0a4dc
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
This is the raw calldata, it is scary af, it was for me too before reading about how to interpret it.
Understanding the output
-
The first 4 bytes (8 bits) are the function selector, which is the first 4 bytes of the
keccak256
hash of the function signature. In this case, the function signature istransfer(uint256[],address)
and thekeccak256
hash of it is0x8229ffb6
. -
The next 32 bytes (64 bits) are the offset to the start of the
ids
array. In this case, the offset is0x40
which is 64 in decimal. -
The next 32 bytes (64 bits) are the address of the
to
address. In this case, the address is0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc
. -
The next 32 bytes (64 bits) are the length of the
ids
array. In this case, the length is0x3
which is 3 in decimal (As you can see, this part is dynamic depending on the length of the array). -
The next 32 bytes (64 bits) are the first element of the
ids
array. In this case, the first element is0x1
which is 1 in decimal. -
The next 32 bytes (64 bits) are the second element of the
ids
array. In this case, the second element is0x2
which is 2 in decimal. -
The next 32 bytes (64 bits) are the third element of the
ids
array. In this case, the third element is0x3
which is 3 in decimal.
Creating our own calldata
Now that we know how to read calldata, we should be able to create our own.
Let’s try to create the calldata for the mint
function:
interface IDay04 {
function mint(uint8 amount, uint256 id, address to) external;
}
Take into account the uint8
type. What do you think the calldata will look like?
Parameters:
amount: 1
id: 23
addr: 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc
First we need to know the function selector, which is the first 4 bytes of the keccak256
hash of the function signature. In this case, the function signature is mint(uint8,uint256,address)
and the keccak256
hash of it is 0x2715eb15703f2d8697ebb56dbe6d9311aee605bf91ddcfea9b2264e30bd4d4a0
so first 4 bytes of it are 0x2715eb15
.
So the raw calldata will look like this:
0x2715eb15
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000017
0000000000000000000000009965507d1a55bcc2695c58ba16fb37d819b0a4dc
Remember that this is output in hexadecimal, so the 17 you see there is actually 23 in decimal.
Ey! but what about the uint8? Doesn’t it change the lenght of something somewhere? Well, the uint8
type is the same as the uint256
type in terms of encoding, the only difference is that the uint8
type is limited to 8 bits, so the value will be truncated to 8 bits.
To prove that we are right, let’s again use foundry
but this time, to further increase our knowledge of foundry
toolset, we won’t be transmiting a tx to the node, instead, use the calldata
command.
cast calldata "mint(uint8,uint256,address)" 1 23 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc
Returns:
0x2715eb15
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000017
0000000000000000000000009965507d1a55bcc2695c58ba16fb37d819b0a4dc
Wrapping up
This is barely the tip of the iceberg, there is a lot more to learn about calldata, we just scratched the surface of dynamic calldata and didn’t even talk about multicall (DeGatchi does in his blog), but I hope this was enough to get you started and interested in the topic plus see how powerful foundry
can be and how important it is to know the tools you are using.
Bonus
As you may see, this output is quite horrible to read, so we can use the foundry
toolset to make it more readable.
cast pretty-calldata <calldata>
This will return:
Possible methods:
- mint(uint8,uint256,address)
------------
[000]: 0000000000000000000000000000000000000000000000000000000000000001
[020]: 0000000000000000000000000000000000000000000000000000000000000017
[040]: 0000000000000000000000009965507d1a55bcc2695c58ba16fb37d819b0a4dc
Day 05 - 3 attack vectors
Today I was lucky to get in time to an @opensensepw discord study session where @mis4nthr0pic was going through a couple of attack vectors in the Solidity-Security-Compendium
Signature Malleability
This is quite a math heavy thing when you start looking into how it actually works and why, but for the context of an auditor it is enough to know a couple of things.
-
In Ethereum, the ecrecover function is used to verify signatures. It is a pre-compiled contract that performs public key recovery for elliptic curve cryptography. This means it can recover a public key (address) from a given signature.
-
In ECDSA, a digital signature is represented by a pair of values
(r, s)
. These values, along with another value called recovery identifier(v)
, are crucial to the signature verification process.- last one byte —
v
- first 32 bytes —
r
- second 32 bytes —
s
- last one byte —
v
is the last byte, while r
and s
is simply the signature split in half. The v
is the recovery ID, which can have two values: 0
or 1
, corresponding to two sides of the y-axis.
By knowing this couple of things (and as far as I understood), you will have problems if the only thing you check to verify the signature is the r
and s
values. This is because the v
value can be manipulated to make the signature valid because of the symetrical nature of the curve.
Nowadays OpenZeppelin has a library that will make sure you are not vulnerable to this attack vector.
Oracle Price Manipulation
I didn’t find this one very interesting because I don’t think it is that feasible nowadays, there are multiple solutions to this and the most basic and the one that we have been using for a while is to use a Chainlink oracle, which solves this by using a decentralized oracle network.
Basically consists of a malicious actor that will manipulate the price of an oracle to perform a favorable trade on certain token pair.
Controlled DelegateCall
This one was my favourite because I have been tinkering around with calldata lately, and this attack vector exactly shows you that by crafting malicious calldata you can exploit a contracts functionality.
This attack could lead to:
- Unauthorized execution of functions, potentially leading to theft of funds, manipulation of contract state, or other unintended consequences.
- Complete contract takeover, giving the attacker control over the contract’s functionality and stored assets.
Talk is cheap, show me the code
pragma solidity ^0.8.0;
contract DelegateCallProxy {
receive() external payable {}
function execute(address _target, bytes memory _data) public payable {
(bool success, ) = _target.delegatecall(_data);
require(success, "DelegateCallProxy: call failed");
}
}
If you see something like this somewhere, first of all, feel sorry for the dev that will get fired, secondly, create a POC that show how you exploit this and contact the dev team.
Our attack contract will look like this:
pragma solidity ^0.8.0;
contract Attack {
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
function attack(bytes memory _data) external payable {
// Attack logic
}
function stealFunds() external payable {
owner.transfer(address(this).balance);
}
}
The steps will be the follwing:
- People send money to the DelegateCallProxy contract.
- The attacker deploys the Attacker contract.
- The attacker crafts a malicious payload to call the stealFunds function of the Attacker contract using delegate call.
- The attacker calls the execute function of the DelegateCallProxy contract, passing the address of the Attacker contract and the malicious payload as arguments.
- The DelegateCallProxy contract performs a delegate call to the Attacker contract, executing the stealEther function in the context of the DelegateCallProxy contract’s storage.
- The stealEther function transfers the DelegateCallProxy contract’s balance to the attacker’s address.
If you want to know how to perform step 3, you can check my previous post about Calldata & Foundry
Wrapping up
I hope you’ve learned something in this post.
If you didn’t already, I highly recommend to join the @opensensepw discord server, they are a great community and they are always willing to help, plus you will have the opotunity to also join this study sessions.
Day 06 🔥 - VIP_Bank
Recently I discovered on Twitter this challenges page from Quill Audits, and I decided to give it a try.
I started with the VIPBank challenge, which is a smart contract that allows you to deposit and withdraw funds from it. The challenge consists on locking the contract, aka DoS attack.
The contract
pragma solidity 0.8.7;
contract VIP_Bank{
address public manager;
mapping(address => uint) public balances;
mapping(address => bool) public VIP;
uint public maxETH = 0.5 ether;
constructor() {
manager = msg.sender;
}
modifier onlyManager() {
require(msg.sender == manager , "you are not manager");
_;
}
modifier onlyVIP() {
require(VIP[msg.sender] == true, "you are not our VIP customer");
_;
}
function addVIP(address addr) public onlyManager {
VIP[addr] = true;
}
function deposit() public payable onlyVIP {
require(msg.value <= 0.05 ether, "Cannot deposit more than 0.05 ETH per transaction");
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public onlyVIP {
require(address(this).balance <= maxETH, "Cannot withdraw more than 0.5 ETH per transaction");
require(balances[msg.sender] >= _amount, "Not enough ether");
balances[msg.sender] -= _amount;
(bool success,) = payable(msg.sender).call{value: _amount}("");
require(success, "Withdraw Failed!");
}
function contractBalance() public view returns (uint){
return address(this).balance;
}
}
The attack
DoS attacks (at least the ones I’ve seen for now) are always based in the same principle, which is using the selfdestruct()
method on an attack contract, which will force send all the funds to the victim contract even though this one won’t accept incoming funds.
In this case, just by sending more than maxETH
to the VIP_Bank
contract, we will be able to lock it.
pragma solidity 0.8.7;
contract Hack {
constructor() {}
receive() external payable {}
function hack(address _Bank) public {
selfdestruct(payable(_Bank));
}
}
POC
This is really the meat of this challenge for me, as I want to practice using the foundry
framework.
We will write some tests that will prove that the VIP_Bank
contract is in fact working properly and also show that once we use our Hack
contract to perform the DoS attack, the contract will be locked.
Tests initial setup
Pretty standard stuff when using foundry
, we create a new instance of the VIP_Bank
contract and the Hack
contract and use the setUp()
function which is called before each test.
pragma solidity 0.8.7;
import "../src/VIPBank.sol";
import "../src/Hack.sol";
import "forge-std/Test.sol";
contract TestVIPBank is Test {
VIP_Bank public vipBank;
Hack public hack;
function setUp() public {
vipBank = new VIP_Bank();
hack = new Hack();
}
Test correct functionality
This is the test that will ensure that the contract is functional and does what it is suposed to do.
function test_canWithdraw() public {
vipBank.addVIP(address(1));
vm.deal(address(1), 0.1 ether);
vm.startPrank(address(1));
vipBank.deposit{value: 0.05 ether}();
vipBank.withdraw(0.05 ether);
vm.stopPrank();
}
Test DoS attack
And this one will show that the contract is locked after we call our Hack
contract.
function testFail_cannnotWithdraw() public {
vipBank.addVIP(address(1));
vm.deal(address(hack), 0.1 ether);
vm.deal(address(1), 0.6 ether);
vm.startPrank(address(1));
for (uint256 i = 0; i < 10; i++) {
vipBank.deposit{value: 0.05 ether}();
}
hack.hack(address(vipBank));
vipBank.withdraw(0.05 ether);
vm.stopPrank();
}
Ideally we should simulate this test without giving address(1)
VIP status, but it doesn’t really matter, anyone could run the hack()
function on the Attack
contract.
Wrapping up
As I’m still learning foundry
this is the way I found to do it, but I’m sure there is a better way, if you know it, please let me know by hitting a DM @mariodev__ on Twitter.
Day 07 - One of those days
Can’t share much today, I had such a busy day today that I could only dedicate 1 hour to my learning journey.
Still, managed to discover a new site that perfectly fits my needs for now: EVM Through CTFs
I will go through the first challenge tomorrow and hopefully finish it by the end of the day so I can write about it in the next post.
Day 08 - Deeper into the EVM
As we were saying…
Today I went on and tried the EVM Through CTFs challenges and, oh god are they good.
Even though the first challenge is a bit of a warmup (especially if you read my previous post Day04 - Calldata & Foundry) it rapidly gets harder and harder, I’m actually stuck in the last part of challenge one, but I’ll get there.
Opcodes & Stack resources
The first thing I did was to look at the EVM Opcodes and evm.codes webpages, I think they are the best and only resource you really need to understand how evm works.
Personally I prefer the playground provided in evm.codes so that’s the one I use the most.
The challenge
You are presented with this unverified contract and you need to understand what it does and finally build your own calldata
so the contract returns a certain value.
The solution
Testing the grounds
To understand what the contract does you need to understand the opcodes
and the stack
and so for that we have the evm.codes playgroung.
Simply paste the bytecode of the contract, change the dropdown option to mnemonic
and you’ll see the opcodes
that result from that bytecode, paste the calldata
you want to test and hit run, you’ll see the stack
and the memory
after each opcode
is executed.
When pasting the calldata
0xb40d7b75
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000004
I was getting the value 0x1c
which in decimal is 28
, but still this value could be achieved by many different ways so I needed to see how it was getting there. (I won’t go into the details of how I built the calldata
, see my previous post Day04 - Calldata & Foundry for that)
Understanding the contract
I don’t think it makes sense here to go opcode
by opcode
and explain what they do, If you want you can take the contract and try to understand it yourself by doing what I said in the previous section.
The contract bascially run a series of operations to properly get the arguments we passed in the calldata
and do some operation with them. In this case the formula it was running to get the result was
a * 8 + b` -> `3 * 8 + 4 = 28
So to get the value 13
we need to pass a = 1
and b = 5
in the calldata
.
0x
b40d7b75
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000005
Wrapping up
Sorry if you expected a super detailed description on how each opcode
works, but I think the best way to learn is to try it yourself and see how it works.
Today was quite a good day for me, I now feel more confident with the EVM and how to interact with low level solidity.
Day 09 - Kinda busy
Today I managed to finish the first challenge of EVM Through CTFs completely, I’m now going through a pretty tough one that seems like it is going to be quite a good learning moment, so be sure I’ll be posting about it.
Then I met a friend and we kept chatting until 21:30, I’m pretty tired now but I’ll try to get some sleep and then get back to work tomorrow.
Also met some people in discord and we are seeing if we can collaborate on some audits together, I’m pretty excited about that.
One last thing
As you can see, I already had 2 days which have not been really that productive and even though I would’ve liked to have done some more stuff related to auting, I think it’s better to take a break and get some rest, meet your friends and family and I’m sure you’ll get back to work with more energy and motivation.
Day 10 - Same bytecode, different code
Continuing with the EVM Through CTFs challenges, today I was able to solve a pretty interesting challenge.
The challenge
In this case the challenge consisted on winning a game agains a bot smart contract (khabib) at the game 0xships.
The thing is that the bot khabib is quite a good player and his code is pretty complex, so trying to build your own bot was completely out of the equation.
This is khabib’s code:
0x608060405234801561001057600080fd5b50600436106100415760003560e01c806306fdde0314610046578063436cc795146100775780638da5cb5b14610098575b600080fd5b604080518082018252600681526535b430b134b160d11b6020820152905161006e919061065b565b60405180910390f35b61008a6100853660046106a9565b6100b3565b60405190815260200161006e565b6000546040516001600160a01b03909116815260200161006e565b60008360ff036100cf576100c8868585610289565b905061027f565b600684901c6000819003610119576100e987866001610310565b15610101576100f9878686610289565b91505061...
Hehehehe, yeah, thats part of the challenge, you have to think of a way to beat him without having access to his high level code.
As we are doing a great job at understanding the EVM, this is not going to be a problem, right?
Well… let’s see.
The solution
First we need to learn how to make a smart contract have the same bytecode as another one.
pragma solidity ^0.8.17;
contract ShipSolution {
constructor() {
bytes memory bytecode = hex"60806040523480156100...";
assembly {
return(bytecode, mload(bytecode))
}
}
}
This is how you do it, you just need to copy the bytecode of the contract you want to reproduce and then return it in the constructor, but there is a catch, can you see it?
Well, take into account that we are doing low level stuff, therefor we must be very explicit in how we want our code to behave.
The catch
Ask yourself, who is the owner of the contract?
We need to trace down the owner function in the original bytecode from khabib and see how it behaves.
Go and copy the bytecode from khabib’s contract and paste it in evm.codes, then search for the owner function and you will find that the owner function is simply returning the 0th slot of storage.
To fix this we need to slightly modify our code:
pragma solidity ^0.8.17;
contract ShipSolution {
address public owner;
constructor() {
owner = msg.sender;
bytes
memory bytecode = hex"60806040523480156100...";
assembly {
return(add(bytecode, 0x20), mload(bytecode))
}
}
}
Explanation
So, why is this the solution?
It is not difficult to understand if you think about it, the owner function is returning the 0th slot of storage, so we need to make sure that the 0th slot of storage is the address of the contract creator, being an address means that it is 20 bytes long.
Important note
Something that I didn’t mention is, we were able to take the bytecode from khabib and paste it into evm.codes and work with it because of the fact that it was an unverified contract.
Unverified only show the runtime bytecode (which in this case it is what we need). Verified contracts on the other hand, show both initialization and runtime bytecode and it is not possible to distinguish between them (at least as far as I know).
Wrapping up
Challenges from evm through ctf are getting harder and harder, but I’m really enjoying them, I’m learning a lot and I’m sure I’ll be able to apply this knowledge in the future.
In case you wonder how are this special contracts called, they are called metamorphic contracts and if you found this interesting, I encourage you to read more about them.
Day 11 - Code is not law
Going back again with yet another challenge from EVM Through CTFs, but this time, I don’t have a solution yet, I’m still working on it.
Still, will share the point at which I’m at right now so in the future I can look back and see how stupid I was hehe.
The challenge
Today’s challenge consists on getting CodeIsNotLaw
tokens from a minter contract, but it is not as straight forward as calling the mint function.
Here is the contract:
contract CodeIsNotLaw is ERC20("CodeIsNotLaw", "CINL", 18) {
address public immutable onlyImAllowedToHaveTokens;
bytes32 public immutable correctCodeHash;
constructor() {
onlyImAllowedToHaveTokens = address(new OnlyImAllowedToHaveTokens());
correctCodeHash = getContractCodeHash(onlyImAllowedToHaveTokens);
}
function mint(address receiver) external {
require(
getContractCodeHash(receiver) == correctCodeHash,
"code hash does not match"
);
if (balanceOf[receiver] == 0) {
_mint(receiver, 1);
}
}
We need to have the correct code hash to be able to mint tokens, therefor, we cannot call this function from our EOA, we need to call it from a contract, plus, this contract must have the exact same bytecode as the OnlyImAllowedToHaveTokens
contract, which is this one:
contract OnlyImAllowedToHaveTokens {
function bye() external {
selfdestruct(payable(msg.sender));
}
}
I you read my previous post you may have an idea on how to first approach the challenge, and you would be correct but still there is the tricky part of, once you have the right code hash, how do you call the mint function from a contract with the same bytecode as the OnlyImAllowedToHaveTokens
contract?
The solution
I know it must be from the constructor where we need to call the mint function, but I don’t know why it is failing…
Will keep working on it and update this post once I have a solution.
Day 12 - Weponizing Gas
Today, reading through some past reports on Solodit I found out about this interesting exploit that is known under the name of 1/64 rule.
The EVM limits the total gas forwarded on to 63/64ths of the total gasleft()
(and silently reduces it to this value if we try to send more)
Example
This is the report from Sherlock that I was reading.
I still don’t fully understand how this works though, but I’ll try to explain it as best as I can.
The exploit
Something interesting I didn’t know about Optimism:
“In OptimismPortal
there is no replaying of transactions. If a transaction fails, it will simply fail, and all ETH associated with it will remain in the OptimismPortal
contract.
Users have been warned of this and understand the risks, so Optimism takes no responsibility for user error.
In order to ensure that failed transactions can only happen at the fault of the user, the contract implements a check to ensure that the gasLimit
is sufficient”
require(
gasleft() >= _tx.gasLimit + FINALIZE_GAS_BUFFER,
"OptimismPortal: insufficient gas to finalize withdrawal"
);
The problem here is that the EVM has a specification that limits the amount of gas that can be sent to an external function call. This limit is set to 63/64ths of the total gas_left()
at that point in the contract execution.
However, when the gas limit is very large, the remaining 1/64th of gas can be greater than a pre-defined value called the FINALIZE_GAS_BUFFER. When this happens, the amount of gas forwarded to the external function call would be less than what was actually specified by the contract.
As a side note, this is one really good use case for fuzz-testing.
Wrapping up
If you want to see the POC of this exploit, you can check it out in the Sherlock report. It is quite long but really interesting (and the report is using foundry ;D )
Day 13 🔥 - Huff, learning & bonuses
Yooooo, first of all I want to thank anyone reading this, it keeps me motivated knowing that there is someone out there reading my posts so, thank you!
Shoutout to @0xMackenzieM who talked to me via DM giving me some feedback on my approach with this blog. If I ever get nearly half on the knowledge he has, don’t doubt a single second that I will pay it forward.
Now to the huff stuff.
Huff rocks
Finally I found out a repo with huff challenges here
I will skip the first “challenge” because it is not even a challenge, it is just a tutorial on how to test the rest of the challenges, so I will start with the CallValue challenge.
The challenge
The task is to write within the MAIN
macro, huff code to get and return the amount of ether sent as part of that call.
Basically, return the msg.value
we have in solidity but using huff.
The solution
When first looking at huff, I thought their docs were going to be my very best friend, but I was wrong, the real docs that you want to suck yourslef into are the EVM opcodes docs. (I mean, their docs are helpful, but you must first dig into some opcode stuff to feel a bit more confortable with huff)
Talk is really cheap, show me the code
#define macro MAIN() = takes(0) returns(0) {
// store CALLVALUE in memory at offset 0
callvalue // [CALLVALUE]
0x00 // [0x00, CALLVALUE]
mstore // []
// return 32 bytes of memory starting at offset 0
0x32 // [0x32]
0x00 // [0x00, 0x32]
return // []
}
Not really that difficult (if you know how the EVM works) even comments explain the code really clearly, so I will be doing the third one as a bonus which is the one in which I made the first mistake, a silly one, but really important and fundamental.
Bonus[0]
The task is to write within the MAIN
macro below, huff code that retrieves the ether balance of the address that sent the transaction, also known as msg.sender in solidity.
#define macro MAIN() = takes(0) returns(0) {
// Store the balance of the sender in memory at offset 0
caller // [CALLER]
balance // [BALANCE, CALLER]
0x00 // [0x00, BALANCE, CALLER]
mstore // []
// return 64 bytes of memory starting at offset 0
0x40 // [0x40]
0x00 // [0x00, 0x40]
return // []
}
This is the correct solution, but it is more interesting to see the mistake I made.
#define macro MAIN() = takes(0) returns(0) {
// Store the sender in memory at offset 0
caller // [CALLER]
0x00 // [0x00, CALLER]
mstore // []
// Store the balance in memory at offset 1
balance // [BALANCE]
0x01 // [0x01, BALANCE]
mstore // []
// return 64 bytes of memory starting at offset 0
0x40 // [0x40]
0x00 // [0x00, 0x40]
return // []
}
This was my first approach, but the thing I was not getting here is the offset
(even though in the first challenge I thought I got it). I was seeing it as array indexes, and they, first of all, are hex
values and, second of all, they are bytes
not indexes
, each variable has its own size, so the offset
is the number of bytes that you want to skip in memory to place that item, not the index of some imaginary stupid array.
I was really mixing concepts here, why the 0x40
made sense in my head, but the 0x00
and 0x01
didn’t? No idea to be honest. Learning consists on this.
- You think you know something
- Then you don’t
- Then you learn it
- Back to step 1
I was suppose to already know this if you read some of my past posts, but as you can see, until you don’t actually get your hands dirty, you don’t really learn.
It is also really useful to get familiar with counting in hex, because you will be doing it a lot, and will save you some headhaches.
Bonus[1]
Today with the help of ChatGPT I made this
And as promised, here is the code
Bonus[2]
This bonus is useless, but I realized once GPT already spit out the last prompt result so yeah… I can conclude from this that AI is so good and quick, that I couldn’t even take the time to think the usefulness of this script hehe.
Here is the tweet where I’m happy but not happy -> tweet
And the code
Wrapping up
Well what a day, I did some really useless stuff, learned some huff, and reinforced some knowledge too.
As you could see today, even things that you are supose to know, will take time until you actually internalize them, so don’t be afraid to make mistakes, because you will learn from them.
Hope you learned something new with me today, and I will see you tomorrow!
Day 14 - Moar huffing
I couldn’t get too much done today, made a very hard and sad decision and… well, I needed to rest and take some time for myself.
So I can only show what I did today, which is not much, and actually has been between today and yesterday.
More huff challenges
I was only able to finish the Multiply challenge from the repo I shared in the previous post.
he task is to write within the MAIN
macro below, a function named multiply
that takes in 2 uint256s, and returns their product. Be sure to revert on overflow.
#define function multiply(uint256, uint256) payable returns(uint256)
#define macro MAIN() = takes(0) returns(0) {
0x04 calldataload // [arg1]
0x24 calldataload // [arg2, arg1]
mul // [arg1 * arg2]
dup1 // [arg1 * arg2, arg1 * arg2]
iszero // [arg1 * arg2 == 0 ? 1 : 0, arg1 * arg2]
multiply // [multiply, arg1 * arg2 == 0 ? 1 : 0, arg1 * arg2]
jumpi // if arg1 * arg2 == 0, jump to multiply
// [arg1 * arg2]
dup1 // [arg1 * arg2, arg1 * arg2]
0x04 calldataload // [arg1, arg1 * arg2, arg1 * arg2]
swap1 // [arg1 * arg2, arg1, arg1 * arg2]
div // [arg1 * arg2 / arg1, arg1 * arg2]
0x24 calldataload // [arg2, arg1 * arg2 / arg1, arg1 * arg2]
eq // [arg1 * arg2 / arg1 == arg2 ? 1 : 0, arg1 * arg2]
multiply // [multiply, arg1 * arg2 / arg1 == arg2 ? 1 : 0, arg1 * arg2]
jumpi // if arg1 * arg2 / arg1 == arg2, jump to multiply
// [arg1 * arg2]
0x00 mstore
0x20 0x00 revert
multiply:
0x00 mstore
0x20 0x00 return
}
This one was a hell of a ride, and hard af to solve, it really took some time. It was key to be really careful with the order of the operations, and to understand what was going on, with the comments I added.
Still, the repo, when running the tests for this challenge, has 1 passing test and another failing one that has to do with the function selector. I can guess why it is failing but don’t know how to solve it.
Wrapping up
Well not much today, but I’m happy with what I did. I’m still learning a lot, and I’m really enjoying this. I’m also happy with the progress I’m making, and I’m really looking forward to the next days when hopefully I’ll be able to do some pair auditing.
See you soon!
Day 15 - Impresive guy & job hunting
Damn… Today found out about @IAm0x52 and this tweet he published a few days ago tweet
Went on and read some of his reports using solodit by filtering the finder field writing 0x52 and I was really impressed by the amount of bugs he found.
I wonder if I will be able to take private audits in a year, that’d be a dream come true to be honest, really looking forward to be auditing as my day job.
Speaking about jobs, why the fuck do companies have open position pages on their website, if when you apply then they take like 2 weeks to asnwer…
I need a job, so I started searching for some entry level solidity developer roles (I’ve only been deving in solidity seriously for the past year) in the web3jobs website, but as I said, already 2 weeks passed and didn’t get any answer from neither, so yeah… will keep applying.
Wrapping up
No techy stuff today, even though I kept playing with some huff, nothing worth sharing, chill day today.
Day 16 - They see me huffing, they hatin’
Hehe sorry, but I can’t stop playing with huff, It is cool when you start to be able to fix your own mistakes, I think that’s a point in which you are starting to understand new stuff.
So today I bring two more challenges from the repo I shared in a previous post.
Non-Payable
The task is to write within the MAIN
macro, huff code that reverts if ether is sent with the transaction. In solidity terms, revert if msg.value > 0
.
#define macro MAIN() = takes(0) returns(0) {
callvalue iszero // [callvalue == 0 ? 1 : 0]
ok // [ok, callvalue == 0 ? 1 : 0]
jumpi // []
0x20 0x00 revert // Revert, callvalue != 0
ok:
0x01 0x00 return // Return 1, callvalue == 0
}
Not very difficult, but this is meant to be an introduction to the OPCODE jumpi
(jump if), which is the equivalent to solidity’s if
statement (there is also the jump
OPCODE which is a forced jump)
If you see my previous post on the Multiply challenge, you’ll see that I needed to separate each instruction with a new line, now I’m starting to keep some instructions together in the same line because I’m getting more familiar with some combinations of OPCODEs and how they interact.
Understadning JUMPI
It is really simple, just like many OPCODEs like retur
, iszero
… jumpi
takes two arguments, the first one is the JUMPDEST
(the jump destination/label) and the second one should be either 0
or 1
, if it is 1
it will jump to the JUMPDEST
and if it is 0
it will continue with the next instruction.
So it is crucial that the instruction right before the jumpi
is a label and there is already in the stack either a 0
or 1
resulting from instructions iszero
, eq
…
FooBar
The task is to write within the MAIN
macro below, huff code that mimics 2 solidity functions.
- One named
foo()
that simply returns 2, - the second named
bar()
that simply returns 3.
#define function foo() payable returns(uint256)
#define function bar() payable returns(uint256)
#define macro MAIN() = takes(0) returns(0) {
// Get function signature
0x00 calldataload // [calldata]
0xe0 shr // [__FUNC_SIG]
// dup1 __FUNC_SIG(foo) eq foo jumpi
dup1 // [__FUNC_SIG, __FUNC_SIG]
__FUNC_SIG(foo) // [foo, __FUNC_SIG, __FUNC_SIG]
eq // [foo == __FUNC_SIG, __FUNC_SIG]
foo // [foo, __FUNC_SIG]
jumpi
// dup1 __FUNC_SIG(bar) eq bar jumpi
dup1 // [__FUNC_SIG, __FUNC_SIG]
__FUNC_SIG(bar) // [bar, __FUNC_SIG, __FUNC_SIG]
eq // [bar == __FUNC_SIG, __FUNC_SIG]
bar // [bar, __FUNC_SIG]
jumpi
0x00 0x20 revert // Revert if unrecognized function is called
foo:
pop
0x02 0x00 mstore
0x20 0x00 return
bar:
pop
0x03 0x00 mstore
0x20 0x00 return
}
Uh pretty scary right? No worries, let’s break it down.
Getting the function signature
// Get function signature
0x00 calldataload // [calldata]
0xe0 shr // [__FUNC_SIG]
Here we are getting the first 4 bytes of the calldata
(the data sent to the contract) which is the function signature, and we are storing it in the stack.
The shr
OPCODE is a bit tricky, it shifts bits to the right.
We obtain calldata which is 32
bytes long, and we want to get the first 4 bytes, so we need to shift 28
bytes to the right, which is 28 * 8 = 224
bits = 0xe0
in hex, and that gets stored in the stack.
Jumping to the right function
dup1 __FUNC_SIG(foo) eq foo jumpi
dup1 __FUNC_SIG(bar) eq bar jumpi
In the code I broke it down so it is easier to follow the stack state, but look how simple it is when you put it all together, rigth?
The thing to take from this one is that you will be using dup1
OPCODE a lot, it duplicates the first element in the stack and this is key because when you perform an operation like eq
or iszero
, this will take values off the stack and you need to keep the original value to perform the jump.
Returning values
foo:
pop
0x02 0x00 mstore
0x20 0x00 return
bar:
pop
0x03 0x00 mstore
0x20 0x00 return
This is pretty straight forward, we are just storing the value we want to return in memory and then returning it depending on the function signature. we obtained previously.
Wrapping up
I’m starting to get the hang of it, I’m still not able to write “production ready” (even though huff devs don’t recomend it but you get my point) code, but I’m getting there.
Hopefully I can start writting about my experience with the Secureum RACE I just joined plus some POCs, I think those are very interesting to share and also I’m aware that huff posts are not very popular, but I’m having fun and I hope you are too.
Day 17 - Huff magic continues
Hey there, huff enthusiasts! I’m back with more huff adventures and some insights into storage in Ethereum smart contracts. I can’t wait to share them with you! I’m still on cloud nine from my recent progress in understanding huff and the EVM. So, today, let’s dive into storage and how it works in Ethereum smart contracts.
Ethereum Smart Contract Storage
In Ethereum, smart contract storage is a key-value store that allows you to store and access data persistently. The storage is part of the blockchain’s state, meaning that once you store data in it, it will remain there indefinitely until you explicitly modify or delete it. However, keep in mind that modifying storage is costly in terms of gas, so it’s essential to optimize storage usage in your smart contracts.
Ethereum smart contract storage is organized as a sparse 32-byte
key-value store, which means that it can handle 2^256
different keys. In practice, the number of keys a contract can use is far less than this theoretical maximum, but it’s still quite large.
In Solidity, you can interact with storage using for example state variables. These high-level construct abstracts away the details of storage and automatically generate EVM bytecode to handle storage operations using SSTORE
and SLOAD
opcodes.
In huff, we use SSTORE
and SLOAD
to directly interact with storage.
The challenge
The task is to write within the MAIN
macro 2 functions…
- One named store()
that takes one function argument and stores it in storage slot 0,
- the second named read()
that simply returns what is stored at storage slot 0.
Storing data in contract storage
To store something in contract storage, we first load it from calldata, add the SLOT0
pointer to the stack, and then use the SSTORE
opcode:
store:
pop
0x04 calldataload // [uint256]
[SLOT0] sstore // [SLOT0, uint256] -> []
stop
Reading data from contract storage
To read from storage, we use the SLOAD
opcode to load the value. This will add the value to the stack, and then we can use the MSTORE
opcode and return the value like a pro:
read:
pop
[SLOT0] sload // [uint256]
0x00 mstore // []
0x20 0x00 return
What’s that [SLOT0]?
Good question, I’m glad you asked! The SLOT0
is a FREE STORAGE POINTER. It’s a constant value that can be used to access the storage of the contract, it is part of huff. You can take a look at huff docs to learn more about it.
The pop statement is there for the same reason as in the previous challenge, go check it out if you haven’t already!
So the final code will look like this:
#define function store(uint256) payable returns()
#define function read() payable returns(uint256)
// To work with storage we need to declare a FREE STORAGE POINTER
#define constant SLOT0 = FREE_STORAGE_POINTER()
#define macro MAIN() = takes(0) returns(0) {
// Get function selector
0x00 calldataload
0xe0 shr
dup1 __FUNC_SIG(store) eq store jumpi
dup1 __FUNC_SIG(read) eq read jumpi
0x00 0x20 revert
// To store someting in storage we need first to load from calldata
// then add to the stack the SLOT0 pointer and then use the SSTORE opcode
store:
pop
0x04 calldataload // [uint256]
[SLOT0] sstore // [SLOT0, uint256] -> []
stop
// To read from storage we need to load the value with SLOAD
// This will add it to the stack and then we can use the MSTORE opcode
// and return as usual (remember that return, returns from memory)
read:
pop
[SLOT0] sload // [uint256]
0x00 mstore // []
0x20 0x00 return
}
Wrapping Up
My huff skills are leveling up with each challenge, and although I’m not quite at the “production-ready” stage, I’m certainly getting closer. I hope these posts are as enjoyable for you as they are for me, and that we can learn together!
Hopefully I can start bringing back some more solidity and foundry tips, I also really enjoy that, but as that’s not as new as huff to me, well I’m enjoying huff more for now hehe.
Day 18 - Keep the job hunt going
Hi everyone, got some semi-good news, let me break them down.
I’m currently applying for a job, this was my idea at the start of this blog project, I want this blog to be the most valuable resource in terms of veracity and an actual day to day journey of a guy with experience in programming and web3 but a total noob in the security space, and how he managed to get a job in the security space.
Currently I’m tracking the companies I’ve applied in a Notion database with some basic data to keep tack of the process.
Why semi-good news?
Well, if you are a daily reader, there might be some days that I might not post anything, or catch up at a later date and post somthing with the corresponding missed date (hope I explained myself XD).
The next challenge
The next challenge is not about the evm nor about huff, I will try to start bringing good and valuable POCs for vulnerabilities I find during my study period of past reports and writeups with my newly met people in the opensense community, that’s why posts might take longer to be published.
Wrapping up
So from now on, more POCs and also updates on how the job hunt is going.
I have no idea how many people are reading this daily or if anyone is reading this at all, but if you are, it’d be super awesome if you could just send a DM hehe, it would motivate me to keep going.
PS: This posts were written a while ago, I do have a job now (2 actually hehe)
Day 19 - 1 wei attack
The learning is starting to flourish XD. As I told you in a previous post, I joined a team of people for auditing and I’m learning a shit ton of stuff, which of course, will also share with everyone reading this posts of mine hehe.
ELI5 1 wei attack
First of all this is also known as inflation attack on EIP-4626.
In simple terms, the inflation attack on EIP-4626 is an exploit where an attacker manipulates the exchange rate between a vault’s shares and its underlying assets, causing a victim to lose their deposited assets.
This is made possible due to the lack of slippage protection in the deposit function when a vault has low liquidity.
Here’s a step-by-step illustration of the attack:
- Alice wants to deposit 1 token into the vault.
- The vault is initially empty, and the default exchange rate is set (usually 1 share per asset).
- Bob, the attacker, sees Alice’s transaction in the mempool (a pool of pending transactions) and decides to sandwich it.
- Bob first deposits a small amount (1 wei) into the vault and receives an equivalent amount of shares.
- The exchange rate remains 1 share per asset.
- Bob then transfers 1 token directly to the vault using an ERC-20 transfer, without receiving any shares.
- The exchange rate now increases, and Alice’s 1 token deposit is worth much less in shares.
- Alice’s deposit is executed, but she receives very few or no shares in return, essentially donating her tokens to the vault.
- Finally, Bob redeems his small number of shares and receives all the assets in the vault, including his own deposits and Alice’s tokens.
How to prevent it?
The proposed mitigation for this attack is to introduce a “decimal offset” along with “virtual assets and shares” to better define the exchange rate, even when the vault is empty.
This would make it more difficult for an attacker to manipulate the exchange rate and would require a significantly larger donation to inflate it.
Day 20 - Adversarial thinking
Today, the auditing team and I got started with the Code4rena audit for EigenLayer and even though it is not live yet (couple of hour remaining), we started to take a look at their public repo to see what we can encounter.
The team is key
I’m super grateful for the opportunity to be part of this team, I’m learning a lot from them, and I’m sure I will learn a lot more in the future, they are really welcoming for newcomers like me, and don’t hesitate to answer any question I might have and share their knowledge with me.
What I wanted to share today is the thought process that a really skillful guy from the team shared and that it seems to have unlocked a new way of thinking for me too.
Simple but incredible advice
So this guy was looking at a piece of code, to be precise, this piece of code:
if (totalShares == 0) {
newShares = amount;
} else {
uint256 priorTokenBalance = _tokenBalance() - amount;
if (priorTokenBalance == 0) {
newShares = amount;
} else {
newShares = (amount * totalShares) / priorTokenBalance;
}
}
What was his thought process? Well, pretty simple if you think about it, but until you dont have that click in your head, you won’t be able to think like that.
Almost literal words from him:
“We have *
and /
operators, those are specially vulnarable to 0
values, what can happen if either totalShares
or priorTokenBalance
are 0
?, and HOW CAN WE GET THE SMART CONTRACT TO THAT STATE?”
This was what clicked for me, see what may break the contract (even if at first sight it seems impossible), and then, how can we get the contract to that state, that’s what adversarial thinking is all about.
Wrapping up
Hope you also got the click in your head, and if you didn’t, don’t worry, it will come to you eventually, just keep pushing.
Thanks for reading!
Day 21 - The End
I’m happy on how this turned out, I shared a 20 day journey where I went from 0 auditing to getting an actual team, I know I said I would share about the projects we audited, but that won’t happen, I’m sorry, I have other plans for both the blog direction and also my personal career.
Auditing is fun, and more so doing it on a team of such cool people, but it requires a huge mental which I don’t have right now.
It’d be awesome for me to be able to take private audits from time to time to get some side income, but getting it as a full time job is not for me.
I hope this past 20 days have been useful for you, and if you have any questions, feel free to reach out to me on twitter @mariodev__.