This post provides an overview of hacking play-to-earn blockchain games and common security pitfalls affecting P2E. It explains in detail how several vulnerabilities were discovered in a P2E game named Manarium.
Authors: Eduardo Alves and Vitor Fernandes (during his tenure at Blaze Information Security)
Introduction
At some point, you likely have heard at least a bit about Non-Fungible Tokens (NFTs) in recent years, the new sensation that record-breaking millions through the market.
In short, NFTs are unique digital collectibles on the blockchain that contains a unique identification code to distinguish them from one another and prevent them from being replicated.
NFTs can be everything like a jpg image, music, tweets, images of physical objects, video game skins, or any digital art, which means anyone can be an artist by creating any digital content and selling this for any price.
In cryptocurrency, you can trade one Bitcoin for another one, and you’ll have exactly the same token, probably just with a different price. However, in the NFTs market, the assets are not interchangeable with other assets of the same type. You can trade like Pokemon cards. However, if you trade one for another, you will receive a different NFT from the one you gave away.
Talking about NFT collections, one of the most famous is the Bored Ape Yacht Club (BAYC), which features profile pictures of cartoon apes that are procedurally generated by an algorithm. Only in 2022, the sales of Bored Ape Yacht Club NFTs have totaled over US$1 billion.
According to the Bored Ape Yacht Club, the concept is not just about buying an avatar but a kind of membership card for a club. Celebrities have purchased these non-fungible tokens. In January 2022, for example, Neymar Jr has announced on his social networks that he had acquired a property in the world of NFTs, that of the Bored Ape Yacht Club, for a little over US$1.1 million.
But, all that glitters is not gold; since this market moves a large amount of money, the environment becomes attractive to cybercriminals. Bored Ape Yacht Club had this experience. The Instagram account and Discord server were both hacked with an unofficial “mint” link being sent out to followers.
As a result, the wallets of those who clicked the link were compromised, and NFT Apes were transferred to new wallets by the hackers. The floor price of which reportedly comes close to $13.7 million. This makes us wonder how it can be so easy to lose millions on a single object.
Since the NFTs are unique, this makes them suitable to use in games as representations of tradeable items, like characters, consumables, and others. NFT games have become popular in the Game-fi world as a way to earn income. You can sell your in-game NFTs to other collectors and players and even earn tokens with play-to-earn models.
A new era of a Ponzi scheme?
The Pre-Bitcoin Era started with eCash in 1983, whose objective was to allow people to anonymously transfer money over the Internet. In 2009 Satoshi Nakamoto created Bitcoin. Although it didn’t become a giant overnight, it is the most recognizable crypto.
In 2012 was launched one of the world’s most recognized exchanges, Coinbase; along with the popularity of Bitcoin, some other cryptocurrencies began to emerge and attracted more crypto investors. These digital currencies are often based on decentralized networks and can be used as assets for investment and as mediums of exchange in financial transactions.
Although some digital currencies like Bitcoin and Ethereum are already consolidated, there are frequent questions related to assets that are highly speculative and extremely risky. The main reason is due to high volatility. At the beginning of 2018, Bitcoin traded as high as $18,336, and then it dropped to a low of $3,400 in 2019. However, the price has been picking up again due to endorsement from companies and big investors. In November 2021, Bitcoin reached $69K, the highest price since its inception.
Play-2-Earn Games Architecture
Play-2-Earn games have a common architecture compared with other games, such as casino games, MMO games, and so on. This kind of architecture will be described below:
- Client-Side → The entire game logic (by default, this must be considered insecure and must not be trusted without any kind of verification on the sent data). It’s very common to come across this approach, especially when we are talking of games focused on client side. These games normally are developed using software that supports web assembly (that will run on the client’s browser), such as Unity3D, and this software will be focused, as a good part of the target in this article uses it.
- Server-Side → In this part of the architecture, all data about the game will be processed via incoming and output data. This kind of process includes data sanitization, access control checks, balance/score updating, and so on. This part of the architecture can be served using very known technologies, such as NodeJs, Go, Python, Ruby…
- Service → Here will be the entire data about the game (and the validations and sanitizations), such as the player’s balance, the score of all users, and the number of times a user can race with their cars (sometimes, information more criticals are stored too, such as the NFTs of the player, which must be stored directly on the blockchain for security reasons). These services are also known by most people. For example, a very known Service used in most of the P2E games tested by us was the Firebase.
- Smart Contract → This is the last part of the Game. Here blockchain technology will enter the scene. In this part, the Game Token was created and deployed in a blockchain (Ethereum, Binance Smart Chain (BSC), Avalanche…). The Token is a Smart Contract code that will run inside the blockchain and will allow users that have a Wallet in the same network to interact with it, transferring and storing. The Smart Contract generally uses the ERC20 standard for Ethereum, BEP20 for BSC, or ARC20 for Avalanche.
- Web Wallet → An software that’s responsible for helping users to interact with the blockchain and dApps (decentralized applications). It’s an interface that abstracts all the interfaces, codes, and URLs to work with blockchain. Wallets don’t hold tokens, just the keys to prove that the user is the owner of these tokens (the tokens remain on the blockchain).
- The User plays connects to the game via a web wallet installed in their browser and plays the game;
- The game will fetch/send data from the user to the back-end;
- The back-end will process the data received and will send it to the service;
- & 5. The back-end and service will return a message about the data modification (200 OK, for example);
- Here, the back-end has an automated script that will fetch the data from the service searching for some information about the game;
- The Service will return the asked data from the back-end;
- The same script will interact with the Smart Contract to execute some function inside it;
- Here, the game doesn’t have an automated process to interact with the smart contract. Therefore, an authorized person (in this case, the Admin) should consult the service and get game data for some kind of verification;
- The Admin will interact with the Smart Contract and call some functions inside it;
- After being called, the Smart Contract will execute the called function (via the script or the via the Admin), as an example, a function that will transfer the Game Tokens to a wallet address.
The example above shows an application that stores the game data inside a service, processes the data on the back-end, and has two kinds of Smart Contract interaction:
- Automated → A script that will automatically interact with the Smart Contract and call its functions, such as transfer tokens.
- Manually → An Admin that will interact with the Smart Contract and call its functions, such as transfer tokens.
It’s important to mention that this approach is not an “absolute truth” and can be changed according to the game developers. Another example too often used is the implementation only of the Client-Side and Server-Side or Client-Side and Service (which most used database type that we faced was NoSQL, with MongoDB as the most chosen database software. Regarding the Service, the most used was Firebase).
P2E Games Signature Process
It’s important to mention that P2E Games must confirm the user’s identity before allowing it to play the game. This process of confirmation is called Signature, where the user must sign a message + nonce (the nonce must be used to protect the signature from replay attacks, so its value always must be random) with their private key and send the signature hash to the back-end, that will verify the signed message hash with the user public key.
Below, the image explains the Signature process of a P2E Game:
- The User connects their wallet in the game;
- Requests new data (String + Nonce) to sign;
- Back-end returns the String and a random nonce to sign;
- The Game will provide the String + nonce to sign with the user’s private key via Metamask;
- After signed data, Metamask will send it back to the game;
- The game will send the signed data to server-side;
- The Server verifies if the signature is valid (using the user public key);
- If it’s not, back to step 2;
- If it is valid, the back-end will fetch all user data from the Service and send it to the game;
- The user can now interact and play the game :).
P2E Games in Action
P2E games need only a web browser, an internet connection (of course), and a Web Wallet software, for example, Metamask. Let’s assume that the user already did the process of signature before playing the game and the architecture of this application contains a Client-Side, Server-Side, and a Service.
The image below illustrates the workflow for the game data update:
In this example, the game was a multiplayer competition where the users could play and score on a global scoreboard. After 10 hours, the competition ends, and the top user will be rewarded with the game token (this is a spoiler of one of the exploited targets :p). So let’s explain deeper about the process from the connection to the game token distribution:
- The connected user plays the game and scores the highest score;
- The game will send the new score to the back end to handle the data and make some verifications;
- After all verifications, the server will send the data to the storage;
- A response from the back-end and service to confirm the data modification;
- Same as above;
- When the competition ends, the Server will call a custom script to interact with the smart contract with the winner’s wallet address;
- After being called, the smart contract will execute the function payWinner with the winner user address;
- As this function was called via an account that has permission to execute it, the tokens will be sent to the user’s wallet.
Stop talking; show me the hacks
Manarium
According to their Gitbook, “Manarium is the first play-to-earn gaming platform for gamers and developers based on Binance Smart Chain. Manarium uses the play-to-earn model in each mini-game to reward the best players with tokens. It is a place where game developers can launch their mini-games and get the fee from the daily prize pool”. In other words, Manarium is a platform that allows users to play multiplayer games and earn the Game token, in this case, ARI.
Manarium has 3 games:
- Spiky Walls → Make the bird fly, but don’t touch the spikes!
- To The Moon → Launch your rocket as high into orbit as possible.
- In The Woods → Calm the monsters, don’t let them get angry.
All the games are based on the model P2E in the form of a daily tournament, in which players can compete with each other and receive the ARI Token at the end of the tournament (of course, if the user is eligible) directly in their wallet address. The games also have a scoreboard that will handle the player’s scores and rank them according to their points.
The scoreboard of winners will have 12 positions, where the first 10 users with the highest score will be placed, and the 2 remaining positions will be filled with 2 randomly sorted users that played the daily tournament.
Below, the table shows the prize distribution between the 12 winners of the daily tournament:
Regarding the prize pool, these are the words of Manarium: “For the first three months after the launch of the Manarium gaming platform, every play-to-earn game will get a daily prize boost of 15,000 ARI from the rewards wallet” and a plus: “The second part of the prize pool consists of players’ contributions. To participate in daily tournaments and have a chance to win a daily prize, every user needs to contribute a certain amount of tokens to the daily prize pool of the game. For a start, the fixed contribution will be 300 ARI”. So basically, when more players join the tournament, the highest will be the reward for the 12 winners.
Manarium uses the architecture composed of a Client-Side (that will be the Games, developed in Unity) and a Service (where the Game data will be stored, they have chosen Firebase), and for the winner prize distribution, the Admin will do the process, fetching the data from Service and calling the Smart Contract to execute the pay function.
The image below illustrates the Manarium Architecture:
- The user connects, plays the game, and makes a new score;
- The game will send the new score to the Service;
- Has the tournament ended?
- No
- Allow users to continue playing the game
- Yes
- Stop the data update on Service (Don’t allow users to play the game)
- No
- Has the tournament ended?
- The Admin will fetch the top 10 users with the highest score;
- After receiving the list of winners, the Admin will interact with the Smart Contract and call the function payWinners with the winners’ list.
- The winners’ list will receive their tokens based on their rank on the scoreboard for this tournament.
- A new tournament will be opened, and the scores will be reset
As we already know the Manarium arch and how it works, let’s exploit it 🙂
For this target in specific, let’s see the attack in 3 phases:
- The first one is related to the initial exploit discovered (this was reported by me to the Manarium team);
- The second is about the first bypass;
- The third is about the new bypass for the vulnerability and for their anti-cheat (after they ‘fixed’ the first vulnerability and implemented a ‘Super Secure Anti-Cheat’ :p).
Exploiting Manarium – 1st Phase
In this phase, the first exploit was discovered by analyzing one of their JavaScript files.
By looking into the file Build.framework.js inside the directory Build, I noticed a function that got my attention, _UpdateAccountScore (sounds good, no?!) at line 3377. Below we can see its code:
As we can see above, some parameters are passed to this function, which are:
- collectionPath → Game Name;
- key → Wallet Address;
- value → A JSON Object containing the wallet address and their score;
- objectName → The playground config (it’s irrelevant for the exploit);
- callback → A callback that will be called after the score update (again, irrelevant for exploit).
At line 3383, we can see where the data itself will be sent to Firebase, so, replacing the parameter for the values, this will be turned in:
firebase.firestore().collection(“GameName”).doc(“USER_WALLET”).set(JSON.parse(“{\”wallet\”:\”USER_WALLET\”,\”score\”:SCORE}”)
So, yes, we must only call this code in the Console Tab via the Game Window 🙂
To prove the successful exploitation, let’s see the images below…
Will this work? It’s only this? Yeah (in this phase, yes)
After refreshing the game page and looking again at the scoreboard, my score value was been changed to 1201:
Ok, you just changed the score, but what else is there? Remember, this is a P2E Game, so winners will be rewarded with their tokens, and this was what happened; as I was the first player, I won this tournament and received some tokens 🙂
Looking into the transaction 0x8b08003…330268 you can see that the tokens were sent to my address (the highest value of tokens):
This vulnerability is more dangerous because they didn’t verify if the user paid the initial tax (300 ARI) to play the game when making the payment (for winners), so anyone that just executes this code line could receive the tokens without playing the game or paying the tax.
Let’s go to the 2nd phase 🙂
Exploiting Manarium – 2nd Phase
Well, in this phase Manarium Team already fixed the vulnerability, but the fix was made in insecure ways; let’s see why.
How did Manarium “fix” the vulnerability?
- Manarium Team changed the way how to send the scoreboard to the Service, by adding authentication before sending the data, and this authentication must be done only via an admin account.
- The problem was… Manarium Team HARDCODED the credentials on the file Build.data, where after some disassembly, we found the following credentials:
- email: [email protected]
- password: zz___zzj6;ffd]@U8CW31123@nEw<
The image below illustrates the request made by the game after making a new score:
After further investigation, we found these credentials inside the file Build.data, as shown in the image below:
BINGO!! We were able to manipulate the game data by proving these credentials, generating a new JWT Token, and updating the score!
The code above will update the score of all wallets passed in the WALLETS list for the 3 games. In other words, my wallets will be inside the top 10 in all 3 games, and the highest profit will be returned (as we don`t need to pay the initial tax to enter the game).
Let’s take a look at the code to understand the process of getting the token and updating the score of a user.
First of all, we need to obtain a valid JWT token to use in the next requests. This will be made by the function step_1():
For this phase, that’s it. The bypass here occurred because the Manarium Team hardcoded the credentials of their firebase instance on the source code of the game.
Exploiting Manarium – 3rd Phase
This phase was the last phase of hacking this project, here Manarium made the following modifications to the project:
- Implemented a new fix for the vulnerability exploited previously;
- Implemented a Super Anti-Cheat system to ban users that tries to hack the game
- They changed their architecture, which anteriorly was composed of Client-Side and Service. After the implementation, their architecture is composed of Client-Side and Server-Side (a local database to store the Game data).
With the new infrastructure, the game has sent requests containing the data directly to the server-side, which now handles and stores these data.
During the analysis of the requests, we found this one in particular:
As we can see above, this request was used to retrieve a new JWT token that can be used to update users’ info.
The next request is responsible for updating the information itself and can be found in the image below:
The data parameter in this request contains all information about the player encoded in base64, which, when decoded we have the following value:
{
"gameTitle":"To The Moon",
"wallet":"0x64C…029B2",
"sessionTime":"2.804",
"timeUTC":"2022-01-09 17:17:19",
"ip":"XX.XXX.XX.XXX",
"gameVersion":2,
"score":2
}
gameTitle → The game name
wallet → The user wallet
sessionTime → The time that the user takes when playing
timeUTC →The current time
ip → The user IP
gameVersion → The version of the game
score →The score of the user in this try
Ok, but how does the ‘Super Anti-Cheat’ work in this version?
The anti-cheat validates the following fields: sessionTime, timeUTC, and score, where the user must have “sufficient time” to make the score. In other words, if a user scores 10 points in a sessionTime of 1 second (this is impossible, as the game has some obstacles that need to wait some time to avoid them), the anti-cheat will detect a possible cheater.
After 10-20 minutes of looking into this anti-cheat, we bypassed it by creating a script with a “human behavior” (a simple sleep and some random numbers) that will generate a high score in a “timed human-compatible.” Let’s look into the script Manarium Exploit – 3 Phase functions.
The function get_auth_token() is self explained:
The send_data() generates the body of the request with the generated values and will send the data to the back-end:
The generate_session_time() is the function to generate a human-compatible time for each score, where we calculated an average of 2 seconds per score and added a random number to be more randomized:
The main function has the brightness of the bot, where the ‘human behavior’ occurs. The first 3 lines will get and print the actual scoreboard:
Which will be something like this:
Inside the While loop, at line 287, we will generate a sessionTime based on the highest score (remember, the sessionTime is calculated with a score * 2.03 + some_random_number).
Line 290 will get the JWT Token, and line 291 will send the data. At line 294 we calculate the sleep_back_menu variable with the sessionTime value + 7.34 seconds (these seconds are related to refreshing the page and reentering the game), and at line 296 we pause the script execution with a sleep (here, we simulate a user attempt, lose and back again to the menu).
Line 300 will verify if our generated score is the highest score in this tournament; if yes, we fetch and print again the scoreboard, and at line 310, the call of sleep again with the values 3600 (1 hour) + a random number between 0-360, this was used to simulate a user that became the tournament leader and stop playing for 1 hour.
At line 319, if we aren’t the tournament leader, we’ll increase our score with a value between 3-12 (increase small values to not be detected with more significant different scores in every try).
The image below shows the execution of the script using a new wallet, this time the wallet 0x221…483eF:
The next image shows the scoreboard inside the game:
Finally, after this tournament ended, we can see in the transaction 0x78d…221e that my address received 1089 ARI Tokens as I was in 3rd place:
On the next versions of the script, we implemented some features, such as multithreading and the support of exploiting all the 3 games simultaneously.
How did Manarium fix the vulnerability?
Manarium changed all the first approaches, which were: requests with unsigned data that could be modified (or generated) by a user. The fix implemented works; we didn’t find a way to bypass this implementation so far, which consists of the use of Signed Requests.
Manarium now generates 3 requests, data1, data2, and data3. All these requests can be found below:
- data1
- data2
- data3
As we can see, the body of all these requests is signed with a private key, if we try to decode this body, a non-human readable message will result, and this key probably is inside the game.
Future research will focus on searching for this key and attempting again a new bypass.
References
The New Way To Sign Data In Your Browser – https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527
Manarium Gitbook – https://manarium.gitbook.io/manarium/LzHH7vsGZQZtJ9k8cYL3/
Manarium Token (ARI) – https://www.coingecko.com/en/coins/manarium
Manarium Games – https://www.manarium.com/games
Manarium Prize Pool – https://manarium.gitbook.io/manarium/LzHH7vsGZQZtJ9k8cYL3/manarium-platform/prize-pool