A-MAZE-X CTF Walkthrough | Part I

matta @ theredguild.org
7 min readSep 22, 2022

--

This post belongs to a series of articles dedicated to solving the DeFi challenges hosted by Secureum at Stanford University on the past 26 of August.

Additionally, this series is part of a greater series called A journey into smart contract security.

A-MAZE X CTF
Image extracted from LiveOverflow’s YT channel

Table of contents

1. Challenge0.VToken — VitaToken seems safe, right?
1.1 Challenge description
1.2 Dissecting the code
2. The vulnerability
3. The exploit
3.1 The Exploit (in Solidity)

Challenge0.VToken — VitaToken seems safe, right?

Now, before jumping to the challenge description, let's do some thinking first. I encourage you to pause, and really think every time you see this emoji near a paragraph: 💭.

— By looking at the name of the file, and the title of this challenge, what can you conclude? — think. 💭

— That it's going to be related to tokens?

— Great! What else?

— The name of the token appears to be VitaToken, maybe as a wink to Vitalik? And it implies is not safe.

— Excellent! Can you guess which kind of token will it be?

The most popular token contracts are ERC20, ERC721, ERC777, and ERC1155.

ERC20: the most widespread token standard for fungible assets, albeit somewhat limited by its simplicity.

ERC721: the de-facto solution for non-fungible tokens, often used for collectibles and games.

ERC777: a richer standard for fungible tokens, enabling new use cases and building on past learnings. Backwards compatible with ERC20.

ERC1155: a novel standard for multi-tokens, allowing for a single contract to represent multiple fungible and non-fungible tokens, along with batched operations for increased gas efficiency.

— If I have to guess, given our context, taking into account this is the first challenge, I'd say it's going to be an ERC20.

—Great guess! We'll see. Now, how can a token contract not be safe? 💭

— Uh, that's a tough one. Am I supposed to know all of this by this moment?

Not necessarily, but since we know these challenges try to simulate real case scenarios, we may be able to find common attack vectors.

Oh, ok… Can we go to the description now?

Challenge description

Let’s begin with a simple warm-up. Our beloved Vitalik is the proud owner of 100 $VTLK, which is a token that follows the ERC20 token standard. Or at least that is what it seems… 😉

Upon deployment, the VToken contract mints 100 $VTLK to Vitalik’s address.

Is there a way for you to steal those tokens from him? 😈

We basically got right all the things we imagined, but don't get so pompous, remember this is just a warm-up.

Dissecting the code

Let's begin with the contract header and the constructor.

Note: I'm not going to speak about SPDX-License-Identifier, pragmas, or imports because I'm assuming you already know everything about them. If that's not the case, I suggest you start with the first part of this document.

VToken implements the contract ERC20, imported from OpenZeppelin's contracts.

This is the third time I link to one of their sites. Their Solidity library is one of the best alternatives out there to secure-develop smart contracts. Having said that, we can rest assured that those contracts are secure by default (though security is a state, not an immutable attribute).

Do you know what ERC stands for, and where do the numbers come from?

An ERC (Ethereum Request for Comments) is a specific type of EIP (Ethereum Improvement Proposals). Its name came from the old-timer RFC (see below). In short, it's a technical specification for an on-chain application that becomes a standard. And you know what's cool about it? Anybody can create one to be discussed with others. Read more about it here.

A Request for Comments (RFC) is a publication in a series from the principal technical development and standards-setting bodies for the Internet, most prominently the Internet Engineering Task Force (IETF)

So, the constructor seems to be properly implemented, and the call to _mint() is as well. If you're using VSCode, and have OZ's contracts installed (which will be mandatory to compile the challenge), you can Ctrl + Click over the import, and browse the ERC20.sol contract file.

Some token implementations usually mint directly to msg.sender upon deployment, instead to a specific contract address.

The only thing that follows, and the last, is the approve()function. It's not an override from the original standard because the function header differs.

Check out the difference (_msgSender()is a wrapper for msg.sender).

_approve()basically approves an address, to be a spender over a number (amount) of tokens from a given owner.

Spoiler: the vulnerability to exploit is right here in this code.

Ok, I'm not actually spoiling since there's no more code to analyze, so you should've realized this by now 😅.

Can you see it? 💭

Let's think again. A few questions to prompt your mind a bit:
1. Who can call that function?
2. What parameters are under our control?
3. Can the difference from the original function affect its intended functionality somehow? why?

The vulnerability

The key difference between the implemented approve() function and the one inherited from the standard ERC20, is how owner is being handled upon being called in the underlining method _approve().

In the VToken contract, owner comes from a parameter under our control, and in the standard, it is always msg.sender.

Do you now see the issue? 💭

This allows us, to approve a transfer from any source of origin (owner), to any destination (spender), with any desired amount.

Let's see an example.

Ideally, if we were to gather Vitalik's tokens, we would need to ask him to approve them for us as a spender.

He would have to run:
vToken.approve(ourAddress, 100 ether)
This will end up calling:
_approve(msg.sender, ourAddress, 100 ether) // msg.sender == vitalik

And only then, we could transfer them to us.

vToken.transferFrom(vitalik, ourAddress, 100 ether)

But, since we have this other implementation that does not force us to use msg.sender as owner, we can directly run it ourselves.

approve(vitalik, ourAddress, 100 ether)This will end up calling:
_approve(vitalik, ourAddress, 100 ether)

And now you can finally execute the transferFrom()as before, getting all their tokens.

The exploit

For this, you will need to work on /test/solveChallenge0.js, where all the contracts that will be needed are already deployed.

Take your time to browse through the code and understand it, I know you will 😃. 💭

Done? Great.

If the contract has been deployed correctly — don't worry, this has been done already by the authors of the challenge —, you will be able to call all the functions directly.

Butand this is a huge but that I understood recently—if you for example call vtoken.approve()(with any parameters of your liking) it will always fail with something like "function is not defined", and if you call any other function it does not. Why? 😞

Since I couldn't bear not knowing the hell was happening, I started to debug the code and reached a conclusion: It only happens with contracts whose functions have their names repeated at least once, like approve in our case.

Let me introduce you to: 🌟function overloading🌟

Solidity allows overloading functions by types, and Javascript does not support it. In consequence, ethers.js just takes away the possibility to call any function that has been overloaded (repeated its name).

I could not find an explanation on this matter in ethers current documentation, so I opened an issue suggesting it to be added, and created a proof of concept repository along with it.

To access overloaded functions, use the full typed signature of the functions (e.g. contract["foobar(address,uint256)"]).

Great, so the two current alternatives are as follows:

vtoken["approve(address,address,uint256)"](owner, spender, amount)vtoken.transferFrom(owner, spender, amount)

We will be using the previously defined variables, and ethers.utils.parseEther()to convert the number of tokens to the actual representation (which coincidentally uses the same amount of decimals as ether to wei). The complete Javascript code is below.

And that's it! Congratulations!

If you run npx hardhat test test/solveChallenge0.js you should see we passed the test, meaning we solved the challenge! 🎊

Did you know that you could run hardhat with a shorthand?: hh
We will be using it from now on. Configuration here.

— Awesome, but can we use a contract instead? I see there's an Exploit0.sol in the folder, aren't we supposed to use it?
Well, yes… we can use a contract, and no... it's unnecessary as long as we solve the challenge. The easiest way would be as we already did, but if you want to warm up your solidity skills I won't stop you from doing so.

The Exploit (in Solidity)

Do you see how our previous exploit could fail in a real scenario? 💭
What are we assuming that we shouldn't if we were to gather all the tokens he currently has?

If by any chance Vitalik spent any of his tokens before you executed the exploit, the hard coding of the tokens would make it fail.

So, what you could do in order to rest assured it won´t? 💭

At least, two things:
— Set an infinite approval, using uint256 maximum value (2²⁵⁶–1).
— Check for his current balance right before transferring.

I leave it to you as an exercise to understand the exploit below and its deployment. You now have all the knowledge to comprehend it.

Exploit0.sol

This is what the Javascript code needed to solve the challenge would look like.

test/solveChallenge0.js
Journey character

Thanks for reading, and again congratulations on finishing this first challenge! 👏

See you in the next post of this journey! ➡️

Thanks for reading! My name is Matt, and I’m learning how to make Ethereum more secure. I will be sharing some things from time to time.
Follow me on twitter
@mattaereal.

--

--