In this post, we’ll share details from a recent, non-published, Nitrogen ransomware case, including how the attackers gained initial access, their lateral movement across systems (confirmed through user access logs), and how they attempted to cover their tracks by clearing logs. By examining Windows Error Reporting (WER) and crash dump files, we uncovered a Cobalt Strike configuration, along with a Cobalt Strike C2 team server and the attacker’s use of a pivot system.
Malvertising to Gain Initial Access
In recent months, threat actors have leveraged targeted Nitrogen-themed malvertising, bundling malicious code within tools that appear legitimate. For instance, thedfirreport documented a Nitrogen campaign that distributed a fake “Advanced IP Scanner,” ultimately leading to a BlackCat ransomware infection. Similar malvertising tactics have been observed with disguised versions of FileZilla and WinRAR.
During one of our recent investigations, a user searching for “WinSCP download” via Microsoft Edge clicked on a suspicious ad served through Bing. The ad redirected them from ftp-winscp[.]org
to a compromised WordPress site hosting a malicious WinSCP ZIP file — establishing the initial foothold (“beachhead”) in a broader attack chain.
Within the ZIP archive, WinSCP-6.3.6-Setup.zip
(SHA-256: fa3eca4d53a1b7c4cfcd14f642ed5f8a8a864f56a8a47acbf5cf11a6c5d2afa2
), several files were bundled: a malicious python312.dll
, three legitimate DLLs, and a renamed python.exe
labeled setup.exe
. Once the user ran setup.exe
, DLL sideloading occurred — WinSCP was installed in the foreground while the malicious DLL was loaded into the running process.
As indicated by the imports in setup.exe, python312.dll is invoked as a dependency at runtime, triggering the execution of the malicious DLL. Because the file path for the DLL is not defined with an absolute file path in setup.exe, Windows relies on its default DLL search order: it first checks the application’s directory, then the system directory, the Windows directory, and finally the PATH environment variable if the DLL is still not found.
Closer inspection of the malicious DLL, also referenced as the “NitrogenLoader,” shows that it mirrors the same exports and ordinals found in a genuine Python DLL. For example, it includes the Py_Main
export mentioned in the setup.exe
import table. However, whereas a legitimate python312.dll
(for instance, 278f22e258688a2afc1b6ac9f3aba61be0131b0de743c74db1607a7b6b934043
) features authentic logic, the malicious file uses a minimalist approach, returning null instructions instead.
Its primary malicious backdoor functionality resides in the DllMain export, in which the packed connect-back logic establishes a C2 connection. Various forensic artifacts — including Prefetch files on the compromised Windows client — confirmed that setup.exe
and, consequently, python312.dll
executed successfully, ultimately compromising Patient Zero.
Windows Host Triaging
Typically, when analyzing a system — unless you’re performing a scheduled compromise assessment — you have some lead pointing you toward the right direction for your forensic investigation. Doing forensics without a clear lead or well-defined questions is like setting off on vacation without deciding where you want to go. With that in mind, we rely on a battle-tested workflow to analyze systems and determine which tools to run, a process we refer to as “preparational forensics”. It’s partially automated, so we don’t have to deploy the same tools every time manually. As usual, we started off by analyzing “patient zero” with Velociraptor’s triage output.
After confirming infection, we took a full disk image. We won’t go into every detail of our standard deep-dive workflow here, but one key step we always take is to run THOR and look for recently created executables in the Master File Table. We focused on executables created that same day because we knew the exact timestamp of the WinSCP infection and suspected the threat actor might have used a C2 framework like Cobalt Strike. This approach led us to files named Intel64.exe
, tcpp.exe
, and IntelGup.exe
.
As mentioned before, it’s also possible to run THOR against a system image, or, as we did, a mounted disk image by running thor64.exe --lab -p F:\ --htmlfile A:\Artifacts\case\output.html
, where the F:\
drive served as the mount point.
Another option is to aim THOR directly at specific files of interest created on the day of initial infection, which, in this case, flagged tcpp.exe
as containing a potential Cobalt Strike configuration. A byte pattern in this specific file that stood out was the recurring value 0x2e
, a default XOR key for encrypting configurations in many versions of Cobalt Strike. Whenever we see stretches of 0x2e
or 0x69
in a file, it usually indicates XOR-encrypted null bytes.
Several methods can help reveal more details about this potential Cobalt Strike configuration. The one we typically use is to copy the suspicious byte section and decrypt it using CyberChef or Python. From there, we can export the decrypted data and feed it into a Cobalt Strike parser.
The first step is to copy the 0x2e
pattern, paste it into CyberChef, and decrypt it using 0x2e
. Straight away, you can see interesting strings appearing.
Next, we can download the decrypted blob and leverage Sentinel One’s CobaltStrikeParser, extracting and parsing even more information.
A particularly noteworthy aspect of the detected Cobalt Strike configuration was its reference to the internal IP address 192.168.101.XXX
on port 5000
, which happened to match patient zero’s own IP. This detail strongly suggests that patient zero was being used as a pivot for a Cobalt Strike beacon — a conclusion that became even clearer later in our investigation. We also observed that gpupdate.exe
was employed as a sacrificial process for Cobalt Strike, as post-compromise payloads are typically injected into dedicated processes.
Note: The manual process described above for extracting Cobalt Strike configurations using the 0x2e pattern will soon be obsolete. THOR v11 includes a built-in feature that automatically detects, decrypts, and parses Cobalt Strike Beacon configurations — directly during the scan, no manual steps required. This feature will be covered in more detail in an upcoming blog post.
Interjection – Cobalt Strike Detection and Threat Intel
From these strings — for example, @%windir%\syswow64\gpupdate.exe, @%windir%\sysnative\gpupdate.exe
, and the watermark hash S+sMUHERQLpRZukekGExAw==
— we can build a custom YARA rule. Encrypting each of these strings with all possible single-byte values makes it possible to detect additional XOR-encrypted Cobalt Strike configurations, not only on patient zero but also on other potentially compromised hosts.
#!/usr/bin/env python3 def main(): results = [] str_input = ["@%windir%\syswow64\gpupdate.exe", "@%windir%\sysnative\gpupdate.exe", "S+sMUHERQLpRZukekGExAw=="] for string in str_input: for key in range(256): # 0x00 through 0xFF xored_bytes = [ord(ch) ^ key for ch in string] # XOR each character xored_hex = "".join(f"{byte:02x}" for byte in xored_bytes) results.append((key, xored_hex)) # Write results to file with open("output.txt", "w", encoding="utf-8") as f: i = 0 for key, xored_str in results: f.write(f"$s{i} = \"{xored_str}\"\n") i += 1 print("All XOR variations written to output.txt") if __name__ == "__main__": main()
Using the script’s output, we can create a very simple YARA rule to be used during the engagement, potentially highlighting even more suspicious files like the one we already discovered.
Notably, the identified Cobalt Strike watermark 678358251 has previously been listed on abuse.ch. This watermark has been associated with multiple threat actors, including the ransomware group Black Basta, further highlighting its reuse across malicious campaigns and threat actors. Cobalt Strike watermarks serve as unique identifiers, allowing to track and correlate activity across disparate Cobalt Strike C2 servers observed in the wild.
Detecting Lateral Movement with User Access Logging
After identifying patient zero, we set out to locate further compromised hosts. Tracking lateral movement from patient zero proved challenging because artifacts on the source system are typically less thorough than those on the destination. Complicating matters even more, the threat actor had cleared critical Windows event logs — among them the Security, System, and PowerShell logs — on several machines, as shown in the following screenshot.
Nevertheless, not all forensic data was lost. Even with extensive log clearing, we built a supertimeline on one of the client’s physical Windows servers, revealing User Access Logging (UAL) entries. These entries provided clear evidence of lateral movement to another Windows Server originating from the beachhead on the exact date of the initial compromise.
Basic Crash Dump Triaging
When we reran THOR against the newly uncovered compromised server system, it yielded some additional leads. In this instance, we discovered a suspicious operating system svchost.exe
file that presented telltale signs of Cobalt Strike activity.
We also found that a Windows Error Reporting (WER) log for this particular svchost.exe
was saved in C:\ProgramData\Microsoft\Windows\WER
. WER is a highly underrated artifact capable of capturing detailed debug information, such as the application name, loaded modules, and a heap dump that preserves the memory data active at the time of a crash. If configured to do so, WER also collects user-mode crash dumps and stores them locally whenever an application crashes — exactly the situation THOR detected here. Although crash dumps are disabled by default, administrators can enable them by configuring the registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps
.
In recent years, these crash dumps have improved considerably and can be analyzed in more depth using tools like WinDBG — a process we’ll explore in the next chapter. In this specific scenario, we verified the crash dump settings by reviewing the registry keys and confirmed that a full dump (dump type 2), which includes all virtual memory, was being saved to the %LOCALAPPDATA%\CrashDumps
directory, with a maximum of ten dump files retained.
From the svchost.exe.17872.dmp
crash dump we identified through THOR, several suspicious string artifacts pointed to a possible Cobalt Strike beacon configuration. THOR referenced a GitHub repository — “Detects specific keywords found in Malleable C2 profiles for Office 365 Calendar” — indicating that both client and server configuration details, including cookie header values from the client and custom headers from the server, had been embedded within the crash dump.
To confirm these findings, we used bstrings.exe
to extract strings from the crash dump running bstrings.exe -f .\svchost.exe.17872.dmp > .\svchost.exe.17872.dmp.strings.txt
. This process uncovered the precise configuration strings highlighted earlier, revealing what appeared to be an entire HTTP response. We even found a Server
header that matched the system responding to the request.
We repeated this methodology until no additional pertinent strings emerged, then ran bstrings.exe
to focus specifically on URLs: bstrings.exe --lr url3986 -f .\svchost.exe.17872.dmp -q –sa
. That step exposed the Cobalt Strike team server, confirming our suspicions regarding an active beacon configuration within the crash dump.
Crash Dump Analysis with WinDBG
In this scenario, the process crash dump was configured to capture a full user-mode dump that included all virtual memory. Having access to a full dump file allowed for a thorough examination of the process at the time it failed. By loading the crash dump directly into WinDBG, the debugger halted at the specific exception that caused the crash and displayed the associated thread — thread 0x5
with an ID of 0x4c78
— along with a reference to the full memory dump type. The debugger also showed the debug session time, which matched the timestamp of the crash dump’s creation.
The available information showed that a failure occurred while the process executed the kernel32
function CreateFileA
(0x4c78 0x5 kernel32!CreateFileA (00007ffd'2ac44960)
). Running !analyze –v
initiated the exception analysis, revealing details about the operating system version, build, CPU registers, and a stack trace, alongside an error code. Unfortunately, the error code did not yield any additional clues, only indicating that the exception must have happened before the error handling routine at 00007ffd'12c5ac52 mscorlib_ni!System.Environment.ResourceHelper.GetResourceStringCode+0x252
.
To gather more insights, the MEX extension provided the command !mex.di
(or simply !di
when using built-in aliases). This command revealed information about the user under whose account the process was running, as well as the operating system version, system uptime, and the process ID.
Further investigation involved the !peb
command, which examined the Process Environment Block (PEB) — a structure containing details on loaded modules, command-line arguments, the image file in use, and the window title for the process. In this instance, the PEB indicated that the process path was C:\StorageReport\tcpp.exe
, a file previously identified as a Cobalt Strike pivot beacon that facilitated tunneling through the patient zero system. With a Cobalt Strike configuration discovered in memory (as supported by the string analysis), it was apparent that malicious activity had been running within this process.
These same details could have been extracted manually by inspecting the PEB structure without relying on the !peb
extension. Typically, one would locate the PEB address first by referencing the pseudoregister $peb
(dt @$peb
). In a kernel-mode dump, the command !process -0 0
would also yield the PEB location. With that address in hand — in this case, 0x000000b977fe1000
— the relevant data can be read by issuing a command such as dt _PEB 000000b977fe1000
.
0:005> dt @$peb Symbol not found at address 000000b977fe1000. 0:005> dt _PEB 000000b977fe1000 ole32!_PEB +0x000 Reserved1 : [2] "" +0x002 BeingDebugged : 0 '' +0x003 Reserved2 : [1] "???" +0x008 Reserved3 : [2] 0xffffffff`ffffffff Void +0x018 Ldr : 0x00007ffd`2af203c0 _PEB_LDR_DATA +0x020 ProcessParameters : 0x000001e4`ef132160 _RTL_USER_PROCESS_PARAMETERS +0x028 Reserved4 : [3] (null) +0x040 AtlThunkSListPtr : (null) +0x048 Reserved5 : (null) +0x050 Reserved6 : 4 +0x058 Reserved7 : 0x00007ffd`292bf000 Void +0x060 Reserved8 : 0 +0x064 AtlThunkSListPtr32 : 0 +0x068 Reserved9 : [45] 0x000001e4`eef20000 Void +0x1d0 Reserved10 : [96] "" +0x230 PostProcessInitRoutine : (null) +0x238 Reserved11 : [128] "???" +0x2b8 Reserved12 : [1] (null) +0x2c0 SessionId : 0
It is in the _PEB_LDR_DATA
member that key information regarding loaded modules resides, as documented by Microsoft. The InMemoryOrderModuleList
field within the _PEB_LDR_DATA
structure is a doubly linked list of loaded modules, so walking this list can provide details on every module.
0:005> dt _LIST_ENTRY ole32!_LIST_ENTRY +0x000 Flink : Ptr64 _LIST_ENTRY +0x008 Blink : Ptr64 _LIST_ENTRY
This includes the primary image executable (in this instance, svchost.exe
) and subsequent items referenced in its InMemoryOrderLinks
or InLoadOrderLinks
fields.
The first loaded module points to the next one via its InMemoryOrderLinks
or InLoadOrderLinks
member, which, in this instance, leads to the address 0x000001e4ef132950
. Because that address is also of type _LIST_ENTRY
, the command dt _LDR_DATA_TABLE_ENTRY 0x000001e4ef132950
can reveal details about the next link. This manual approach — iterating through the linked list entry by entry — proves especially useful when you need to investigate a specific module or structure in greater depth.
Returning to the original purpose — gathering conclusive evidence of a Cobalt Strike beacon residing in memory — analysis continued by examining suspicious strings and testing them against a Cobalt Strike YARA rule by Elastic.
Observed strings were traced to the corresponding memory address within the dump, revealing that all originated from a similar region. Searching for the MZ header indicated the presence of what looked like a loaded binary at that location.
By investigating the DOS header (ntdll!_IMAGE_DOS_HEADER
at 000001e4'eef80000
), one can identify the PE header offset (e_lfanew
), determine the approximate size of the binary (SizeOfImage
), and theoretically dump that data. However, it is important to note that paging can cause portions of memory to be absent from the dump file, so the extracted DLL may be incomplete or partially overwritten.
0:005> dt -r ntdll!_IMAGE_DOS_HEADER 000001e4`eef80000 +0x000 e_magic : 0x5a4d +0x002 e_cblp : 0x90 +0x004 e_cp : 3 […] +0x028 e_res2 : [10] 0 +0x03c e_lfanew : 0n184 0:005> ? 000001e4`eef80000 + 0n184 Evaluate expression: 2082773401784 = 000001e4`eef800b8 0:005> dt -r _IMAGE_NT_HEADERS 000001e4`eef800b8 ole32!_IMAGE_NT_HEADERS +0x000 Signature : 0x4550 +0x004 FileHeader : _IMAGE_FILE_HEADER +0x000 Machine : 0x14c +0x002 NumberOfSections : 2 +0x004 TimeDateStamp : 0 +0x008 PointerToSymbolTable : 0 +0x00c NumberOfSymbols : 0 +0x010 SizeOfOptionalHeader : 0xe0 +0x012 Characteristics : 0x2102 +0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER +0x000 Magic : 0x10b […] +0x038 SizeOfImage : 0x3000 +0x03c SizeOfHeaders : 0x200 +0x040 CheckSum : 0xc085 +0x044 Subsystem : 2 +0x046 DllCharacteristics : 0x540 +0x048 SizeOfStackReserve : 0x100000 +0x04c SizeOfStackCommit : 0x1000 +0x050 SizeOfHeapReserve : 0x100000 +0x054 SizeOfHeapCommit : 0x1000 +0x058 LoaderFlags : 0 +0x05c NumberOfRvaAndSizes : 0x10 +0x060 DataDirectory : [16] _IMAGE_DATA_DIRECTORY +0x000 VirtualAddress : 0 +0x004 Size : 0
Using .writemem
in WinDBG with an appropriate address range (000001e4'eef80000 L3000
) attempts to write this region to disk. In this case, portions of memory at 000001e4'eef81000
were unreadable, likely due to paging, and the range did not encompass the exact strings indicative of the beacon configuration.
Consequently, additional blocks of memory were dumped around the suspicious strings — for instance, those containing %02d/%02d/%02d %02d:%02d:%02d
, %s (admin)
, or Content-Length: %d
— in an effort to capture more complete data. Although this did not yield a fully parsable beacon configuration in this specific instance, the discovered indicators, combined with previous string analysis, further reinforced that a Cobalt Strike payload had indeed been running within the process at the time of the crash.
Summing Up
The Nitrogen ransomware group exemplifies a modern, multi-stage intrusion operation that blends social engineering, evasive malware, and post-exploitation frameworks. By abusing malvertising — often disguising payloads as legitimate tools like WinSCP, Advanced IP Scanner, or FileZilla — Nitrogen establishes initial access via DLL sideloading, with malicious loaders delivering backdoor functionality through NitrogenLoader.
Once inside the network, Cobalt Strike becomes their tool of choice for lateral movement, command and control, and post-compromise activity. In our case study, Nitrogen used a compromised host as a pivot system while simultaneously wiping critical Windows logs to hinder detection and response efforts.
Throughout this post, we highlighted various ways to detect and extract Cobalt Strike configurations, including pattern analysis, byte-level XOR decryption, and custom YARA rules. In particular, we emphasized the power of crash dump analysis — specifically using Windows Error Reporting (WER) artifacts and WinDBG — to uncover in-memory indicators of Cobalt Strike beacons, configuration strings, and HTTP response structures embedded in dump files.
With that being said—stay safe, make use of lesser-known artifacts like WER, crash dumps, and UAL — and always read the labels before you install something from an ad.