Skimming the CREAM – recursive withdrawals loot $13M in cryptocash

You must have had that happy feeling (happiest of all when it’s still a day or two to payday and you know that your balance is paper-thin) when you’re withdrawing money from a cash machine and, even though you’re still nervously watching the ATM screen telling you that your request is being processed, you hear the motors in the cash dispensing machinery start to spin up.

That means, even before any banknotes get counted out or the display tells you the final verdict, that [a] you’ve got enough funds, [b] the transaction has been approved, [c] the machine is working properly, and [d] you’re about to get the money.

Well, imagine that if you hit the [Cancel] button at exactly the right moment between the mechanism firing up and the money being counted out…

…and if your timing was spot on, then your card would stay in the machine, your account wouldn’t get debited, and you’d be asked if you wanted to try again, BUT YOU’D GET THE CASH FROM THE CANCELLED TRANSACTION ANYWAY!?!!?

And imagine that, as long as you kept pressing that magic button at just the right moment, you could loop back on yourself and layer ghost withdrawal on ghost withdrawal…

…until the machine finally ran out of money, or hit some internal software limit on recursive withdrawals, or you decided to quit while you were ahead and get clear of the ATM before an alarm went off.

They thought of that

Cashpoint machines aren’t infinitely wealthy, of course, because cash is bulkier than you might think, so if you didn’t get your card back and could only work this trick at one ATM, you’d briefly be rich, but you wouldn’t be an instant millionaire.

You might get away with somewhere between $10,000 and $100,000 (less in the UK, where cash machines generally only contain £10 and £20 notes), depending on the maximum capacity of the machine, the age of the banknotes (used money doesn’t lie quite as flat as crisp, uncirculated bills), how full the bank typically stacks that particular machine, and the time of day.

But in real life, it’s never (or almost never, given that “never” is a treacherous word in cybersecurity) going to happen that way, because your bank isn’t crazy: withdrawals follow a software engineering principle known in the jargon as ACID, which stands for atomic, consistent, isolated and durable.

Which is a fancy way of saying that you won’t get the money if the debit hasn’t been recorded against your account, and your account won’t get debited if the money can’t be dispensed: it’s always both or neither, never just one or the other.

(And you can bet your boots that if ever there were a glitch, it would almost certainly favour the bank, and you’d have to report the problem in person to get the machine checked out to confirm that the money didn’t actually emerge correctly.)

In boolean logic, you could describe this situation as (A AND B) OR ((NOT A) AND (NOT B)), or XNOR (the negation of exclusive or) for short.

Note. In the unlikely event that you ever receive more money from an ATM that you were expecting, for example if you withdraw £100 and get a wedge of ten £20s instead of £10s, or if your post-withdrawal balance shows up as undiminished even after you’ve just pocketed $500 from the machine, don’t assume it’s tough luck for the bank and thus that the money is a free gift. If it were a genuine mistake, and you jolly well knew it, you’ll almost certainly be found liable for the amount, given that you did, after all, receive it and keep it.

DeFi not quite so careful

DeFi, short for decentralised finance, is all the rage at the moment, especially in the form of unregulated cryptocurrencies and so-called “smart contracts”, which are essentially short programs – software code in which you express a sequence of trading commands – that operate automatically to shift your cryptocurrency holdings around in the ether.

The decentralised part, which is also the deregulated (or, more precisely, the unregulated) part, means that there are no “clearing houses” or traditional procedures that would be applied if you were operating through centralised banks.

In old-school banking, transactions are sluggish, and may require human approvals along the way, but can (at least sometimes) be reversed entirely if some part of the process goes wrong or is successfully contested.

Simply put, DeFi aims to avoid centralised control, to bypass the vested interests of existing financial insitutions, and thus to speed up and liberalise online trading,…

…while simultaneously removing a lot of the regulatory protection (and potentially ignoring centuries worth of operational wisdom) that you enjoy in the traditional, slow-coach banking world that DeFi fans aim to break away from.

You’d be unlikely to accept the fast-talking, modern coding motto of move fast and break things if you were relying on internet-enabled software to drive your car, design a bridge you’d have to use every day, or subject you to potentially dangerous medical intervention…

…yet in the finance sector, you’d be forgiven for thinking that this motto is the rule, rather than the exception.

Hundreds of millions missing in action

More than two weeks ago, for example, we described how a software design blunder led to the Chinese cryptocurrency exchange Poly Networks suffering a cyber-robbery of more than half a billion dollars ($610 million in total, apparently), until the hacker behind the heist somewhat reluctantly decided to hand back the funds, a process that apparently took until earlier today to complete.

On 2021-08-20, we wrote about a Japanese outfit called Liquid that apparently lost more than $100 million in an electronic smash-and-grab of its own.

That company – whose goal of keeping your cryptocurrencies liquid and tradeable as quickly as possible turned out to leave the company itself in a dangerously illiquid state – is only gradually getting going again, after assuring customers that they won’t end up out of pocket themselves.

Apparently, the company has rushed out a brand new security system for its cryptocurrency storage, and is now telling customers to “rest assured, […] our state-of-the-art [multi-party computation] technology ensures assets remain secure at all times. […] Your assets are safe with us and will always be.”

Another smart contract system bites the cyberdust

This week, it’s the turn of Taiwan-based cryptofinance company C.R.E.A.M. to suffer the shortcomings of smart contract software sloppiness, with a cyberthief allegedly making off with some $13 million in the crytocoins AMP and ETH.

The company’s own notification on Twitter just says that the exploit happened…

“by way of re-entrancy on the AMP token contract”.

Re-entrancy, or recursion if you want to call it that, is a digital problem that’s very much like the unlikely cash machine withdrawal “trick” that we speculated about at the start of this article.

For example, imagine if you have smart contract code (greatly simplified below) that allows the other party to check that they have at least $X in their account; then to call smart contract code from their side of the deal to process $X; then to deduct that $X from their account.

Don’t worry if you aren’t a programmer, because the overall misbehaviour should be clear: you’re accepting function calls to a smart contract called company.withdraw() where customers can specify an account to withdraw from, an amount to withdraw, and smart contract code of their own to be called to process the withdrawal of the specified amount.

After you’ve verified their balance can cover the funds, and permitted them to transact the approved amount, you then debit their account to reflect the money they just spent.

Like this, in pseudocode:

function company.withdraw(account, amount, contractcode) { 
   // Check that there is 'amount' left in 'account' 

   call company.verifybalance(account, amount); 

   // Call 'contractcode' function with approved 'amount'.

   call contractcode(amount);      

   // And then take 'amount' out of 'account'        
   call company.reducebalance(account, amount);  

But this opens a hole in which the smart contract code provided in the user’s request can re-enter your own code, calling it recursively (i.e. without waiting for the previous call to complete), like this:

function customer.contract(amount) {  // Customer's smart contract code
  // First, spend the 'amount' approved by the company
  [...disburse the amount somehow...] 

  // Then, re-enter the withdraw() function above, recursively
  // specifying this very function as the smart contract once again,
  // which will itself be called again, which will in turn
  // recursively call withdraw() again...  

  call company.withdraw(account, amount, customer.contract);

  // And the line above in the withdraw() function where reducebalance() 
  // is supposed to be called will never be reached, because this code
  // will keep jumping back into the withdraw() function right at the 
  // top, which will come back here, etc. etc. :-(                  

If you trace the program flow with your finger, you will see that if the customer correctly authenticates their account, and has at least 1000 units of credit available to pass the initial balance check, then if they trigger a transaction by issuing a call company.withdraw(account,1000,customer.contract), the flow of code will go like this:

0001: call company.withdraw(account,1000,customer.contract); // Start a transaction
0002: call company.verifybalance(account,1000);  // Succeeds because account has >= 1000 in it
0003: call customer.contract(1000)               // So there is 1000 to spend
0004: [...disburse the amount somehow...]        // And thus we spend 1000 that actually have
0005: call company.withdraw(account,1000,customer.contract); // Sneakily re-enter withdraw() at the top
0006: call company.verifybalance(account,1000);  // Succeeds again because account wasn't debited yet
0007: call customer.contract(1000)               // So there is apparently still 1000 to spend
0008: [...disburse the amount somehow...]        // And we get to spend our first 1000 again
0009: call company.withdraw(account,1000,customer.contract); // Again re-enter withdraw() at the top
000A: call company.verifybalance(account,1000);  // Succeeds yet again because account still not debited
000B: call customer.contract(1000)               // And another 1000 gets approved 
000C: [...disburse the amount somehow...]        // And we spend our first 1000 for a third time
000D: [...and so on recursively forever...]      // !!!KA-CHING! RE-ENTRANCY JACKPOT SITUATION!!!

C.R.E.A.M. (which really is an abbreviation, as the dots imply, that stands for Crypto Rules Everything Around Me), has said simply that it has stopped the exploit by pausing supply and borrow on AMP”, where AMP is the cryptocurrency system where the company’s bug was abused, and advised: “Post-mortem to come”.

What to do?

What can we say, except the same as we said the time before the time before?

  • Don’t bet more than you can afford to lose. DeFi is neither regulated by the authorities nor mature in its cybersecurity abilities, so there’s not only a higher-than-usual risk of your funds going missing, but also a lower-than-usual likelihood of getting your money back if they do.
  • Don’t keep all your funds in a “hot” state.. Store as much as you can offline and encrypted in so-called cold wallets, so your cryptocoins aren’t all instantly available online for crooks to grab hold of if someone else makes a security blunder.