T1647

Plist File Modification

Defense Evasion Last updated:

This detection identifies adversarial modification of macOS property list (plist) files to enable persistence, evade defenses, or alter application behavior. Attackers use tools such as plutil, PlistBuddy, and the defaults command to insert or modify keys like LSUIElement (hide app from UI), LSEnvironment (inject environment variables for dynamic linker hijacking), RunAtLoad, and ProgramArguments in LaunchAgent or LaunchDaemon plists. Known malware families including XCSSET and Cuckoo Stealer abuse plist modification to persist across reboots and conceal malicious processes. The detection monitors process execution of common plist editing utilities with arguments targeting sensitive keys and system persistence paths.

What is T1647 Plist File Modification?

Plist File Modification (T1647) maps to the Defense Evasion tactic — the adversary is trying to avoid being detected in MITRE ATT&CK.

This page provides production-ready detection logic for Plist File Modification, covering the data sources and telemetry it touches: Microsoft Defender for Endpoint. The queries below are rated high severity at medium confidence, and ship for 7 SIEM platforms — KQL, SPL, Elastic, QRadar, Sumo, YARA-L, LogScale.

MITRE ATT&CK

Tactic
Defense Evasion
Technique
T1647 Plist File Modification
Canonical reference
https://attack.mitre.org/techniques/T1647/
Microsoft Sentinel / Defender
kusto
let SuspiciousPlistKeys = dynamic(["LSUIElement", "LSEnvironment", "RunAtLoad", "ProgramArguments", "StartCalendarInterval", "KeepAlive", "DFBundleDisplayName", "CFBundleIdentifier", "LSBackgroundOnly"]);
let PersistencePaths = dynamic(["LaunchAgents", "LaunchDaemons", "com.apple.dock", "com.apple.loginwindow", "com.apple.loginitems"]);
let PlistEditors = dynamic(["plutil", "PlistBuddy", "defaults"]);
let SuspiciousParents = dynamic(["bash", "zsh", "sh", "python3", "perl", "ruby", "osascript", "curl", "wget"]);
// Primary: direct plist editor execution with suspicious arguments
let DirectEdits = DeviceProcessEvents
| where Timestamp > ago(24h)
| where FileName in~ (PlistEditors)
| where ProcessCommandLine has ".plist"
| where ProcessCommandLine has_any ("-insert", "-replace", "-set", "-remove", "write", "-convert", "-extract", "add", "delete")
| extend MatchedKey = case(
    ProcessCommandLine has_any (SuspiciousPlistKeys), "SuspiciousPlistKey",
    ProcessCommandLine has_any (PersistencePaths), "PersistencePath",
    true, "GenericPlistEdit"
)
| extend DetectionType = "DirectPlistEdit";
// Secondary: scripting language modifying plist files directly
let ScriptEdits = DeviceProcessEvents
| where Timestamp > ago(24h)
| where FileName in~ ("python3", "python", "perl", "ruby", "osascript", "node")
| where ProcessCommandLine has ".plist"
| where ProcessCommandLine has_any ("writePlist", "plistlib", "NSUserDefaults", "CFPreferences", "PropertyList", "plist.write")
| extend MatchedKey = "ScriptingLanguagePlistWrite"
| extend DetectionType = "ScriptPlistEdit";
// Combine and enrich
DirectEdits
| union ScriptEdits
| extend SuspiciousParentContext = InitiatingProcessFileName in~ (SuspiciousParents)
| extend SuspiciousPathContext = FolderPath has_any ("tmp", ".hidden", "Downloads", "Library/Application Support")
| extend RiskScore = case(
    MatchedKey == "SuspiciousPlistKey" and SuspiciousParentContext, 90,
    MatchedKey == "PersistencePath" and SuspiciousParentContext, 80,
    MatchedKey == "SuspiciousPlistKey", 70,
    MatchedKey == "PersistencePath", 65,
    SuspiciousParentContext, 50,
    40
)
| where RiskScore >= 50
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine, MatchedKey, DetectionType, RiskScore, FolderPath
| order by RiskScore desc, Timestamp desc

Detects execution of macOS plist editing utilities (plutil, PlistBuddy, defaults) and scripting language invocations that write to plist files. The query prioritizes modifications targeting high-risk keys (LSUIElement, LSEnvironment, RunAtLoad, ProgramArguments) and persistence paths (LaunchAgents, LaunchDaemons) while scoring alerts based on parent process context and invocation patterns consistent with malware behavior.

high severity medium confidence

Data Sources

Microsoft Defender for Endpoint

Required Tables

DeviceProcessEvents

False Positives

  • Legitimate macOS application installers using plutil or PlistBuddy to configure app preferences during setup
  • System administrators using the defaults command to manage enterprise preferences and MDM profiles
  • Developer tooling such as Xcode build scripts or CocoaPods that modify Info.plist during compilation
  • Homebrew package manager modifying application plist files during install or upgrade operations
  • IT management tools (Jamf, Munki, Chef) that programmatically write LaunchAgent plists for legitimate automation

Sigma rule & cross-platform mapping

The detection logic for Plist File Modification (T1647) above is provided in a vendor-neutral form so you can deploy it on any SIEM. The same logic is shipped here as native KQL (Microsoft Sentinel / Defender), SPL (Splunk), Elastic (Elastic Security (EQL)), QRadar (IBM QRadar (AQL)), Sumo (Sumo Logic CSE), YARA-L (Google Chronicle / SecOps), LogScale (CrowdStrike LogScale (CQL)) queries. In Sigma terms, this detection targets the following logsource:

logsource:
  category: process_creation
  product: windows

Browse the community-maintained Sigma rules for this technique:


Testing Methodology

Validate this detection against 3 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.

  1. Test 1Modify LSUIElement to hide macOS application via plutil

    Expected signal: DeviceProcessEvents: plutil process with -insert LSUIElement argument targeting a .plist file path; InitiatingProcessFileName will be the shell (bash/zsh)

  2. Test 2Write malicious LaunchAgent plist for persistence via PlistBuddy

    Expected signal: DeviceProcessEvents: PlistBuddy with multiple Add command invocations targeting ~/Library/LaunchAgents/; DeviceFileEvents: .plist file creation in LaunchAgents directory

  3. Test 3Inject LSEnvironment with DYLD_INSERT_LIBRARIES for dynamic linker hijacking setup

    Expected signal: DeviceProcessEvents: PlistBuddy with 'Add :LSEnvironment dict' and 'Add :LSEnvironment:DYLD_INSERT_LIBRARIES' command arguments; high-fidelity process args containing both LSEnvironment and DYLD_INSERT_LIBRARIES strings


Response Playbook

Triage

  1. Step 1: Identify the exact plist file path modified — check ProcessCommandLine for the full path argument passed to plutil, PlistBuddy, or defaults. Determine if it targets a LaunchAgent/LaunchDaemon path (~Library/LaunchAgents, /Library/LaunchAgents, /Library/LaunchDaemons) or application Info.plist.
  2. Step 2: Inspect the specific key-value pair being written. If the key is LSUIElement=1 or LSBackgroundOnly=1, this hides the application from the user. If LSEnvironment contains DYLD_INSERT_LIBRARIES, this indicates dynamic linker hijacking (T1574.006). If RunAtLoad=true with ProgramArguments pointing to a non-standard binary, this is persistence.
  3. Step 3: Examine the parent process chain. A legitimate installer has a parent of Installer.app or a known package manager. A suspicious chain includes Terminal.app → bash → plutil, or any network-fetching process (curl, wget) as an ancestor.
  4. Step 4: Check whether the binary referenced in ProgramArguments or the modified app bundle exists and is code-signed. Run: spctl --assess --verbose /path/to/binary and codesign -dv /path/to/binary. Unsigned or ad-hoc signed binaries are high-fidelity indicators.
  5. Step 5: Review the timeline around the plist modification. Look for file download events (DeviceFileEvents with file extensions .zip, .dmg, .pkg) or network connections in the 60 minutes preceding the plist edit to identify the initial access vector.
  6. Step 6: Check if a launchd job was loaded following the plist write by looking for subsequent launchctl load or launchctl bootstrap commands within the same session context in DeviceProcessEvents.

Containment

  1. Isolate the affected macOS endpoint via MDM (Jamf: sudo jamf policy -event isolate) or Defender for Endpoint network isolation to prevent potential C2 communication if LSEnvironment-based hijacking is confirmed.
  2. Disable the malicious LaunchAgent or LaunchDaemon immediately: launchctl unload ~/Library/LaunchAgents/<malicious.plist> or launchctl bootout system /Library/LaunchDaemons/<malicious.plist>. Remove the plist file after unloading.
  3. Revert unauthorized LSUIElement or LSEnvironment modifications by restoring the original Info.plist from a known-good source or using plutil to remove the injected key: PlistBuddy -c 'Delete :LSUIElement' /path/to/Info.plist.
  4. If the dock plist (com.apple.dock.plist) was modified to add a malicious app path, remove the entry and restart Dock: killall Dock.
  5. Block the associated process hash in your EDR platform to prevent re-execution of the binary referenced in ProgramArguments or the modified app bundle.

Evidence Collection

  1. Collect all plist files in LaunchAgent and LaunchDaemon directories: ~/Library/LaunchAgents/, /Library/LaunchAgents/, /Library/LaunchDaemons/, /System/Library/LaunchAgents/. Use find ~/Library/LaunchAgents -name '*.plist' -newer /var/log/system.log to identify recently created or modified entries.
  2. Capture the full contents of any suspicious plist with plutil -p <file>.plist to decode binary plist format and extract all key-value pairs for forensic documentation.
  3. Collect macOS Unified System Log entries covering the activity window: log collect --last 2h --output /tmp/system_logs.logarchive. Parse with log show --archive /tmp/system_logs.logarchive for process execution and launchd events.
  4. Export process execution history from the macOS audit log: sudo praudit /var/audit/current | grep -E 'plutil|PlistBuddy|defaults' and capture the full audit trail.
  5. Preserve memory of the suspicious process if still running using a memory acquisition tool to capture potential in-memory indicators of injected payloads via LSEnvironment/DYLD manipulation.
  6. Document the code signing status of all binaries referenced in suspect plist ProgramArguments: codesign -dv --verbose=4 /path/to/binary > /tmp/codesign_evidence.txt 2>&1.

Escalation Criteria

  • ! Escalate immediately if LSEnvironment key contains DYLD_INSERT_LIBRARIES pointing to a non-Apple, unsigned dylib — this confirms dynamic linker hijacking (T1574.006) and active code injection capability.
  • ! Escalate if the plist modification was performed by a process that arrived via a network download (curl/wget parent, or file downloaded in same user session) — indicates a dropper-stage malware payload.
  • ! Escalate if multiple endpoints show identical plist modifications within a short window, suggesting automated lateral movement or a supply chain compromise distributing a malicious app bundle.
  • ! Escalate if the modified LaunchDaemon runs as root (UserName key absent or set to root) — root-level persistence is a critical severity finding requiring immediate IR team engagement.
  • ! Escalate if the binary referenced in the modified plist communicates with external IPs or domains not associated with legitimate software vendors — confirmed C2 channel via persistence mechanism.

Investigation Guide

Forensic Artifacts

  • > LaunchAgent plist files: ~/Library/LaunchAgents/*.plist, /Library/LaunchAgents/*.plist
  • > LaunchDaemon plist files: /Library/LaunchDaemons/*.plist, /System/Library/LaunchDaemons/*.plist
  • > Application Info.plist: /Applications/<AppName>.app/Contents/Info.plist
  • > macOS Dock preferences: ~/Library/Preferences/com.apple.dock.plist
  • > Login items plist: ~/Library/Preferences/com.apple.loginwindow.plist
  • > macOS Unified System Log archive: /var/log/system.log and /private/var/log/asl/
  • > macOS audit log: /var/audit/current and rotated /var/audit/YYYYMMDDTHHmmss
  • > Launch Services database: ~/Library/Caches/com.apple.LaunchServices/
  • > plutil binary at /usr/bin/plutil — check modification timestamp for tampering
  • > Process accounting data via macOS BSM audit trail
  • > Extended attributes on plist files: xattr -l <file>.plist — quarantine flag absence on downloaded files is suspicious

Tuning Guidance

The highest-noise source will be legitimate application installers (PKG, DMG-based apps) and MDM enrollment workflows. To reduce false positives: (1) Create an allowlist of known-good installer processes (Installer.app, install, pkgd) and MDM agents (jamf, Munki, mdmclient, osinstaller) and suppress their plist writes. (2) Focus highest-priority alerting on modifications to LaunchDaemon paths (system-level, root persistence) over LaunchAgent paths (user-level). (3) For LSUIElement detections, only alert on modifications to third-party app bundles — suppress changes to Apple-signed bundles using code signing verification in your EDR. (4) Tune risk score threshold upward (from 50 to 65) in environments with active software packaging workflows. (5) Correlate with file reputation/hash intelligence — if the binary in ProgramArguments is known-good, suppress the alert. (6) Consider time-of-day baselining: legitimate installer activity clusters during business hours and Munki/Jamf check-in windows.


Hunting Queries

Hunts for plist files created or modified in persistence directories by processes that are not known-good installers or MDM agents — surfaces stealth persistence mechanisms not caught by process-based detections

Hunting — KQL
kql
// Hunt for plist files written to persistence paths by non-standard processes
DeviceFileEvents
| where Timestamp > ago(7d)
| where FileName endswith ".plist"
| where FolderPath has_any ("Library/LaunchAgents", "Library/LaunchDaemons", "Library/StartupItems")
| where InitiatingProcessFileName !in~ ("Installer", "install", "pkgd", "jamf", "munki", "osinstaller", "softwareupdated", "mdmclient")
| where ActionType in ("FileCreated", "FileModified")
| summarize PlistCount=count(), UniqueFiles=dcount(FileName), Files=make_set(FileName, 20) by DeviceName, InitiatingProcessFileName, InitiatingProcessAccountName, bin(Timestamp, 1h)
| where PlistCount > 0
| order by PlistCount desc
Hunting — SPL
spl
index=* (sourcetype="macos:unified_log" OR sourcetype="carbon_black:edr:json")
| where like(file_path, "%LaunchAgents%.plist") OR like(file_path, "%LaunchDaemons%.plist")
| where file_event_type IN ("CREATE", "WRITE", "MODIFY")
| where NOT process_name IN ("Installer", "jamf", "munki", "osinstaller", "softwareupdated", "mdmclient", "com.apple.MobileFileIntegrity")
| stats count as plist_writes, dc(file_path) as unique_plists, values(file_path) as plist_files by host, process_name, user
| where plist_writes > 0
| sort - plist_writes

Specifically hunts for modifications to plist keys associated with defense evasion — LSUIElement (hide app), LSBackgroundOnly (background-only mode), and LSEnvironment/DYLD_INSERT_LIBRARIES (dynamic linker hijacking), which are the highest-fidelity signals of malicious plist manipulation

Hunting — KQL
kql
// Hunt for LSUIElement or LSEnvironment keys in recently modified Info.plist files via process argument inspection
DeviceProcessEvents
| where Timestamp > ago(14d)
| where FileName in~ ("plutil", "PlistBuddy", "defaults")
| where ProcessCommandLine has_any ("LSUIElement", "LSBackgroundOnly", "LSEnvironment", "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
| extend ThreatSignal = case(
    ProcessCommandLine has "DYLD_INSERT_LIBRARIES", "CriticalDyldInjection",
    ProcessCommandLine has "LSEnvironment", "EnvironmentInjection",
    ProcessCommandLine has "LSUIElement" and ProcessCommandLine has "1", "UIHiding",
    ProcessCommandLine has "LSBackgroundOnly" and ProcessCommandLine has "1", "BackgroundHiding",
    "Other"
)
| order by Timestamp desc
Hunting — SPL
spl
index=* (sourcetype="crowdstrike:events:ProcessRollup2" OR sourcetype="carbon_black:edr:json" OR sourcetype="macos:unified_log")
| where process_name IN ("plutil", "PlistBuddy", "defaults")
| where match(process_cmdline, "LSUIElement|LSBackgroundOnly|LSEnvironment|DYLD_INSERT_LIBRARIES|DYLD_LIBRARY_PATH")
| eval threat_signal=case(
    match(process_cmdline, "DYLD_INSERT_LIBRARIES"), "critical_dyld_injection",
    match(process_cmdline, "LSEnvironment"), "environment_variable_injection",
    match(process_cmdline, "LSUIElement.*1|1.*LSUIElement"), "ui_element_hiding",
    match(process_cmdline, "LSBackgroundOnly.*1"), "background_only_hiding",
    1=1, "other"
)
| table _time, host, user, process_name, process_cmdline, parent_process_name, threat_signal
| sort - _time

Correlates LSUIElement plist modifications with subsequent execution of the modified application — identifies the operational use of UI-hiding after the modification is made, confirming malicious intent versus mere configuration change

Hunting — KQL
kql
// Hunt for processes with no UI that were recently granted persistence via LSUIElement plist modification
// by correlating plist modifications with subsequent process executions that have no associated window
let PlistModifiedApps = DeviceProcessEvents
| where Timestamp > ago(24h)
| where FileName in~ ("plutil", "PlistBuddy")
| where ProcessCommandLine has "LSUIElement" and ProcessCommandLine has "1"
| extend AppPath = extract(@"([/\w.]+\.app)", 1, ProcessCommandLine)
| where isnotempty(AppPath)
| project ModTimestamp=Timestamp, DeviceName, AppPath;
DeviceProcessEvents
| where Timestamp > ago(24h)
| join kind=inner PlistModifiedApps on DeviceName
| where Timestamp > ModTimestamp
| where FolderPath has AppPath
| where not(InitiatingProcessFileName in~ ("launchservicesd", "loginwindow", "Dock"))
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, AppPath, ModTimestamp
| order by Timestamp desc
Hunting — SPL
spl
index=* sourcetype="carbon_black:edr:json" earliest=-24h
| eval is_plist_mod=if(process_name IN ("plutil","PlistBuddy") AND match(process_cmdline,"LSUIElement.*1"), "yes", "no")
| eval app_path=if(is_plist_mod="yes", replace(process_cmdline, ".*?([\w./]+\\.app).*", "\1"), null())
| where is_plist_mod="yes" AND isnotnull(app_path)
| append [search index=* sourcetype="carbon_black:edr:json" earliest=-24h | where NOT process_name IN ("plutil","PlistBuddy") | rename process_path as exec_path]
| stats values(app_path) as modified_apps, values(exec_path) as launched_from by host
| eval overlap=mvfilter(match(launched_from, mvjoin(modified_apps, "|")))
| where isnotnull(overlap)
| table host, modified_apps, overlap

Atomic Red Team Tests

Test 1 Modify LSUIElement to hide macOS application via plutil
macos

Simulates an adversary using plutil to set LSUIElement=1 in an application's Info.plist, which causes the app to run without appearing in the Dock or Command-Tab switcher. This is used by malware like XCSSET to conceal malicious processes from the user.

Command

bash
# Create a test app bundle structure
mkdir -p /tmp/AtomicTest.app/Contents/MacOS
cat > /tmp/AtomicTest.app/Contents/Info.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict><key>CFBundleName</key><string>AtomicTest</string></dict></plist>
EOF
# Inject LSUIElement key to hide the app from the Dock
plutil -insert LSUIElement -bool YES /tmp/AtomicTest.app/Contents/Info.plist
# Verify the modification
plutil -p /tmp/AtomicTest.app/Contents/Info.plist | grep LSUIElement

Cleanup

bash
rm -rf /tmp/AtomicTest.app

Expected Telemetry

DeviceProcessEvents: plutil process with -insert LSUIElement argument targeting a .plist file path; InitiatingProcessFileName will be the shell (bash/zsh)

Expected Detection

Alert: Plist File Modification — LSUIElement key inserted via plutil with risk score 70, MatchedKey=SuspiciousPlistKey

Test 2 Write malicious LaunchAgent plist for persistence via PlistBuddy
macos

Simulates a malware dropper using PlistBuddy to create a LaunchAgent plist with RunAtLoad=true and a malicious ProgramArguments path. This technique is used by Cuckoo Stealer and other macOS malware families to achieve user-level persistence via launchd.

Command

bash
# Create a persistence LaunchAgent plist using PlistBuddy
PLIST_PATH="$HOME/Library/LaunchAgents/com.atomictest.persist.plist"
/usr/libexec/PlistBuddy -c "Add :Label string com.atomictest.persist" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments array" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments:0 string /bin/sh" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments:1 string -c" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments:2 string 'touch /tmp/atomic_persistence_triggered'" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :RunAtLoad bool true" "$PLIST_PATH"
# Verify the created plist
plutil -p "$PLIST_PATH"

Cleanup

bash
launchctl unload $HOME/Library/LaunchAgents/com.atomictest.persist.plist 2>/dev/null; rm -f $HOME/Library/LaunchAgents/com.atomictest.persist.plist; rm -f /tmp/atomic_persistence_triggered

Expected Telemetry

DeviceProcessEvents: PlistBuddy with multiple Add command invocations targeting ~/Library/LaunchAgents/; DeviceFileEvents: .plist file creation in LaunchAgents directory

Expected Detection

Alert: Plist File Modification — PersistencePath match for LaunchAgents with RunAtLoad key insertion, risk score 80 with shell parent context

Test 3 Inject LSEnvironment with DYLD_INSERT_LIBRARIES for dynamic linker hijacking setup
macos

Simulates an adversary injecting the LSEnvironment dictionary with DYLD_INSERT_LIBRARIES into an application's Info.plist to enable dynamic linker hijacking (T1574.006). When the target app launches, macOS will load the attacker-controlled dylib before the app's own libraries. This technique was used by the XCSSET malware.

Command

bash
# Create a test app bundle with a valid Info.plist
mkdir -p /tmp/HijackTarget.app/Contents/MacOS
cat > /tmp/HijackTarget.app/Contents/Info.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict><key>CFBundleName</key><string>HijackTarget</string><key>CFBundleExecutable</key><string>HijackTarget</string></dict></plist>
EOF
# Inject LSEnvironment dictionary with DYLD_INSERT_LIBRARIES path
/usr/libexec/PlistBuddy -c "Add :LSEnvironment dict" /tmp/HijackTarget.app/Contents/Info.plist
/usr/libexec/PlistBuddy -c "Add :LSEnvironment:DYLD_INSERT_LIBRARIES string /tmp/malicious.dylib" /tmp/HijackTarget.app/Contents/Info.plist
# Verify injection
plutil -p /tmp/HijackTarget.app/Contents/Info.plist | grep -A2 LSEnvironment

Cleanup

bash
rm -rf /tmp/HijackTarget.app

Expected Telemetry

DeviceProcessEvents: PlistBuddy with 'Add :LSEnvironment dict' and 'Add :LSEnvironment:DYLD_INSERT_LIBRARIES' command arguments; high-fidelity process args containing both LSEnvironment and DYLD_INSERT_LIBRARIES strings

Expected Detection

Alert: Plist File Modification — ThreatSignal=CriticalDyldInjection or EnvironmentInjection, maximum risk score 90, requires immediate triage as potential dynamic linker hijacking setup

Related Detections