Background
Zellic recently sponsored CSAW 2022. CSAW is an annual cybersecurity event hosted by NYU in Brooklyn, New York. Over the years, it's become a centerpiece of the US/Canada CTF scene. Each year, CSAW CTF attracts the top high school and college CTF players. In fact, my cofounder, Jazzy, and I actually met at CSAW 2017—where we subsequently formed the team which would eventually go on to become perfect blue. Thus, we saw CSAW'22 as an opportunity to give back to the CTF community.
As a sponsor, we had the chance to contribute a challenge to the CTF. I decided to create a simple Ethereum smart contract challenge. The challenge would revolve around exploiting a vulnerable smart contract. However, the CTF players would need to be able to interact with the smart contract, without leaking their solution or exploit to other players. This is a common problem when creating blockchain CTF challenges.
Luckily, samczsun and the rest of the team at Paradigm have developed an excellent framework for deploying and hosting this kind of CTF challenge. It solves the problem by deploying a private, forked blockchain instance (with Anvil) for each participant on-demand. Here's what that looks like in practice:
The framework is easy to use, albeit not thoroughly documented. Hopefully, this post will provide a practical example on setting up the framework for your own CTF.
The smart contract
Before we dive into using the CTF framework, let's also quickly discuss the smart contracts themselves. This will help provide useful context later.
The smart contract is a straightforward vault which allows users to deposit and withdraw both USDC and DAI. However, when withdrawing, if the vault's balance of the desired token is insufficient, it performs a swap on Uniswap to get more of it.
The problem here is the lack of slippage checks. An attacker can easily drain the contract by causing the contract to repeatedly make bad trades at unfavorable prices. For instance, this could be done through price manipulation. The attacker then profits off of the resulting price impact.
To exploit this contract, the attacker needs USDC and DAI. Thus, the testnet we host for CTF players should be a mainnet fork so the usual DeFi facilities (Uniswap, WETH, etc.) will be available to them.
Our challenge contract (Chal.sol) is deployed and set up by a setup contract. The following code gives an idea of how our on-chain contracts plug into the CTF framework, so please don't gloss over it:
While developing the challenge's smart contracts, I used Foundry to test locally. It's easy to test my own solution to the challenge by just making a Foundry test.
Setting up the CTF framework
First, I just made a new virtual server (for me, a new droplet on DigitalOcean). I overprovisioned the server a lot since it's cheap (will only last for 48 hours) and means I don't have to worry as much about players overloading it.
I also made a new project in Quicknode so I'd have a RPC endpoint to use. Alchemy/Infura/etc is fine too.
For all of this, I just did everything as root. I don't care because it's a single-purpose virtual machine and will be discarded after the CTF is over.
As per the instructions in the Paradigm CTF 2022 repo, install dependencies:
- Installed Docker (copy pasted commands from here)
- mpwn: git clone https://github.com/lunixbochs/mpwn
- python3: apt install -y python3 python3-dev python3-pip
- libgmp: apt install -y libgmp-dev build-essential
- other dependencies: pip install yaml ecdsa pysha3 web3
Each challenge is based on this Docker image. Clone this repo and copy that directory out.
Add this to the end of the Dockerfile (taken from one of the challenges from Paradigm CTF):
Make a directory called contracts. This is where your contracts will live.
Also make a directory called deploy and put this in deploy/chal.py. This is your setup script that deploys your Setup contract.
Now you can build the Docker image like this:
Note that mytag can be whatever you want. And '.' just refers to the current directory where the Dockerfile is living.
Now you can run the Docker image for our challenge with this command:
Now you should have a working CTF challenge. You can netcat to the port the challenge is listening on (in this example, port 31337) and you should be greeted by the CTF framework.
Setting the secret key (important)
It is very important to set the environment variable SECRET_KEY to a random, secret value. Otherwise, participants can directly make RPC calls to the Anvil instance, cheesing the challenge. For more info, you'd want to look the implementations of server.py and auth.py.
Thanks to rkm0959 of Super Guesser for pointing this out!
Testing against the remote
All good CTFs must have health checks using real solve scripts on its challenges. This is extremely important for quality control and liveness during the CTF.
We can test our live, remote environment using an exploit contract with Forge:
Note this doesn't implement any kind of interaction with the launcher netcat server (i.e., the one listening on port 31337). Ideally, you should also have a script to communicate with that, launch new instances, and parse the communications.
Customizing the ticket stuff (Anti-DoS)
In Paradigm CTF, they use a "ticket" system to prevent abuse / DoS. They do this by assigning each account in the CTF website (i.e., ctf.paradigm.xyz, CTFd, whatever) a ticket. Then, each ticket can only have one blockchain instance deployed at a time. This means that you'd have to register multiple accounts for the CTF, which is presumably behind something like a Captcha.
The way this is hooked up to the CTF framework is that they call some web endpoint to check whether a ticket is valid. This code lives in eth_sandbox/launcher.py:
So you'd need to replace this hardcoded URL with your own endpoint.
For me, I didn't want to bother with that, so I just completely replaced this functionality with some generic CTF PoW stuff. This is provided purely as an illustrative example:
Note that this PoW doesn't have a nonce. This is so that players only need to solve the PoW once, and then can just reuse their secret ticket thereafter, as a quality of life hack. With this basic scheme, there's nothing stopping players from parallelizing multiple instances of PoW solver and amassing a lot of valid tickets to DoS you.
If you wanted to, you could hack in some additional in-band registration/rate limiting mechanism (by IP address, by captcha, etc.). I didn't feel the need for this, so this is left as an exercise for the reader.
Conclusion
In this blog post, we looked at how Paradigm's CTF framework lets you easily host smart contract CTF challenges. We hope this helps anyone making a CTF challenge!
Finally, all of the code used in this blog post is publicly available here.
About Us
Zellic is a smart contract auditing firm founded by hackers, for hackers. Our security researchers have uncovered vulnerabilities in the most valuable targets, from Fortune 500s to DeFi giants. Whether you’re developing or deploying smart contracts, Zellic’s experienced team can prevent you from being hacked.
Contact us for an audit that’s better than the rest.