← Blog · · df00tech

Detecting Indicator Removal (T1070): KQL and SPL Queries for Event Log Clearing, Timestomping, and Audit Tampering

Detection Engineering MITRE ATT&CK KQL SPL Microsoft Sentinel Splunk Defense Evasion Threat Hunting

Most intrusions leave a trail. The better attackers try to wipe it before they leave. Clearing the Security log, turning off audit subcategories, deleting the NTFS USN journal and backdating dropped binaries are all ways to do that. MITRE ATT&CK groups them under T1070 Indicator Removal and T1562.002 Disable Windows Event Logging.

The good news for defenders is that a cover-up is usually noisier than the attack it hides. Normal admin work almost never clears a production Security log. When it happens, your SOC should treat it as a high-priority lead. This guide gives you working KQL (Microsoft Sentinel and Defender XDR) and SPL (Splunk) detections for five parts of the problem:

  • Event log clearing (T1070.001)
  • The commands used to clear or tamper with logs
  • Silencing logs without clearing them (T1562.002)
  • Hosts that are still online but have stopped sending security events
  • Timestomping (T1070.006)

Telemetry You Need First

None of these detections work unless the data is there. Before you deploy them, check these sources:

SignalSourceSentinel tableSplunk source
Security log clearedSecurity Event ID 1102SecurityEventXmlWinEventLog:Security
Any other log clearedSystem Event ID 104EventXmlWinEventLog:System
Audit policy changedSecurity Event ID 4719SecurityEventXmlWinEventLog:Security
Process command linesMDE / Sysmon EID 1 / 4688DeviceProcessEventsSysmon or Endpoint.Processes
File creation time changedSysmon Event ID 2EventXmlWinEventLog:Microsoft-Windows-Sysmon/Operational
PowerShell script contentEvent ID 4104EventXmlWinEventLog:Microsoft-Windows-PowerShell/Operational

One architectural point matters here. Forward events off the host in near real time. If your agent batches events every few hours, a local wevtutil cl destroys evidence that never reached the SIEM. Once events are forwarded, clearing the local log only removes the local copy. It also creates the 1102 event you are about to alert on.

Detection 1: Event Log Cleared (T1070.001)

Windows writes Event ID 1102 to the Security log when that log is cleared. It writes Event ID 104 to the System log when any other channel is cleared, including System, Application, Sysmon and PowerShell Operational. Both events survive the clear because they are written right after it.

KQL (Microsoft Sentinel)

union
  (SecurityEvent
    | where EventID == 1102
    | extend ClearedChannel = "Security"),
  (Event
    | where EventLog == "System" and EventID == 104
    | extend ClearedChannel = extract(@"The (.+) log file was cleared", 1, RenderedDescription))
| project TimeGenerated, Computer, Account, ClearedChannel, EventID, RenderedDescription
| sort by TimeGenerated desc

SPL (Splunk)

index=wineventlog
  ((source="XmlWinEventLog:Security" EventCode=1102)
   OR (source="XmlWinEventLog:System" EventCode=104))
| eval cleared_channel=if(EventCode=1102, "Security", Channel)
| stats min(_time) as first_seen max(_time) as last_seen values(user) as user
        values(cleared_channel) as cleared_channel count by host
| convert ctime(first_seen) ctime(last_seen)

Triage tip: For Event 104, the Channel field in Splunk and the rendered description in Sentinel tell you which log was cleared. Field names depend on your Windows add-on version. If user is empty, pull SubjectUserName from the UserData XML. Treat a cleared Sysmon or PowerShell Operational log as seriously as a cleared Security log. Attackers who know what you collect go after those channels first.

Detection 2: Log-Clearing and Tampering Commands

The 1102 event tells you a log was cleared. Process telemetry tells you how and by what. It also catches attempts that failed or were blocked. This query covers the common built-in tools:

  • wevtutil cl and wevtutil sl /e:false
  • The PowerShell Clear-EventLog, Remove-EventLog and Limit-EventLog cmdlets
  • The .NET EventLogSession.ClearLog method
  • auditpol /clear
  • USN journal deletion with fsutil usn deletejournal, which falls under file deletion and artifact removal

KQL (Defender XDR / Sentinel)

DeviceProcessEvents
| where Timestamp > ago(7d)
| where
    (FileName =~ "wevtutil.exe" and ProcessCommandLine matches regex @"(?i)\s(cl|clear-log)\s")
 or (FileName =~ "wevtutil.exe" and ProcessCommandLine matches regex @"(?i)\s(sl|set-log)\s" and ProcessCommandLine has "/e:false")
 or (FileName in~ ("powershell.exe", "pwsh.exe") and ProcessCommandLine has_any ("Clear-EventLog", "Remove-EventLog", "Limit-EventLog", "ClearLog("))
 or (FileName =~ "auditpol.exe" and ProcessCommandLine has_any ("/clear", "/remove", "disable"))
 or (FileName =~ "fsutil.exe" and ProcessCommandLine has "usn" and ProcessCommandLine has "deletejournal")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
          InitiatingProcessFileName, InitiatingProcessCommandLine
| sort by Timestamp desc

SPL (Sysmon process creation)

index=sysmon source="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1
  ( (Image="*wevtutil.exe" (CommandLine="* cl *" OR CommandLine="*clear-log*" OR CommandLine="*/e:false*"))
  OR ((Image="*powershell.exe" OR Image="*pwsh.exe") (CommandLine="*Clear-EventLog*" OR CommandLine="*Remove-EventLog*" OR CommandLine="*Limit-EventLog*" OR CommandLine="*ClearLog(*"))
  OR (Image="*auditpol.exe" (CommandLine="*/clear*" OR CommandLine="*/remove*" OR CommandLine="*disable*"))
  OR (Image="*fsutil.exe" CommandLine="*deletejournal*") )
| table _time host User ParentImage Image CommandLine
| sort - _time

Ransomware operators often loop wevtutil el into wevtutil cl to clear every channel on the host. You will see dozens of identical child processes in a few seconds. Group by host and parent process, and escalate straight away when the count goes above a handful.

Detection 3: Silencing Logs Without Clearing Them (T1562.002)

Experienced operators know that 1102 gets people's attention, so they stop logging instead. The main techniques are:

  • Removing audit subcategories, which generates Event ID 4719
  • Disabling individual channels through the WINEVT\Channels\<name>\Enabled registry value
  • Setting the Security autologger's Start value to 0, which takes effect at the next boot

KQL: Audit subcategory removed

SecurityEvent
| where EventID == 4719
// %%8448 = Success removed, %%8450 = Failure removed
| where AuditPolicyChanges has_any ("%%8448", "%%8450")
| project TimeGenerated, Computer, Account, SubcategoryGuid, AuditPolicyChanges

KQL: Event channel or autologger disabled via registry

DeviceRegistryEvents
| where ActionType == "RegistryValueSet"
| where (RegistryKey has @"\WINEVT\Channels\" and RegistryValueName =~ "Enabled" and RegistryValueData == "0")
     or (RegistryKey has @"\Control\WMI\Autologger\EventLog-" and RegistryValueName =~ "Start" and RegistryValueData == "0")
| project Timestamp, DeviceName, RegistryKey, RegistryValueName, RegistryValueData,
          InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessAccountName

SPL: Audit policy removal

index=wineventlog source="XmlWinEventLog:Security" EventCode=4719
  (AuditPolicyChanges="*%%8448*" OR AuditPolicyChanges="*%%8450*" OR AuditPolicyChanges="*removed*")
| stats values(SubcategoryGuid) as subcategories values(AuditPolicyChanges) as changes count by host, user
| sort - count

Tuning: Group Policy refreshes produce 4719 events that the machine account appears to initiate. If you manage audit policy centrally, baseline these events, or alert only when the change conflicts with your GPO. Removing Success auditing for Logon, Process Creation or Security Group Management should always be investigated. For attacks that stop logging at the tracing layer rather than through policy, see T1562.006 Indicator Blocking.

Detection 4: The Host Is Online but Not Logging

Some tools kill the Event Log service's worker threads or patch ETW in memory. They produce no 1102, no 4719 and no registry change. What they do leave is silence. The query below looks for hosts whose agent still checks in but whose Security log has stopped arriving.

KQL (Sentinel)

let silenceThreshold = 2h;
let lastSecurity = SecurityEvent
    | where TimeGenerated > ago(1d)
    | summarize LastSecurityEvent = max(TimeGenerated) by Computer = tolower(Computer);
Heartbeat
| where TimeGenerated > ago(30m)
| summarize LastHeartbeat = max(TimeGenerated) by Computer = tolower(Computer)
| join kind=inner lastSecurity on Computer
| where LastSecurityEvent < ago(silenceThreshold)
| extend SilentFor = now() - LastSecurityEvent
| project Computer, LastHeartbeat, LastSecurityEvent, SilentFor
| sort by SilentFor desc

SPL (forwarder internal logs as the heartbeat)

| tstats latest(_time) as last_security where index=wineventlog source="XmlWinEventLog:Security" earliest=-24h by host
| join type=inner host
    [| tstats latest(_time) as last_forwarder where index=_internal earliest=-30m by host]
| where last_security < relative_time(now(), "-2h")
| eval silent_hours=round((now()-last_security)/3600, 1)
| convert ctime(last_security) ctime(last_forwarder)
| sort - silent_hours

Hostname formats often differ between tables (FQDN vs. short name), so normalise them before you join. Quiet workstations such as kiosks and lab machines can legitimately go two hours without a Security event. Tune the threshold per asset class instead of using one global value.

Detection 5: Timestomping (T1070.006)

Timestomping changes a file's timestamps so a dropped payload looks as old as the operating system. Sysmon Event ID 2 records every change to a file's creation time, with both the new and the previous value. The strongest signal is an executable or script whose creation time was moved backwards by a large amount.

KQL (Sysmon in Sentinel)

Event
| where Source == "Microsoft-Windows-Sysmon" and EventID == 2
| extend EvData = parse_xml(EventData)
| mv-apply d = EvData.DataItem.EventData.Data on (
    summarize F = make_bag(bag_pack(tostring(d["@Name"]), tostring(d["#text"])))
  )
| extend Image = tostring(F.Image),
         TargetFilename = tostring(F.TargetFilename),
         NewCreation = todatetime(F.CreationUtcTime),
         PrevCreation = todatetime(F.PreviousCreationUtcTime)
| extend DaysBackdated = datetime_diff('day', PrevCreation, NewCreation)
| where DaysBackdated > 30
| where TargetFilename matches regex @"(?i)[.](exe|dll|sys|ps1|bat|vbs|js|aspx|jsp|php)$"
| extend ProcessName = tolower(extract(@"([^\\]+)$", 1, Image))
| where ProcessName !in ("msiexec.exe", "7z.exe", "7zg.exe", "winrar.exe", "robocopy.exe", "onedrive.exe", "trustedinstaller.exe")
| project TimeGenerated, Computer, Image, TargetFilename, PrevCreation, NewCreation, DaysBackdated

SPL (Sysmon)

index=sysmon source="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=2
| eval new_ct=strptime(CreationUtcTime, "%Y-%m-%d %H:%M:%S.%3N"),
       prev_ct=strptime(PreviousCreationUtcTime, "%Y-%m-%d %H:%M:%S.%3N"),
       days_backdated=round((prev_ct-new_ct)/86400, 1),
       process_name=lower(mvindex(split(Image, "\\"), -1))
| where days_backdated > 30 AND match(TargetFilename, "(?i)[.](exe|dll|sys|ps1|bat|vbs|js|aspx|jsp|php)$")
| search NOT process_name IN ("msiexec.exe", "7z.exe", "7zg.exe", "winrar.exe", "robocopy.exe", "onedrive.exe", "trustedinstaller.exe")
| table _time host Image TargetFilename PreviousCreationUtcTime CreationUtcTime days_backdated

Archive extractors, installers and sync clients set file times as normal behaviour. That is why the allowlist is keyed on process name. Keep a closer eye on web server directories, where a backdated .aspx or .jsp file is a classic way to hide a web shell.

Many PowerShell timestomps set LastWriteTime or LastAccessTime, which Sysmon Event ID 2 does not record. Cover those with script block logging:

Event
| where Source == "Microsoft-Windows-PowerShell" and EventID == 4104
| where EventData matches regex @"(?i)\.(CreationTime|LastWriteTime|LastAccessTime)(Utc)?\s*="
   or EventData has "SetCreationTime"
| project TimeGenerated, Computer, EventData

During forensics, check whether the NTFS $STANDARD_INFORMATION and $FILE_NAME timestamps disagree. Most user-mode timestomping tools only change $STANDARD_INFORMATION.

Triage Playbook

  1. Pivot backwards in time. A clear or a logging gap marks the end of the activity someone wanted to hide. Pull everything your SIEM already has for that host from the previous 24 hours: logons, new services, scheduled tasks and outbound connections.
  2. Identify the actor. Look at the account and parent process behind the clear. An interactive admin session is a very different lead from cmd.exe spawned by a web server or RMM agent.
  3. Check for spread. Search for the same command line on other hosts. Ransomware crews usually clear logs across the whole estate right before or after encryption.
  4. Restore visibility. Re-enable disabled channels and audit subcategories, then confirm events are arriving again before you close the incident.

Tuning and False Positives

  • Image builds and Sysprep may clear logs during golden-image preparation. Scope exclusions to build OUs and build service accounts, never to all hosts.
  • Log rollover is not clearing. A full log that overwrites old events does not generate 1102. If you see 1102 on a server, someone or something asked for it.
  • Tie severity to asset tier. Log clearing on a domain controller, certificate authority or backup server should page someone. On a developer VM it can wait for the queue.

Indicator removal almost never happens on its own. Build correlations with these detection pages:

Start with Detection 1. It is cheap, it rarely fires on benign activity, and it works on the data most SOCs already collect. Then add the silence-gap query, which catches the tools that avoid every other signal on this page.

Get new detections in your inbox

New ATT&CK coverage plus CISA KEV / CVE detection rules, roughly weekly. No spam, unsubscribe anytime.