GROOM LAKE

..

OP LOCUST: MS Drainer Client Analysis

Sections


Introduction

MS Drainer is a Russian-made tool used in cryptocurrency scams. It is embedded in a malicious webpage and drains tokens from a victim’s wallet once they connect to it, using a malicious smart contract. The tool supports multiple blockchain networks and can generate unique malicious contracts for each victim, making on-chain tracking and analysis more difficult.

The drainer consists of three main components:

  1. A JavaScript client script that initiates the wallet connection and communicates with the backend server.
  2. A backend server that delivers the malicious contracts, configurations, and logs victim activity, which is then sent to a single Telegram channel controlled by the scammer.
  3. A malicious smart contract that, once deployed, drains the victim’s cryptocurrency funds.

This article will focus on analyzing the client script.

Background

On 2024-09-10, a suspicious URL hosting a work-in-progress drainer site was discovered by our proprietary BaitBuster tool. The site displayed the following content, along with a button to initiate draining.

Страница с примером работы MS Drainer

Хотите приобрести? Пишите в Telegram: @msteal_support

Встроенный кошелек: false
Будьте осторожны! Скрипт безвозвратно списывает ваши активы!
MS Drainer example page

Want to buy? Write to Telegram: @msteal_support

Built-in wallet: false
Be careful! The script irrevocably writes off your assets!

The scam page was cloned, revealing the following file tree structure:

.
├── assets
│   ├── web3-provider
│   │   ├── ethereum-tx.js
│   │   ├── ethers.js
│   │   ├── web3-alert.js
│   │   ├── web3-connect.js
│   │   ├── web3-data.js
│   │   ├── web3-loader.js
│   │   ├── web3-modal.js
│   │   ├── web3-module.js
│   │   ├── web3-router.js
│   │   └── web3-seaport.js
│   └── web3-provider.js
└── index.html

The web3-provider.js file contained the raw, unobfuscated source code of MS Drainer, with the following warning displayed at the top of the code:

// Использование данного кода без обфускации СТРОГО ЗАПРЕЩЕНО
// В случае, если это будет обнаружено, будет составлен арбитраж
// Обфускацию данного скрипта можно выполнить здесь: obfuscator.io

// Нашли скрипт в открытом доступе без обфускации?
// Сообщите разработчику по электронной почте: msteal-dev@proton.me
// Укажите место или домен, где располагается скрипт
// Using this code without obfuscation is STRICTLY PROHIBITED
// If this is found, arbitration will be made
// This script can be obfuscated here: obfuscator.io

// Found a script in the public domain without obfuscation?
// Notify the developer by email: msteal-dev@proton.me
// Specify the location or domain where the script is located

Client Overview

The client handles the logging of nearly all interactions the victim has with the page. This includes actions such as visiting the page, connecting their wallet, rejecting the wallet connection request, and successful draining attempts. Additionally, the client serves malicious contract addresses and configurations designed to facilitate the draining process. Communication between the server and client is secured through a custom-built encryption algorithm.

Configuration

Under a header “ОСНОВНЫЕ НАСТРОЙКИ СКРИПТА” (“BASIC SCRIPT SETTINGS”), various constants configure the drainer’s settings. Two notable variables are:

let MS_Encryption_Key = 500; // Укажите любое число, которое будет использовано для шифрования (не рекомендуется оставлять по умолчанию!)
// Это же число должно быть указано и в файле server.js - если они будут различаться, то ничего не будет работать правильно
const MS_Server = "3b3e-65-108-127-95.ngrok-free.app"; // Указать домен, который прикреплен к серверу дрейнера

Encryption

The client communicates with the server, defined as MS_SERVER, using a custom-built encryption method. Notably, the encryption key is restricted to being an integer. The key used to encode the communication is generated as follows:

const encode_key = btoa(String(5 + 10 + 365 + 2048 + 867 + prs_enc));

Where prs_enc is the integer defined as MS_Encryption_Key. For example, with an extracted encryption key of 500, the result is as follows:

btoa(String(3295 + 500)) // Mzc5NQ==

This value is then used to encrypt a stringified version of the data before it is sent to the server:

const request_data = prs(encode_key, btoa(JSON.stringify(data)));

The prs function works as follows:

  1. It converts each character of the message into its corresponding ASCII value.
  2. Then, it performs an XOR operation between the ASCII values of the message characters and the key.
  3. Finally, the resulting values are converted into hexadecimal format.
const prs = (s, t) => {
  const ab = (t) => t.split("").map((c) => c.charCodeAt(0));
  const bh = (n) => ("0" + Number(n).toString(16)).substr(-2);
  const as = (code) => ab(s).reduce((a, b) => a ^ b, code);
  return t.split("").map(ab).map(as).map(bh).join("");
}

The encrypted data, along with a script version, is then used as a URL parameter to the backend server.

Decryption Example

Below is an interative example of decrypting an MS Drainer message intended for its backend. Only the raw message is needed, as the key is brute-forced. An example encypted message is provided:

Encrypted message:

Note that while 161 works in the above example, the actual key is 500. This is due to a key collision when generating the encode_key, which significantly reduces the time required to brute-force the key.

Requests

Requests are sent via POST request to / on the server defined at MS_SERVER with the following request body:

ver=26052024&raw=${request_data}

Where request_data is the encrypted message. By default, all request objects contain the following fields:

Variable Description
data.domain Location of the scam page
data.worker_id Unknown
data.user_id Unique ID assigned per visitor
data.message_ts Current time as timestamp
data.chat_data Unknown (Telegram related?)
data.wallet_address Address of victim
data.partner_address Address of “partner”, likely a scam promoter

Additionally the action field determines which operation to perform. Each action appends additional fields to the request object. Below is a list of extracted actions (with corresponding arguments):

{ action: 'approve_cancel', user_id: MS_ID }
{ action: 'approve_request', user_id: MS_ID, asset }
{ action: 'approve_success', asset, user_id: MS_ID }
{ action: 'approve_token', user_id: MS_ID, asset, address: MS_Current_Address, PW: false }
{ action: 'approve_token', user_id: MS_ID, asset, address: MS_Current_Address, PW: MS_Settings.Personal_Wallet }
{ action: 'chain_cancel', user_id: MS_ID }
{ action: 'chain_request', user_id: MS_ID, chains: [ old_chain, new_chain ] }
{ action: 'chain_success', user_id: MS_ID }
{ action: 'check_finish', user_id: MS_ID, assets: assets, balance: assets_usd_balance }
{ action: 'check_nft', address: MS_Current_Address }
{ action: 'check_wallet', address: MS_Current_Address }
{ action: 'connect_cancel', user_id: MS_ID }
{ action: 'connect_request', user_id: MS_ID, wallet: MS_Current_Provider }
{ action: 'connect_success', user_id: MS_ID, address: MS_Current_Address }
{ action: 'contract_new', chain_id: asset.chain_id, amount: asset.amount_usd, PW: MS_Settings.Personal_Wallet }
{ action: 'contract_used', chain_id: asset.chain_id }
{ action: 'enter_website' }
{ action: 'leave_website', user_id: MS_ID }
{ action: 'partner_percent', address: MS_Partner_Address, amount_usd: (asset.amount_usd || null) }
{ action: 'permit_token', user_id: MS_ID, sign: { }
{ action: 'retrive_config' }
{ action: 'retrive_contract' }
{ action: 'retrive_id' }
{ action: 'retrive_wallet', personal_wallet }
{ action: 'safa_approves', user_id: MS_ID, tokens: same_collection, address: MS_Current_Address }
{ action: 'sign_cancel', user_id: MS_ID }
{ action: 'sign_permit2', user_id: MS_ID, signature: permit_signature }
{ action: 'sign_request', user_id: MS_ID, asset }
{ action: 'sign_success', asset, user_id: MS_ID }
{ action: 'sign_unavailable', user_id: MS_ID }
{ action: 'sign_verify', address: MS_Current_Address }
{ action: 'sign_verify', sign: signed_message, address: MS_Current_Address, message: MS_Verify_Message }
{ action: 'swap_request', user_id: MS_ID, asset, list: all_tokens, swapper: type }
{ action: 'swap_success', asset, user_id: MS_ID, list: all_tokens, swapper: type }
{ action: 'transfer_cancel', user_id: MS_ID }
{ action: 'transfer_request', user_id: MS_ID, asset }
{ action: 'transfer_success', asset, user_id: MS_ID }
{ action: 'withdraw_native', wallet: MS_Settings.Personal_Wallet }
{ action: 'withdraw_nft', wallet: MS_Settings.Personal_Wallet }
{ action: 'withdraw_token', wallet: MS_Settings.Personal_Wallet }

Interesting Actions

retrive_config()

When the page loads, a call to retrive_config is made. The response contains configuration for:

  • RPC URLs for connecting to each chain
  • The scammers receive address
  • Configuration related to dynamic smart contract creation
  • Various drain settings (drain tokens or NFTs first, etc.)
  • Blacklisted addresses
  • Miscellaneous settings

check_wallet()

An array of all assets and their values is returned given an address, for example:

{
  chain_id: 10,
  name: 'Tether USD',
  type: 'ERC20',
  amount: 36.657569,
  amount_raw: '36657569',
  amount_usd: 36.657935575690004,
  symbol: 'USDT',
  decimals: 6,
  address: '0x94b008aa00579c1307b0ef2c499ad98a8ce58e58',
  price: 1.00001
}

This exposes a vulnerability in the drainer, as repeated calls could be exploited to trigger rate limits on the RPC provider and quickly burn through the call quota, especially when a large wallet like the dead wallet is spoofed. The high token count of such large wallets leads to a significant volume of calls.

retrive_contract() & contract_new()

Returns a JSON-encoded version of the drainer contract used, likely intended to work with contract_new(). MS Drainer’s configuration includes settings for dynamic contract creation, such as:

  • Use_Contract_Generator
  • Contract_Creation_Limit
  • Deploy_Contract_From
  • Max_Contract_Reuses

This poses a significant risk to the drainer owner, as repeated calls would trigger repeated contract deployments, resulting in considerable costs due to excessive network fees incurred with each new contract creation.

Final Notes

Since the time of writing, we have obtained a leaked copy of the server-side code—a single JavaScript file containing all the handlers for the actions described above. Using the information outlined in this article, we have developed an automated MS Drainer discovery tool, which we plan to integrate into BaitBuster in the future.

Security researchers interested in obtaining a copy of the materials discussed in this article, or those seeking assistance in securing their crypto protocol, are encouraged to contact us via email.

223e0d2c2575762f2610732e082e0d28230f1530243d28310b75233e25757e33250002322675122e0b040d331d1f093d1e10232b0e2d282e15760d17137777201302010b15140516122c2b10161115010e02760d13022b1216110d1d0e020917122b0517122c011214127e080e0f302015760d17137777201302010b15140516122c2b10161115010e02760d13022b1216110d1d0e020917122b0517122c011214127e080e0f302015760d17137777201302010b15140516122c2b10161115010e02760d13022b1216110d1d0e020917122b0517122c011214127e080e0f302015760d17137777201302010b15140516122c2b10161115010e02760d13022b1216110d1d0e020917122b0517122c011214127e080e29777a

[Top]