How to hack an EOS casino

in #eos5 years ago (edited)

Text by Kirill Yurkov, Code by Филипп Пономарев

Intro

Hi! My name is Kirill. I‘ve been hacked. Several times actually. Never lost anything mission critical to hackers, but still, it is an indescribable feeling to find that your test server is used in a spam network. Thankfully it was early in my career and I learned a lot from those experiences. But in blockchain and IT, in general, it is not enough to learn from your mistakes, you need to foresee, prevent, and mitigate. To help all EOS developers do that we started this series: to illustrate typical mistakes, to help prevent hacks, to share and learn. We decided to start with common errors in probably the most popular Dapp “genre” in EOS — casinos.

3 main questions

To hack EOS casino you first need to understand how they work and possible vectors of attack. Typically you want to know

How random numbers are generated?
How does token transfer work?
How free games are protected?
To better illustrate how different attacks work let’s look at a brand new team, which decides to develop their very first casino Dapp. (Samples for this article are available at https://github.com/LetItPlay/SamplesForArticle)

Careless casino

So let’s say we are a team of developers. And we want to build a fair and transparent casino. We want it to be 100% on-chain so that everyone will know that we cannot cheat. We start with EOS casino classics Dice game. The idea of the Dice game is very simple:

  1. User bets that next random number from 1 to 100 will be more than 51 transferring a number of tokens.
  2. Casino bets that it will be 51 or less.
    If the user wins his bet is doubled and transferred to the user, otherwise, the casino keeps the tokens.
    Our contract will include random number generation, the game itself and logic go handle receiving and sending tokens.

Let’s generate an on-chain random number

uint64_t get_rand() {
return (now() + tapos_block_prefix()) * 179424691u % 0x0fedcba7afffffff; //some magic values
}

You see we use block_prefix which is totally random and can’t be predicted (spoilers: it can be predicted). Also, we use the current time to make predicting the number even harder. Furthermore, we multiply the totally random sum by some super secret number and then we mod it by a prime number and cite Wikipedia [https://en.wikipedia.org/wiki/Lehmer_random_number_generator] to prove that we did a good job.

Then we implement the game itself. The code is rather straightforward:

void dice_game(asset quantity, name player, string memo)
{
  auto roll = get_rand() % 100 + 1;
  print(" roll is ", roll);
  if (roll > 51)
  {
  uint64_t coef = 2;
  auto win = asset((quantity.amount * coef) / 10000, quantity.symbol);
  print(" you won ", win.amount);
  pay_to_user(player, win, static_cast<uint8_t>(roll));
  }
  else
  print(" you lose");
}

We use our random number to generate, what we think is a random number whose values are uniformly distributed in [1..100] (spoilers: they are not). And if the number is more than 51 we send the user double the betted amount.

void pay_to_user(name player, asset amount, uint8_t roll)
{
    action(
    permission_level{ get_self(), name("active") },
    name("eosio.token"), name("transfer"),
    std::make_tuple(
        get_self(),
        player,
        amount,
        string("You win in dice game!") + std::to_string(roll)
    )
    ).send();
}

To accept bets we use a classical (although in many ways flawed) pattern. When the user transfers tokens to us, our contract receives an action from require_recipient inside transfer method of a typical token contract[https://github.com/EOSIO/eosio.contracts/blob/bfd1793032ed69bba8047b4807f692eaed2ed5e5/eosio.token/src/eosio.token.cpp#L100]. In response to this “event”, we execute our dice game code.

public:
    using contract::contract;
    CGame1(name receiver, name code, datastream<const char*> ds)
        : contract(receiver, code, ds)
    {};

    void apply_transfer(name from, name to, asset quantity, string memo)
    {
        if (from == get_self()) // we are transfering from us
            return;
        print("from : ", from, " to : ", to, ", quantity : ", quantity.amount, ", memo : ", memo);
        dice_game(quantity, from, memo);
    }
};

//EOSIO_DISPATCH(CGame1, (transfer))

extern "C" {
    void apply(uint64_t receiver, uint64_t code, uint64_t action) {
        if (code == name("eosio.token").value && (action == name("transfer").value)) {
            CGame1 thiscontract(name(receiver), name(code), datastream<const char*>(nullptr, 0));
            struct transfer_args {
                name from;
                name to;
                asset         quantity;
                string        memo;
            };
            auto unp_t = unpack_action_data<transfer_args>();
            thiscontract.apply_transfer(unp_t.from, unp_t.to, unp_t.quantity, unp_t.memo);
        }
    }
}

Surprised by how easy it is to write a casino, we then publish our contract and wait for tokens to flow our way.

To understand what mistakes we just made let’s look into initial questions in more detail.

Random number generation

There are a lot of mistakes EOS casino can make when handling random number generation. They can easily cheat their users by hiding all aspects of random number generation from them. Alternatively, while striving for fairness and openness, they can build a Dapp which is easily hackable.

Random number generation, in general, is actually a rather complicated topic. We’ll try to avoid irrelevant complexities and concentrate instead on EOS specifics.

At this point in time and for foreseeable future it is completely impossible to create a random number generator which works 100% on-chain. Each and every one of Dapps based on on-chain random generation can be hacked.

To implement random number generation in EOS Dapp you ALWAYS need two parties. A basic algorithm then goes [https://github.com/EOSIO/eos/tree/release/1.0.x/contracts/dice]

  1. Party A generates a random number and commits to it by publishing its hash.
  2. Party B generates a random number and commits to it by publishing its hash.
  3. After verifying that their counterpart has published its hash and cannot back down from the “deal” each party broadcasts its random number.
  4. Random numbers from A and B are combined (in a manner that does not undermine randomness).

When we are talking about casinos, the player is party A, a casino is party B. However to reduce the number of transaction user has to sign, the algorithm actually looks like this:

  1. Casino generates a list of random numbers on the back-end.
  2. Casino publishes in the blockchain hashes of these numbers. These hashes are available and can be viewed by anyone by reading a table stored in the smart-contract
    [https://bloks.io/transaction/a7be11709ac899dadcb6db48e7894006ae8f0a204d47cfa1102c59470e566630]
    [https://www.eosx.io/account/bingobetgame?mode=contract&sub=tables&table=servhashes].
    NB, It is important to understand that the casino can still cheat. By choosing the best number from the published list, for example.
  3. Player publishes his random number and commits to the “deal” by transferring tokens (money) in the same transaction.
  4. Casino publishes a random number from the list.
  5. Random numbers are combined.

When done right this algorithm guarantees that players cannot cheat and casino can only cheat inside smart-contract code. In theory. In practice

  • Random number generation on the back-end can be attacked.
  • The back-end itself can be attacked.
  • EOS node used by the back-end can be attacked.
  • Token transfer can be attacked in a variety of ways.
  • Any number of errors in the smart-contract can be exploited.
    To attack the Careless casino hacker will probably start by looking at the smart-contract code.

Decompilation

In gross violation of EOS constitution, most EOS casinos do not open-source their code. To better understand how any one of them works you have to decompile them.

Let’s decomile our Careless smart-contract.

First, we get the code. Thankfully it is very easy to do

cleos -u http://jungle2.cryptolions.io get code carelessbets -c game2.wasm --wasm

Then we use a standard decompiler available in CDT

./wasm2c game2.wasm -o decompiled.c

The result is not exactly pretty. But it is quite readable. We did simplify it a bit to make it easier to understand. You can find a full version at [ https://github.com/LetItPlay/SamplesForArticle]

* import: 'env' 'current_time' */
extern u64 (*Z_envZ_current_timeZ_jv)(void);
/* import: 'env' 'tapos_block_prefix' */
extern u32 (*Z_envZ_tapos_block_prefixZ_iv)(void);
/* import: 'env' 'printui' */
extern void (*Z_envZ_printuiZ_vj)(u64);
….
static void apply(u64 p0, u64 p1, u64 p2) {
 u64 l3 = 0;
 FUNC_PROLOGUE;
 u32 i0, i1;
 u64 j0, j1, j2;
 f8();
 j0 = (*Z_envZ_current_timeZ_jv)();
 l3 = j0;
 i0 = (*Z_envZ_tapos_block_prefixZ_iv)();
 j1 = l3;
 j2 = 1000000ull;
 j1 = DIV_U(j1, j2);
 i1 = (u32)(j1);
 i0 += i1;
 i1 = 179424691u;
 i0 *= i1;
 j0 = (u64)(i0);
 (*Z_envZ_printuiZ_vj)(j0);
 i0 = 0u;
 f10(i0);
 FUNC_EPILOGUE;
}
...

Our super-secret formula can be easily extracted from this code.

But the actual problem is that all numbers which we believe to be random are not random at all. Tapos_block_prefix is defined by the user who calls the transfer transaction [https://github.com/EOSIO/eosio.cdt/blob/master/libraries/eosiolib/transaction.h]. Current_time is the time of the block and it can be easily predicted and controlled by the user. Which means that any user capable of decompiling our code will always know before sending his transaction is he going to win or lose.

Random number generation attacks

Transparency of blockchain removes a lot of hassle from cryptography analysis. Each and every transaction is easily available. As a result, if the random number generator on the back-end is not cryptographically strong its vulnerabilities can be found and exploited.

The easiest possible attack is to gather the history of all hashes and numbers and look for repetitions. Weak random number generators have a relatively short cycle. For more comprehensive and realistic attacks look at classical attacks on RNG [https://en.wikipedia.org/wiki/Random_number_generator_attack][https://www.ethicalhacker.net/features/book-reviews/mitnick-the-art-of-intrusion-ch-1-hacking-the-casinos-for-a-million-bucks/].

But even without sophisticated cryptographical analysis, we can find an error in Careless number generator. To simplify we will assume that a random number is actually 8 bit long (from 0 to 255). Now we want to get a number from 1 to 100. In the Careless contract we just

get_rand() % 100 + 1;

Let’s say our result is 31. It means that the initial random number was 31 or 131 or 231. Alternatively, our result may be 62. Then the initial number was 62, 162, or 262… You see the error now! Chances of us getting a number greater than 56 are 33% less than getting a number less or equal to 56.

Back-end attacks

Dapp developers like to concentrate on smart-contract code leaving their back-end vulnerable. You can use traditional methods to gain access to the back-end machine. If you are really lucky (or if the developer is really careless) you can find private keys for EOS accounts there. Alternatively, you can gain access to a table containing numbers, which are yet to be published.

EOS node attacks

In order for the casino to work back-end has to interact with EOS blockchain. Typically this means that casino will have several EOS nodes to get data from and to send transactions to. This can be exploited. For example, a well-known blacklist attack is based on this. We will go in further details in the upcoming article on token transfer attacks.

Token transfer attacks

This is a huge and very important topic. So we decided to cover it in the next article. For example, it is possible to write a smart-contract which will impersonate a Careless casino user, will play Dice and will never lose. The example is available at https://github.com/LetItPlay/SamplesForArticle.

Free game attacks

Most of EOS casinos encourage users to come back daily by having one or more free games. A typical example is a “Lucky Draw” game. Once per day a user has a small chance of winning. An important weakness of this sort of games is that they assume that each account represents a single “live” user. To abuse this game you need what we call an “accountnet”.

Accountnet is in many ways similar to botnet — often distributed (but sometimes not) system of EOS accounts controlled by one central server. It is rather easy to create and manage a huge accountnet (thousands or even tens of thousands of accounts) in Jungle Testnet for example. In EOS mainnet resources are rather expensive but one can afford accountnet of several hundred accounts. Once created you can use your accountnet to play “Lucky Draw” in dozens of EOS casinos, slowly increasing number of controlled accounts. To do that you need

Script to generate an account and provision it with resources.
Software to manage (send commands, manage resources, etc) your accountnet.
It is rather obvious that accountnets can be used in DOS and DDOS attacks. In EOS such attacks are even more dangerous for Dapps because they typically consume memory which Dapps pay for.

A good question to ask here is why casinos have free games if in reality, they are mostly played by bots from accountnets. The truth is a lot of (up to 90% in some casinos according to our estimations) of the transaction traffic and volume we see in EOS casinos is generated by bots. Look at DappRadar[ https://dappradar.com/rankings/protocol/eos/category/gambling]. See very distinct cycles on the graph. Distinct peaks are actually accountnets coming to play. Maybe some of them are “natural”, we know for a fact that many of them are not.
cardio.png
Casinos and Dapps compete for top spots in a number of ratings[https:/dappradar.com, https://www.dapp.com/, https://dapp.review/, https://www.stateofthedapps.com/, ratings inside mobile wallets]. To get to the top they welcome as much traffic from accountnets as they can.

Disclaimer

Despite the clickbait title, the aim of this article is to prevent hacks, not to enable them. We have been developing for EOS since DAWN 3 and made almost every mistake in this article. Thankfully we fixed them. Most of them even before publishing our Dapps, all of them before they were exploited. Still, the state of security in EOS Dapp remains appalling because there is not enough easily accessible info. And we found the hard way that even “best practices” are sometimes wrong or very often not a good fit for a particular problem. To remedy this we decided to tell EOS developers about the common pitfalls in Dapp development.

Sort:  

Interesting article :-) You should consider reposting it on our forum: https://eos.discussions.app

Voted with compliments of @sp-group by @pibyk

Congratulations @kirill-yurkov! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!