This document discusses various "smart contract honeypots" that appear vulnerable but are actually designed to exploit would-be attackers. It provides examples like a wallet with a reentrancy bug that doesn't actually allow funds to be stolen, and a gambling contract that displays a fake winning number. The document aims to educate about unintuitive behaviors in Ethereum and Solidity that honeypot designers exploit. It also notes the need for block explorer tools to carefully validate contract code and data.
2. This talk covers:
1. Seemingly vulnerable Ethereum smart contracts ...
2. that are not actually so vulnerable ...
3. that have really been deployed in the wild.
3. This talk doesn’t cover:
1. How to find honeypots (future work!)
2. Scams that aren’t attacking attackers
3. The legal status of stealing money from smart
contract attackers
4. Ethereum/Solidity: Crash Course
● Funds in Ethereum are tracked by account. An account can
either be an external account or a contract account.
● Accounts send and receive transactions, which propose some
(hopefully valid) changes to the state of the chain.
● The fees for these transactions to be processed are called
gas. Fees are paid in Ether, at a rate set by miners.
● When a transaction is confirmed, the state changes caused
by the transaction are accepted by the network as valid.
5. Ethereum/Solidity: Crash Course
● All contracts are executed on the EVM: Ethereum Virtual
Machine. Documented in the Yellow Paper.
● Each instruction has an associated gas cost, and must have
sufficient funds to successfully execute.
6. Ethereum/Solidity: Crash Course
● Solidity: the most widely used programming language
targeting the EVM (Ethereum Virtual Machine)
● Statically-typed, Turing-complete, JavaScript-like
language used for programmatically managing magic internet
money.
7. Ethereum/Solidity: Crash Course
● Each contract has a constructor that is run when the
contract is deployed with supplied arguments to initialize
the state.
● Functions in contracts are, by default, publicly visible
and callable. You must annotate otherwise.
● Each contract can have one unnamed function: the fallback
function. This lets you handle simple fund transfers.
8. Ethereum/Solidity: Crash Course
● Functions are called by sending a transaction with
associated transaction data.
● Transaction consists of a Method ID followed by arguments.
● The ABI is well documented. Constructing data is handled
transparently by web3 bindings, but can take some effort
to decode if you don’t have contract source.
9. Ethereum/Solidity: Crash Course
● In addition to directly calling contracts, contracts
themselves are allowed to invoke message calls, which
Etherscan refers to as internal transactions.
● This allows for things like library contracts, or for a
set of distinct contracts to use functionality of each
other.
11. Ethereum/Solidity: Crash Course
● All contracts are able to store state between invocations
in their storage data area, a key-value store that maps
256-bit words to 256-bit words.
● There are two other places a contract can store data:
memory and stack. These are not persistent but are cheaper
to use.
● Variables default to storage as their backing, and must
explicitly be declared otherwise if desired.
12. Solidity: Common Pitfalls
● Unhandled reentrant control flow, e.g. the DAO
● Delegatecall into vulnerable libraries, e.g. the Parity
hack
● Unprotected critical functions, e.g. the Parity freeze
● Improper handling of secrets, e.g. many gambling games
13. Tools for Ethereum Analysis
● etherscan.io for a block explorer++
● Binary Ninja + Ethersplay for reversing EVM bytecode
● Mythril and Manticore for analyzing all the things
● Remix: an IDE for Solidity that’s pretty handy
● Awesome Ethereum Security: more links
● web3 + a synced full node. This can take some time
14. Etherscan: Exploring BLOCKCHAIN
● Indexes blocks and transactions and displays them
● Allows for searching, viewing, and even interacting with
contracts
15. Smart Contract Honeypots: Intro
● Irrevocable transfers + bad code = free money
● After so many hacks, lots of people looking for a payday
by breaking smart contracts. Lots of targets.
● Ethereum and Solidity have a lot of unintuitive behaviors,
makes it easier to obfuscate intent.
● Honeypots presented look vulnerable but actually are
exploitative. They also require Ether to “exploit”.
16. My Accidental Honeypot: HODLWallet
● Deployed a vulnerable contract for a challenge we were
running at HITB Amsterdam called the HODLWallet.
● The bug was a classic reentrancy attack:
17. My Accidental Honeypot: HODLWallet
● It took less than an hour before someone deposited funds
into the HODLWallet:
18. Not So Accidental: the Private Bank
● In February, /u/CurrencyTycoon posted to /r/ethdev about a
neat way he had found to lose money.
● The contract was named Private_Bank, and was a simple
wallet contract with a glaring reentrancy vulnerability.
19. Not So Accidental: the Private Bank
● In their exploit, the internal transactions appeared to be
succeeding, but no Ether was actually being transferred.
● Another attack transaction actually succeeded! Weird...
20. Not So Accidental: the Private Bank
● So what’s the problem? Internal transactions.
● The TransferLog.AddMessage call actually invokes another
contract, innocently named Log.
● Although nothing looks very
malicious there…
21. Not So Accidental: the Private Bank
● Because of how Etherscan verifies the contract source that
you can provide, the source looks safe but is wrong.
● In reality, the contract called into a different closed-
source contract that would revert their tx.
● You could verify this by looking at the actual TransferLog
address, which does not verify with the provided source.
● This also means that someone testing with the provided
source will think their exploit works, but doesn’t.
22. A Simpler Approach: Whitespace
● Many attacks, like Private_Bank, take advantage of what
users see in block explorers (especially Etherscan)
● WhaleGiveaway looked like it would give you free Ether, if
only you’d send at least 1 ETH to it first. But a few
hundred spaces after the open brace:
23. Even Simpler: Free Shitcoins
● In April, @ShitcoinSherpa posted a tweet about the “best
scam I’ve ever seen”.
● An enterprising user “accidentally” posted his private key
in various chat rooms. This was in the account:
● What the account lacked, however, was Ether for gas...so
of course other users tried to add some and take the
coins.
24. Even Simpler: Free Shitcoins
● What these would-be attackers didn’t know is that there
was a script waiting to collect any ETH that got sent.
● What they also didn’t know, is that MNE had a quirk in its
contract that meant the tokens weren’t transferrable
anyway!
25. CryptoRoulette: Shall We Play a Game?
● CryptoRoulette purports to be a gambling smart contract:
guess the right number, win some ETH!
● This contract has a poor secret generation method:
● Worse though, it looked like the number was only shuffled
after each play, meaning the winning value could simply be
read from the contract! Free money, right?
26. CryptoRoulette: Shall We Play a Game?
● Well, not so much.
● It looks like the number
read from the contract
doesn’t match! Why?
● The trick is in how Solidity handles uninitialized
structures: their default value is storage location 0.
● By writing to this uninitialized structure, it actually
overwrites the data in slot 0 (which is secretNumber!)
27. MultiplicatorX3
● The MultiplicatorX3 contract was deployed in late 2017,
and appeared quite simple: send in ETH, get the entire
balance back:
● Unfortunately for any would be attackers, this function
will never give money back. See why?
28. To Fix or not to Fix: Takeaways
● Undeniable that it’s satisfying to watch scammers get
scammed, but these contracts demonstrate serious issues.
● Lots of work to be done to make Solidity more intuitive in
how it behaves.
● Explorer tools need to be more careful when displaying
data, especially when it comes to source code.
● ???
29. Thanks!
Got other cool honeypots that would fit in this talk? Let me
know! @_supernothing / bens@polyswarm.io
Learn more about us: @PolySwarm / info@polyswarm.io
This slide deck: https://bit.ly/polyswarm-ethereum-honeypots
2019 @ Swarm Technologies, Inc.
polyswarm.io
info@polyswarm.io
Funds in Ethereum are tracked by account rather than by UTXO (as in Bitcoin). This choice was made to simplify the implementation of contract addresses.Changes to the chain state are transactions. In a normal user account sending to another user account, this is simply a transfer of value that confirms if valid. For transactions to a contract, the contract can implement functions that handle specific inputs, that define what to do with incoming funds and can modify the local state of the contract.Fees are the cost of doing business. They are a cost, based on a per-EVM-instruction basis, for executing a transaction. 21000 gas is the cost of simply sending funds from one account to another, and is the lowest cost.When confirmed, the state changes are actually committed to the chain, and the settled state is now final.
All the code that runs when transactions go through a contract runs on the EVM, a virtual machine in which each instruction is associated with a specific cost of the operation. This prevents issues with contracts that try to use excessive resources. More generally, it is why you can deploy “Turing-complete” contracts and not immediately be foiled by the Halting Problem.
If you’re writing smart contracts today, Solidity is probably what you’re writing in. It’s statically typed, Turing-complete, JavaScript-like, and often described as a literal dumpster fire.Solidity has a very controversial history. On one hand, it’s very easy to learn, and very easy to deploy your own personal smart contract. However, it has a well-earned reputation of making it very easy to make very serious mistakes, and this led to the massive thefts that are associated with Ethereum.
All interaction with contract accounts is done by calling into public functions. What this means in practice is each contract, when compiled, has a simple dispatch table at the entrypoint of the contract that directs execution based on the function hash being invoked. The functions in this table are all the public functions on the contract; by default, this is every function in the contract unless it is marked otherwise. Each public function is allowed to change the state of the contract, both internal and public. This can be anything from updating balances, or recording logs, or basically anything that the contract needs to keep track of what’s happening.
Calls into functions are performed by issuing a transaction to the given contract address with some associated transaction data. This data essentially sets the initial state of the EVM for when the contract is invoked. When the EVM is spun up to verify a given transaction, it loads the code of the contract, sets up the initial state, and jumps to the smart contract entry point. If execution succeeds, the resulting modification to the state is committed to the chain.
Once contracts are executing, they are allowed to call other contracts as well in what are called message calls or internal transactions. This means that you can build contracts that call other contracts to do stuff, which lets you build interesting, sometimes quite complex, sets of contracts that all rely on each other to perform specific functions. In addition, you can build contracts that function as libraries, that many instances of the same contract can refer to, which saves money when deploying the contract.
In a sense, smart contracts define rules for transitioning between two different states. These states are stored in the storage data area, which is a key-value mapping of values that the contract can reference when it runs. Accessing and modifying this is more expensive than memory or stack, because it most read from older chain data and actually store new data onto the blockchain. The modification of storage is key to understanding how smart contracts operate.
The DAO hack, which resulted in 3.6 million ETH being stolen, was due to what is known as a reentrancy vulnerability. Because contracts can call other contracts, if an attacker can get a specific contract to call their own, that malicious contract can then call back into the contract that originally called them. If the contract is not careful to make sure that all appropriate state updates are made before calling the external contract, it’s possible to cause the contract to do things it shouldn’t. In this case, that something was continuing to send ETH to the attacker even though their balance should have been exhausted.The Parity hack was even simpler, though: because an initialization function was not marked as internal, it was possible for anyone to, after the multi-sig wallet was set up, set themselves as the sole owner of the funds and drain the wallet. This is not ideal behavior
Now with basics out of the way, we can talk about honeypots. In this context, we’re discussing various ways that scammers are tricking would-be attackers into sending Ether to them, and then taking it. There’s a number of clever approaches that people have taken to this so far, and I expect that the trend won’t die down anytime soon. The current environment has created a perfect storm of sorts: lots of people trying to break contracts who are new to the ecosystem, and the fact that if they fail you can gain money. Add to this how new the tools for inspecting contracts/transactions are, and how unintuitive some parts of the language are, it makes it pretty easy to write very tricky honeypots.After talks about huge paydays of tens to hundreds of millions of dollars, it’s no surprise that plenty of attackers were looking for their own paydays. Because of this, honeypot authors had a lot of targets, and a lot of targets who were new to smart contracts. Combined with some of the pretty unintuitive behaviors that this new tech has, and the sometimes poor interfaces for viewing code, it’s possible to fake vulnerabilities.
I first got interested in these honeypots because I kind of accidentally made one myself, by writing an intentionally vulnerable contract that was only exploitable if you had private keys I had generated. We were running a challenge for HITB AMS where people could exploit the contract and get some Ether, and we’d give them a prize on top of that.The contract was supposed to be a wallet that would limit the number of times a user could withdraw funds (up to 3 times), and would limit the amount you could withdraw each time to a very small amount, less that the total of the users balance. What this means is that it should be impossible to withdraw your total balance; if you could, then you had solved the challenge.The bug was a simple reentrancy bug, which, because the number of withdrawals wasn’t updated before calling an external contract, it was possible to bypass the 3 withdrawal limit. To prevent users from stealing *all* the balances instead of just their own, the balance is properly updated *before* the external call, which prevents funds from being stolen. This also means that, even though anyone can deposit funds into this “vulnerable” wallet, without one of the existing balances I created, they’d never be able to withdraw more money than they put in.
I was very surprised when this happened, as, while the bug is fairly obvious, it was not a long time before someone had sent funds to this contract that was specifically not vulnerable to a random attacker! If it was designed slightly differently (minimum deposit amount, and making it impossible to withdraw), it wouldn’t have been too hard to acquire funds from these random attackers.
In February someone posted a honeypot to /r/ethdev that they fell victim to. The contract was Private_Bank, a simple wallet.
If you recall, contracts can call other contracts. In this case, the Private Bank calls a separate Log contract, that appears to be benign based on the source code uploaded to Etherscan. All it appears to do is add a history log, and return, which shouldn’t affect exploitation at all, right?
Because this contract is calling an external contract, the source shown in Etherscan is by no means the same as what is actually being called. Etherscan validates contract source code by compiling the contract with the exact same settings that you did locally, and if it can recreate the exact same bytecode, it marks it as verified and stores the source for everyone to see. Unfortunately, if something calls an external contract, there’s no way for Etherscan to verify the source you provided for those other contracts is actually valid, only that the ABI matches what you built with locally (since that’s all that would change the bytecode of that specific contract).Because of this little quirk, it makes it very easy to convince people that the code they’re looking at is the *actual code*, when in fact it’s something completely different. In this case, the attacker would revert anyone else’s exploit attempts, but allow theirs to proceed unimpeded. This means that everyone else’s money would be stuck forever, and the attacker could steal it at any time.
Sometimes these scams are not so technical, but their simplicity doesn’t make them any less effective. This attacker went into many chatrooms, posted his private key, and waited for the inevitable stream of people who thought they were about to make some easy money from a rube.The trick here is that, like the rest of the scams, the attacker needs to convince someone to send Ether to an account. In the case of most of these contracts, this is the hard part, since you need to convince your target that they should send in some reasonable amount of Ether (not test amounts), and that looks weird in the contract. In this case, because of how ERC20 tokens function (they need gas to execute their contracts!), it’s not at all unreasonable to people that they should just send their Ether there temporarily to steal a lot more money.
Data, as you remember, is stored in storage. By default, variables are stored there and will persist between invocations of the contract. What’s happening in this contract is that a local game struct is being declared, but not being initialized. Because Solidity doesn’t know what storage slot this variable refers to, it defaults to slot 0. In this case, it causes us to corrupt secretNumber with msg.sender, which will always be greater than 16. Because of this, it’s impossible to win!