← All talks

Permanently Bricking Smart Contracts for Fun and Profit

BSides Cape Town · 202224:1967 viewsPublished 2023-09Watch on YouTube ↗
Speakers
Tags
Mentioned in this talk
Languages
About this talk
A deep dive into a class of vulnerability in upgradeable smart contract proxies that allowed attackers to permanently brick contracts by invoking self-destruct through delegatecall on an uninitialized logic contract. The talk walks through proxy architecture, the UUPS pattern, exploitation mechanics, and the responsible disclosure that led to the largest bug bounty ever paid at the time, with over a billion dollars at risk.
Show original YouTube description
BSIDES CPT 2022 Track 2 Permanently bricking smart contracts for fun and profit - Ashiq Amie
Show transcript [en]

Sweet, cool. Can you guys hear me? Cool, thanks for coming to my talk.

So today we're going to speak about a special bug that affects a lot of smart contacts, and there was over one billion dollars at risk during its peak. This bug was also awarded the world's largest bug bounty ever paid ever. So let's get into it.

Just a bit about myself: I'm Ashik Amin. I recently became independent a few months ago, but before that I was working at Ironside over two years. That's where I learned about smart contracts. I think some of the Ironside guys aren't on, so if they are, you should definitely speak to them. They're super duper talented and produce the most amazing work, so shout out to Ironside.

In my spare time, I like to play CTFs and I play under the team name Twisted Steak Sandwich. The team is just me — I'm the whole team — but I don't play for points, so I just do it for learning, just for fun. When I do have anything noteworthy, I will write a bunch on my blog. So at ashik.co.za, you can see all of my CTF blog posts, and there's also just social media handles if you guys are going to get in touch. So Twitter is probably the best. I don't really like this code, this code is full of spam, so I'll probably miss your message, but Twitter is pretty good.

Beyond that, I also post about my research. A lot of my research is about bug bounties, so during my time at Ironside I focused on bug disclosures and posted all of the different bugs that I've disclosed over time. So today we'll be speaking about one of the bugs that I disclosed and escalated as well.

Cool. The bug is about booking smart contracts, and in particular speaking proxy contracts. But to get to it, I'm going to build up all the requirements that we need to talk about a bit tonight. So I'm going to go into smart contracts, into the requirements that we need for the bug, speak about the exploit, speak about the impact, and then talk about escalation. And another thing — we can talk about impact and any of the remediation.

Cool, cool. So what are smart contracts, man? IBM is a smart contracts or programs on the blockchain and they run in three determined conditions are made. But I mean, you can Google it and you'll find 10,000 definitions, but basically they are blockchain programs, right? But what does that mean?

Usually people talk about smart contracts as analogous to vending machines. The reason they use this analogy is because vending machines don't have this idea of a cron, right? So on their own, vending machines don't do anything until someone comes up and puts some money in, selects an item, then there is some kind of output, right? Smart contracts sort of behave the same way. There's no notion of cron job, so they won't act on themselves, right? There has to be some interacting with the contract before something happens. So there's usually some kind of external either human being or even just a script that's running.

More importantly, a smart contract has two very important things, and that's just code and state. So the code is compiled bytecode, but you can write it in any language: Solidity, Vyper, and there's a couple old ones too. But the state — yeah, so the state is the other part of it, and the state is basically the storage of the contract, and that's just holding all the state variables. So anything important, let's do it like metadata or anything else, right?

Cool. So because smart contracts live on a blockchain and not just on a server somewhere, it inherits a lot of properties, but the main three ones for today are irreversible transactions. So when you interact with any smart contract or anything on the blockchain, there's no way to reverse the transaction. Once it's done, it's finalized, right? If you are an expert at blockchain, you will argue with me and say that's not true, because you can technically fork the chain and pay someone, pay all the miners to get it right. But for today's purpose, it's not deleted applied, so we can just call it irreversible.

The next property is atomic transactions, and this just means that transactions happen as is. So it either happens altogether or none at all, right? And this is very important. So if your code hits the revert at some point later in the stack, the entire thing goes divides back basically, and there's no state changes committed. So that just means that everything happens at once or none at all.

And then the last property is immutable code, and this is the most important one for today. So immutable code just means that once you've deployed your bytecode, there's no way to change it, right? It's there forever and it's just never going to change. You could hardcode some parameters to pause your contracts, but there's no way to do anything else.

There is a special edge case of immutability, and that's called self. So there is an instruction on the blockchain that you can use, specifically for Ethereum in Ethereum-style chains, where you could wipe out all of the state and wipe out all of the code. So in that sense, it's not super immutable because it's either on or off — you can edit the code.

So I mean, if you're a dev, which I think some of you guys are, you might be thinking, what the hell, who wants to write immutable code? That's very scary. So this has been an issue. I mean, I try to get this graph where I'm trying to show that there's been three billion dollars worth of damage that's already been taken place because of invisible code, but I couldn't get an exact estimate because it's also things like private key compromises and insider trading. So it might be higher, but the point is that it's a high number, in the billions.

So there's a very real need for upgradability, but unfortunately this is a double-edged sword because upgradability comes with its own issues that cost even more money to be lost. So to talk about availability and how it works, we have to first talk about how smart contracts communicate with each other at all, right?

And the first way is a dot call. So that call works in the same way you might think normal computable computer program works, and let's just buy one contact calling another one, and the state changes are committed to the call to the contract that we called, right? Pretty straightforward. So as the example in the slide, if you had a call transfer, the token balance on contract B state would be updated.

The next goal is delegate call, and this is basically what's called code copy, right? So instead of the state changes computing on the initial contract, so the target contract is completed on the initial contact, right? And what this means is the code at contract B is copied and those state changes applied to contact A. So you can just think of delegate call as copy, right?

And these two things we can use together and form some kind of mini architecture, and this will be enough to have an upgradable pattern, right? So what we do is we separate the bytecode from the state by using two separate contracts with respect one or the other, right? So we call the proxy contract the one that holds the state, and the logic contract the ones that all the bytecode. So if at some point we find some buggy issue in the logic contract, we can

Upgrade the logic contract by simply pointing to a new logical contract. At the bottom of the slide this is a tiny diagram. Cool, so those are two ingredients that we have: we have upgradability and we have self-destruct. But the last ingredient that we need for our bug is just to talk about initialization.

So when you deploy a smart contract, there's a construct that runs which is the normal function that you consider in normal computer programming, in computer programs. But in this case, because state and the logic are separate, we need to initialize the foxy contract. Simply, we need to assimilate a constructor to get the state across. So the way it works, we just hard code a special function. It makes sure it only runs once, and usually, just like a normal constructor, there's access control that happens. It gets provided to different users when the initialization takes place.

Cool, so this is the first iteration of the bug, and before we talk about the diagram and how the actual exploit works, we need to just talk about the assumptions that we need to make. So in this case, we need to assume that our logic contract has some kind of delegate call, right? So just taking on that assumption, there's an exploit that you can do. So one is that we check if the logic contract has been initialized, because if you remember, the state of the logic contract is not important in our architecture at all. So we can additionalize the logic contract, get any of the access consoles needed if it's been initialized, and then we can deploy our own contracts because it's permissionless. Anyone's allowed to deploy contracts, and in this context we can include a self-destruct instruction.

So what we could do is, using this delicate call that we assumed, we could upgrade the logic contract and destroy it, right? So what does that look like? Once the delegate call's performed, our bytecode, the logic contract, is wiped. The logic state is wiped. So this just means that the proxy points to nothing, and this is the first iteration of the brick. But this is just temporary, so it's only the first iteration, and the proxy administrator could come in and say, hey, I've seen the brick, it's an issue, but no problem, we've included upgradability on purpose, so we can just go in and upgrade the proxy, point to a new logic, sorted out.

Cool, so let's chat about impact. Even though it's only a temporary brick, it's still pretty bad. So the logic contract is permanently destroyed, right? Once it is destroyed, there's no way to get it back because transactions are irreversible, as we discussed. The assets that are on the proxy itself, so if you have however much billion dollars worth of tokens, those are not accessible at all while the proxy is pretty. So until an administrator comes through and upgrades the contract, it's locked in. And this could also be made more severe if there's a time lock, right? Because we have this sort of trust admins to not just hot swap our logic at any time. So sometimes there's a time lock involved where you have to commit a code for maybe seven days or 14 days, and people are allowed to check out what the code is doing before it's applied. So your assets might be locked up for a little while.

And the next thing is a proxy nop. So when the proxy is damaged and just becoming a brick, there's no hard-coded reverts, right? So the code just nops through, and that just means that any calls to the proxy itself after the brick means that there's just nothing happening. So if access controls are relied on the proxy, then reaches bypass those access controls. And this happened in RBv2. And the last one is just reputational damage, which isn't a real big issue, but I wouldn't trust my money once this code conflict, right?

Cool, so let's talk about profit. The first profit mechanism is exploiting the nop. So in RBv2, their contracts are vulnerable to this exact bug, and if it was exploited, it is possible to steal a couple million dollars at a different contract because there was a nop through the access control. But it didn't happen though, because there was someone disclosed responsibly. There was actually no exploit, so all good. The next way is shorting the lock token. So if your contract has a bunch of native tokens, FTT token or any other token, you could short the token beforehand, exploit the contract, and then of course panic, and you'll make a lot of money. This also has never happened before, but it is some way to make money. And then the best case is responsible disclosure, which is sort of what happens in each case with this bug, right? So for RBv2, in the example, the Whitehead got paid twenty five thousand dollars for disclosing, which is not bad. It's on par with critical bargains of web2, right?

Cool, so we already spoke about the admin upgrading to a fresh logic contract. But if you're a bug bounty hunter, you could think, cool, what are some of the things you could do to fix the bug if you find it? And one way is to just initialize the contact directly without using your malicious contact, right? So that just means we're going to pick the contract as is, but not in a malicious way. And then petrifying the logic conflict, so that just means as you deploy the contract, the constructor automatically initializes the logic so that no one else can reinitialize it.

Okay, so we've done good. We made twenty five thousand dollars, we could have entered a couple million from being stolen, the admins were annoyed, but they've upgraded their code. But we can do a little bit better. So when I say a little bit better, there's a special branch of proxies where we can escalate the template with permanent one, and that's the UUPS proxy. So how does it work? It's very similar to our original proxy but with a couple differences, and the main idea behind it is to highly optimize the proxy itself. And what that means is that the upgrade mechanism is moved from the foxy to the logic. So this also means that the proxy itself has no right functions at all. So the only way back to the state is to delegate code into the logic concept, and everything else sort of works as is. The initialization provides access control as usual, and the main point of the UUPS proxy is that the upgrade itself has the liquid visual scene.

So this is what the upgrade mechanism looks like, and only two main steps. One is to check that we have authorization to perform an upgrade, and the second one is to perform the upgrade itself, right? So authorize upgrade once initialized, you get those controls to do it. Cool, looking at the upgrade mechanism, we can see this delicate call, right? In this delegate call is allowed to run at any time provided just a bit send some data in. And this just means that anyone that upgrades can always perform a daily format, and if you remember, this is performed on the logic contact.

Cool, so the actual exploit for permanent brick performs in the exact same way as the previous one, where if it's not initialized, you can initialize it with our own malicious contact and call a self-destruct. So everything looks the same, but the differences are in the way the actual impact works. So part of the impact is that the proxy now points to a logic that is permanently destroyed, and the proxy permanently points to nothing, which means there's no strain changes, it never be performed again. So any of your assets are gone forever, right?

Cool, and updated impact: so one of the main things that's different is that the proxy itself points to nothing permanently. The similar impact is that the logic contract is permanently destroyed, and the third is that your assets are locked forever. So if you have a couple hundred million, let's go unfavorable. And the unbox synops are working in the same way too.

Cool, so when I was working at IOCyto, I disclosed this bug to five companies. Two of these were not mine, but two of them were also undisclosed, so I just hot swapped them with other pictures. On the top right hand side is from DDub, and at the bottom is some Teller. And just on these five contracts alone, we managed to save over 15 million dollars from being hacked, right?

And at the time, I thought, cool, this is probably a bit more of a widespread issue. So I spoke to Kyle, who was my boss at the time, and we decided to escalate this issue a bit further and rehearsed a meeting with OpenZeppelin, which is the author of the UEPS proxy. And when I spoke to them, we just told them, hey, look, we found these five contracts that might be an issue. And they said, yeah, actually someone came to us a couple days earlier, they found the same issue, and we've started the remediation process. And that process was demonstrating every single blockchain, and they found 150 other contexts that were vulnerable.

So luckily, they found it, right? But what did they do? They detected all of the contracts across different chains, they updated the source code to include a PhD file logic so that all of it is fixed in the future, and they've monitored their solution. So basically, every time there's a new deployment, they will check whether or not they're vulnerable and just fix the vulnerability as insights of favor.

So you might be thinking, we'll be done, right? Like everything, so there's just no more bugs. They've detected everything on every chain, source code is updated, safe, monitoring for news vulnerable contracts. Surely there's nothing else. And simply, it was just one more bug, right? There was one more, and that affected Wormhole. So Wormhole was vulnerable to this exact bug in a slightly nuanced way, where an anonymous whitehat found 700 billion dollars at risk, and just for the disclosure alone, they got paid 10 million dollars. I wasn't the whitehat, they got the 10 million. If it was me, I wouldn't give this talk, I would be on the beach somewhere or something. But yeah, cool.

Cool, so that's it for me. The last couple takeaways is just that part of the reason this bug slipped through everyone was that it wasn't a code level bug. So all the top auditing firms, Trail of Bits, OpenZeppelin, everyone missed this bug because it is a state dependent bug, right? And what this means is people understood of looking one way and no one is looking the other way. So if anyone's interested, you should 100% start learning how smart contracts work, and I'm sure you could find some of your unbox. Cool, I hope you enjoyed the talk.

Oh, happy to take any questions too.

100%, so there's a bunch of different online materials. I can send you a list personally, but I've mainly focused on training from the security side. So there's some war games you can learn about Solidity, and you can learn about how vulnerabilities work on Solidity, and plenty of CTFs for sure. CTFs can definitely upscale and find these bugs for yourself.

Okay. Yeah, cool. So the question was, is there any way we could find these bugs automatically and always remediate them as he is? Yeah, so not right now, not that I know of any. The idea is mostly grassroots, so as bugs are discovered, they usually come up with blog posts and talk about how the bug was discovered, how it's intermediate it, what to do next. And unfortunately, most people don't like to disclose their bugs too. But I've worked with people and they've asked me very specifically to not publish a certain bug that I'm sure affected another content. So there's no real easy way, unfortunately. But hopefully with incentivizing 10 million dollars worth of bugs, I'm sure it'll come out. So yeah, not at the stage.

Yeah, I think the nice thing about Bhagban is that you can just drop most of your assumptions about any kind of library or any contact lens. If you think this one thing is secure, it might not be, right? Wormhole had 700 million, and looking at Walmart on its own, I might think, cool, 700 million is quite a lot of money, I'm sure it's safe. But they got act three times for 300 million in a different case. So you can start anywhere you want to leave, and I think the main thing is just to do some upskilling in the beginning for genetics, and once you're comfortable with that, 100%, you can just go whatever you feel comfortable with.

Yeah, absolutely. The main thing is that different functions have different access levels, right? Different access controls. You don't want anyone being able to upgrade the contract or pick your content, right? So you can definitely pick in access control for certain cases for certain accounts, but not everyone does this, and that's why they get hacked, and that's why this three billion dollar is gone. But yeah, the code is quite genetic, so it's quite granular in the sense that you can hard code any kind of access control that you want and prevent certain messages and prevent certain calls or delegate calls or any kind of message you want. But you'd have to hard code that in depending on your needs.

Cool, thanks guys.

[ feedback ]