Break My Contract (Part 1) - Buffer Overflow

About two weeks back I published a succesfull post on r/cryptocurrency called “Break My Contract, Steal My Money — A Solidity Riddle”. Basically, I wanted to share my passion for learning Solidity and create awareness for smart contract vulnurabilities in the same move. You can find the post here:

https://www.reddit.com/r/CryptoCurrency/comments/o6fshx/break_my_contract_steal_my_money_a_solidity_riddle/

The post was linking to a deployed smart contract with a built-in security exploit and 25,000 $SCAM tokens that anybody was free to steal… If they know how.

What Was the Trick?

In this article I want to shed some light on how someone could have “hacked” the contract and gained access to the deployed tokens. The Solidity code for the hackable smart contract can be found on GitHub:

https://github.com/scamcoincrypto/solidity-riddles/blob/main/hackable.sol

So let’s dive into the basic concept behind all of this. Let’s take a look at the code and find the vulnurability. The smart contract is fairly basic. And as you can see, it contains a few mapping data types, that store token balances and addresses based on an assigned ID.

uint8 public Count = 0;
mapping(address => bool) addressRegistered;
mapping(address => uint8) addressToId;
mapping(uint8 => uint256) balances;

It is crucial to note, that the ID is stored as a uint8 data type. That’s an 8-bit unsigned integer, which only has a fairly small range. It can store values from 0 to 255, before it wraps around back to zero. If it reaches 255 and we increment it by one, we get a so called buffer overflow and the variable resets to zero.

How Can We Exploit This?

We can exploit this vulnurability and overwrite the address coresponding to a specific ID in the addressToId variable. This would in turn allow us to access the token balance stored in the balances variable in a malicious way, stealing someone elses tokens. To overwrite the address, we need to take a hard look at the DepositScam function:

function DepositScam(address origin, uint256 amount) external {
BEP20 scamToken = BEP20(SCAM_TOKEN_ADDRESS);

if (!addressRegistered[origin]) {
Count++;
addressRegistered[origin] = true;
addressToId[origin] = Count;
}

uint8 id = addressToId[origin];
balances[id] += amount;
scamToken.transferFrom(origin, address(this), amount);
}

As we can see, this function transfers $SCAM tokens from any address and stores the balance. If the origin address is an unknown address, it registers the address first, incrementing the Count variable and using it as the ID for the new address.

Remember, the Count variable is stored as a uint8 data type. So if we call the DepositToken function with 255 unique addresses, it experiences a buffer overflow and restarts counting at zero. That’s our chance to call the function one last time with our own address and overwrite the address holding the 25,000 $SCAM deposited by the first caller.

How Can We Deposit $SCAM From 255 Addresses?

The DepositScam function can be called with arbitrary addresses. But we face another problem here. Any call of the function will try to use the transferFrom function of the $SCAM token to transfer tokens from the origin address to the smart contract.

The transferFrom function is a standard feature of any BEP20 or ERC20 token. It only works however, if the owner of the origin address has set an allowance for this transfer beforehand and holds enough tokens. If this allowance was not set (or the origin address doesn’t hold enough tokens), the transaction will fail, which in turn will fail the call of the DepositScam function.

But there is an easy way around this, and it’s buried in the code for the $SCAM token. The transferFrom function will work in any case - even without an allowance set beforehand - if the transferred amount of $SCAM tokens is equal to zero. With this knowledge, we have our final puzzle piece to breaking the contract and stealing the $SCAM:

We need to call the DepositScam function 255 times, with unique but arbitrary addresses and an amount of zero. We then need to call the function one last time with our own address to overwrite the original depositor. We can then call the WithdrawScam function and the smart contract will assign the balance of the original depositor to us.

function WithdrawScam(uint256 amount) external {
BEP20 scamToken = BEP20(SCAM_TOKEN_ADDRESS);
uint8 id = addressToId[msg.sender];
uint256 bal = balances[id];
require(amount <= bal);
balances[id] -= amount;
scamToken.transfer(msg.sender, amount);
}

This entire process can be automated by writing a custom smart contract that goes through all required steps in one, single transaction.

So How Do You Prevent This?

There are a few easy steps to make such an attack a lot harder or even impossible. One thing that can be done to harden the contract is to use a uint256 data type for the ID. This data type stores values from 0 to 2²⁵⁶-1, which is an unbelievably huge range that makes any attempt to reach its limit practically impossible.

Another easy step is to use the msg.sender attribute when calling DepositScam. With msg.sender, you can access the address of the caller of the function. This change makes it a lot harder to register a large amount of unique addresses.

function DepositScam(uint256 amount) external {
address origin = msg.sender

[…]

}

But the most important way to prevent any vulnurabilities involving buffer overflows or underflows, is to use the SafeMath library.

Always Use SafeMath!

SafeMath is an open source library by OpenZeppelin and it has become standard practice to always use SafeMath for all arithmetic operations inside your smart contract. Using SafeMath is the very first thing you should adopt when developing smart contracts. Explaining the details about it is outside the scope of this article, but you can access the library here:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol

Find $SCAM Online

You can also find $SCAM online and join our community:

https://scam-coin.org/

https://www.reddit.com/r/scam_coin/

https://t.me/SCAM_Coin_Community

https://github.com/scamcoincrypto

Let’s change the world, one $SCAM at a time!