WireLurker Detector — Admin Guide

Generated May 2026 from PaloAltoNetworks/WireLurkerDetector at 0957460

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Unofficial, AI-generated guide for a Palo Alto Networks open-source repository; not reviewed by Palo Alto Networks.

Audience: security engineers, incident responders, reverse engineers, and developers who need to integrate, fork, or build on top of the detector. This guide covers architecture, detection heuristics, operational integration, limitations, and the source-level internals of both scripts.


1. Executive Summary

WireLurker Detector is a two-file Python 2 toolkit published by Palo Alto Networks in November 2014 as part of the disclosure of the WireLurker malware family — the first campaign observed targeting non-jailbroken iOS devices via USB pairing from infected Macs.

Attribute Value
Repository https://github.com/PaloAltoNetworks/WireLurkerDetector
Pinned commit 0957460 (2022-08-15 — cosmetic / metadata change only)
Codebase size 2 Python files, ~6.1 KB total
Language Python 2 (uses print statement syntax)
Runtime OS macOS (Darwin) — enforced at runtime
External deps pyDes (decryptor only); stdlib otherwise
License ISC-style, permissive
Maturity Historical: published with the 2014 disclosure; later commits are metadata changes.
Threat model Signature-based detection of the exact 2014 WireLurker IOCs on disk.

Operational role. The detector is a read-only scanner. It enumerates known-bad paths, walks /Applications and ~/Applications for .app bundles, applies three file-level heuristics, and prints findings. It performs no network I/O, no quarantine, no remediation. decryptor.py is a research helper that decrypts captured C&C payloads specific to WireLurker variant C.

Why it matters today. It is not a general antivirus and should not be deployed as one. It is still useful as (a) a forensic probe on legacy macOS systems during incident response, (b) a reference implementation of 2014-era macOS malware detection heuristics, and (c) a teaching artifact for reverse-engineering and IR coursework.


2. Architecture

2.1 High-level shape

┌──────────────────────────────────────────────────────────┐
│                   WireLurker Detector                    │
│                                                          │
│  ┌────────────────────────────┐  ┌────────────────────┐  │
│  │  WireLurkerDetectorOSX.py  │  │   decryptor.py     │  │
│  │  (scanner — runs on macOS) │  │  (research helper) │  │
│  │                            │  │                    │  │
│  │  main()                    │  │  main()            │  │
│  │   ├─ scan_files(MAL)       │  │   └─ pyDes DES-ECB │  │
│  │   ├─ scan_files(SUS)       │  │      + base64      │  │
│  │   └─ scan_app()            │  │                    │  │
│  │        └─ is_app_infected()│  │                    │  │
│  │             └─ is_file_    │  │                    │  │
│  │                hidden()    │  │                    │  │
│  └────────────────────────────┘  └────────────────────┘  │
│            │                              │              │
│            ▼                              ▼              │
│   Local filesystem (read only)   stdin base64 payload    │
└──────────────────────────────────────────────────────────┘

The two scripts are completely independent. They share no code and are invoked separately.

2.2 Data-flow — WireLurkerDetectorOSX.py

 argv (none)
   │
   ▼
 platform.system() == "Darwin" ?  ── no ──► stderr, exit -1
   │ yes
   ▼
 scan_files(MALICIOUS_FILES)   ── found ──► print "[!] Found malicious file: …"
   │
   ▼
 scan_files(SUSPICIOUS_FILES)  ── found ──► print "[!] Found suspicious file: …"
   │
   ▼
 scan_app()
   │
   ├── os.walk("/Applications")
   ├── os.walk(expanduser("~/Applications"))
   │    │
   │    ▼
   │    for each root ending in ".app":
   │        is_app_infected(root)
   │            ├── plistlib.readPlist(Info.plist)
   │            ├── heuristic 1: hidden executable swap
   │            ├── heuristic 2: hidden trojan loader
   │            └── heuristic 3: embedded installer
   │
   ▼
 any findings ?  ── yes ──► print WARNING, exit 1
   │ no
   ▼
 print clean, exit 0

All three passes are linear and synchronous. There is no threading, no caching, no persistence. A single invocation reads the filesystem, prints results, and returns.

2.3 Data-flow — decryptor.py

 argv[1] = captured base64 message
   │
   ▼
 session_key = str(sum(int(c) for c in argv[1][:10]))
   │
   ▼
 key = session_key + "dksyel"
   │
   ▼
 ciphertext = base64.b64decode(argv[1][10:])
   │
   ▼
 pyDes.des(key, ECB, PAD_PKCS5).decrypt(ciphertext)
   │
   ▼
 print plaintext

The key derivation is deterministic given the message prefix, which means anyone who captures WireLurker-C traffic can recover plaintext without a separate key exchange — a deliberate weakness the Palo Alto Networks researchers exposed.

2.4 Component inventory

Component File Role
main() (detector) WireLurkerDetectorOSX.py:119 Orchestrates the three-pass scan and prints results.
scan_files(paths) WireLurkerDetectorOSX.py:52 Returns paths from the input list that exist on disk.
is_file_hidden(f) WireLurkerDetectorOSX.py:62 True if the file has the BSD UF_HIDDEN flag; falls back to parsing ls -ldO.
is_app_infected(root) WireLurkerDetectorOSX.py:82 Applies the three .app-bundle infection heuristics.
scan_app() WireLurkerDetectorOSX.py:107 Walks /Applications and ~/Applications, calls is_app_infected on every .app bundle.
MALICIOUS_FILES WireLurkerDetectorOSX.py:23 18 hardcoded absolute paths.
SUSPICIOUS_FILES WireLurkerDetectorOSX.py:44 4 hardcoded paths (2 absolute, 2 user-home-relative).
main() (decryptor) decryptor.py:187 Parses one argv, derives the key, decrypts, prints.

3. System Requirements

3.1 Minimums

Dimension Requirement
OS macOS / OS X. The detector hard-exits on anything else.
Architecture Any — Intel, Apple Silicon. No native code.
Python 2.7+. 2.6 may work but is untested. Python 3 does not work (parse-time SyntaxError).
Disk Negligible (~6 KB for the scripts). Log files grow with output size; typical clean scan is a few KB.
RAM Negligible. The scan holds one list of findings in memory.
Filesystem APFS and HFS+. Both expose UF_HIDDEN via os.stat(f).st_flags.
Network None for the detector. The decryptor is offline.

3.2 Optional

Item When needed
pyDes Python 2 package Only to run decryptor.py.
sudo Not required, but recommended so that the scan can read root-owned LaunchDaemons and SIP-restricted paths.
bash + coreutils Only for the ls -ldO fallback of is_file_hidden() on platforms without stat.UF_HIDDEN. macOS always has both.

3.3 Anti-requirements (things that block use)

  • macOS 12.3 or later with no manually installed Python 2 — the interpreter was removed.
  • Linux or Windows — the detector refuses to run.
  • Python 3 — fails at import/parse time on the print statements.

4. Configuration Reference

There is no configuration. The detector has:

  • No command-line arguments.
  • No environment variables.
  • No config file.
  • No runtime flags.

Every behavior is a constant in source:

Constant Location Meaning
MALICIOUS_FILES WireLurkerDetectorOSX.py:23 Paths whose presence triggers a malicious finding.
SUSPICIOUS_FILES WireLurkerDetectorOSX.py:44 Paths whose presence triggers a suspicious finding.
__version__ WireLurkerDetectorOSX.py:11 '1.2.0' — displayed in the banner.
Heuristic filenames is_app_infected in WireLurkerDetectorOSX.py:82 start.sh, FontMap1.cfg, appinstaller, infoplistab, CFBundleExecutable.

Customization requires a source fork.

Tip

To extend the detector for an internal variant with additional IOCs, append to MALICIOUS_FILES / SUSPICIOUS_FILES. Keep paths absolute; user-relative paths must be wrapped in os.path.join(expanduser('~'), ...) for scan_files() to resolve them.


5. Deployment

5.1 Deployment model

The detector is a single-shot CLI scanner. There is no daemon, no scheduled service, no agent. Typical deployment options:

  1. Ad-hoc incident response. Copy the .py to the target Mac, run once, read output, delete.
  2. Fleet scan via MDM. Use Jamf / Kandji / Mosyle to push the script, execute it, and collect stdout.
  3. SSH sweep. Loop over hosts and run the script remotely; capture output server-side.

5.2 Fleet scan (pattern)

for host in $(cat macs.txt); do
    ssh -n "admin@$host" \
        '/usr/bin/python2 - < /path/to/WireLurkerDetectorOSX.py' \
        > "scans/$host.log" 2>&1
done

Note

Piping the script into python2 - avoids leaving the file on disk. Some endpoint-protection suites will still capture the command line; coordinate with your SOC before running.

5.3 Container / sandbox note

Running inside a macOS container or VM is fine. Running inside a Linux container is not — platform.system() returns Linux and the script exits with code -1. There is no escape hatch.

5.4 Distribution integrity

When packaging for internal distribution, pin a commit hash and store the SHA-256 of the script alongside it:

git rev-parse --short HEAD
# 0957460

shasum -a 256 WireLurkerDetectorOSX.py > WireLurkerDetectorOSX.py.sha256
a0c… WireLurkerDetectorOSX.py

Verify on every host before executing.

5.5 Deployment checklist

  • [ ] Target host runs macOS (Darwin).
  • [ ] python2 is installed and python2 --version prints 2.7.x.
  • [ ] Script is pinned to a known commit and SHA-256 verified.
  • [ ] Scan runs under sudo if root-only paths are in-scope.
  • [ ] Output is captured to a file for audit trail.
  • [ ] AV allowlist entry is in place (the IOC strings trigger false positives in some scanners).

6. Security Analysis

6.1 What the script does with privilege

Action Privilege needed Notes
os.path.exists(path) for each IOC Depends on path On root-only paths (/Library/LaunchDaemons/*) plain user sees them if directory-read is allowed, but some plists require root.
os.walk('/Applications') User-readable Always works.
os.walk('~/Applications') User-readable Always works, missing dir handled silently.
plistlib.readPlist(Info.plist) User-readable Info.plist is world-readable in standard bundles.
os.stat(f).st_flags User-readable File flags do not require extra privilege.
subprocess.Popen("ls -ldO …") fallback User-readable Only runs when UF_HIDDEN is unavailable.

The script does not write to disk, open sockets, modify permissions, or execute anything in the inspected bundles. Its worst-case side effect is CPU/IO load from the .app walk.

6.2 Robustness of the script itself

Vector Exposure Mitigation
Shell fallback in is_file_hidden() When stat.UF_HIDDEN is unavailable, the function falls back to ls -ldO run through a shell. On current macOS that branch is not reached. A fork for older systems can use a list-argv subprocess.Popen with shell=False (see section 10).
Unpickling / eval None — plistlib.readPlist parses XML plists, not pickle. Nothing to do.
Denial of service A symlink loop under /Applications could cause os.walk to spin. os.walk(followlinks=False) is the default, which blocks the attack.
AV false positive IOC strings trigger signature scanners. Allowlist by hash.

6.3 What the decryptor reveals

decryptor.py is a disclosure of WireLurker-C's session-key derivation algorithm. Publishing it showed how weak the malware's C&C encryption was. There is no ongoing operational risk in 2025 because the C&C infrastructure has long been defunct.

6.4 Secrets handling

None. The detector handles no secrets. The decryptor's "key" is derived from the ciphertext itself and is not a secret.

6.5 Network exposure

Zero. Neither script opens sockets.


7. Operations

7.1 Running the detector under launchd as a periodic job

If you want to run the detector on a schedule on a single macOS host, wrap it in a launchd plist:

<?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>Label</key>
    <string>com.example.wirelurker-scan</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/python2</string>
        <string>/opt/wirelurker/WireLurkerDetectorOSX.py</string>
    </array>
    <key>StandardOutPath</key>
    <string>/var/log/wirelurker-scan.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/wirelurker-scan.err</string>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key><integer>3</integer>
        <key>Minute</key><integer>0</integer>
    </dict>
</dict>
</plist>

Save as /Library/LaunchDaemons/com.example.wirelurker-scan.plist, then:

sudo launchctl load -w /Library/LaunchDaemons/com.example.wirelurker-scan.plist

7.2 Log management

The detector writes only to stdout/stderr. A sensible rotation policy for /var/log/wirelurker-scan.log uses newsyslog:

# /etc/newsyslog.d/wirelurker.conf
/var/log/wirelurker-scan.log  640  14  1024  *  JN

7.3 Parsing the output

Output is human-readable text. A minimal parser:

import subprocess, re

proc = subprocess.Popen(['python2', 'WireLurkerDetectorOSX.py'],
                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, _ = proc.communicate()

malicious = re.findall(r'\[!\] Found malicious file: (.+)', out)
suspicious = re.findall(r'\[!\] Found suspicious file: (.+)', out)
infected_apps = re.findall(r'\[!\] Found infected application: (.+)', out)

if proc.returncode == 0:
    verdict = 'clean'
elif proc.returncode == 1:
    verdict = 'infected'
else:
    verdict = 'error'

There is no JSON / structured output mode.

7.4 Integration with SIEM

Because the script returns an exit code and writes plain text, any SIEM agent that can collect exit codes and logs can ingest it. Typical patterns:

  • Splunk UF — add a scripted input pointing at python2 /opt/wirelurker/WireLurkerDetectorOSX.py, schedule daily.
  • Elastic Agent / Filebeat — tail /var/log/wirelurker-scan.log, grok-parse the [!] lines.
  • osquery — wrap the scan in a shell script and expose via --extension, returning findings as rows.

7.5 Alert-worthy lines

Treat any line matching these patterns as a P1 alert:

^\[\!\] Found malicious file: .+
^\[\!\] Found infected application: .+

Lines matching Found suspicious file: deserve investigation but are not conclusive — the maiyadi cache directory is the most common false positive.

7.6 Backup / restore

Nothing to back up — the tool holds no state. A Time Machine restore of a previously infected system can reintroduce the WireLurker artifacts (see community issue #18). Run the detector immediately after any restore from legacy backups.


8. Internals — Source-Level Reference

8.1 scan_files(paths) (WireLurkerDetectorOSX.py:52)

def scan_files(paths):
    results = []
    for f in paths:
        if os.path.exists(f):
            results.append(f)
    return results

Linear, synchronous, no caching. os.path.exists follows symlinks; if a listed IOC path is a dangling symlink it will return False and miss. In practice WireLurker drops real files, not symlinks.

8.2 is_file_hidden(f) (WireLurkerDetectorOSX.py:62)

Tries os.stat(f).st_flags & stat.UF_HIDDEN when available. On platforms that lack UF_HIDDEN, it falls back to:

proc = subprocess.Popen("ls -ldO '%s' | awk '{print $5}'" % f,
                        shell=True, stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT)
output = proc.stdout.read()
return output.find('hidden') != -1

Notes:

  • When UF_HIDDEN is unavailable, the fallback builds a shell command from the path; a fork can switch to a list-argv call (see §6.2).
  • The fallback column index ($5) is correct for macOS BSD ls -ldO output; on other Unixes the column differs or the flag isn't printed.
  • proc.communicate() is called after proc.stdout.read(), which on Python 2 is safe but unusual — the canonical form would be proc.communicate()[0].

8.3 is_app_infected(root) (WireLurkerDetectorOSX.py:82)

Three independent tests, OR-combined:

Test Condition
1 UF_HIDDEN is set on both Contents/MacOS/<CFBundleExecutable> AND Contents/MacOS/<CFBundleExecutable>_
2 UF_HIDDEN is set on both Contents/Resources/start.sh AND Contents/Resources/FontMap1.cfg
3 Contents/MacOS/appinstaller AND Contents/Resources/infoplistab both exist as files

Any exception (plist parse, KeyError on CFBundleExecutable, OSError on stat) → returns False. This is deliberate: unparseable bundles are silently treated as clean to avoid flooding the report.

8.4 scan_app() (WireLurkerDetectorOSX.py:107)

for target in ['/Applications', expanduser('~/Applications')]:
    for root, __, __ in os.walk(target):
        if root.lower().endswith('.app'):
            if is_app_infected(root):
                infected_apps.append(root)

Case-insensitive suffix match on .app. os.walk emits each directory once, so a bundle at any depth is visited exactly once. Nested .app bundles (rare, e.g. plugins inside a top-level app) are all checked independently.

8.5 main() banner and exit logic (WireLurkerDetectorOSX.py:119)

print 'WireLurker Detector (version %s)' % __version__
print __copyright__

Uses Python-2 print statements throughout — the root cause of every Python-3 compatibility bug in the tracker. The function returns 0 when all three finding lists are empty and 1 otherwise; sys.exit(-1) is used on non-Darwin.

8.6 decryptor.py:main() (decryptor.py:187)

Key takeaways:

Line What Implication
decryptor.py:194 sum([int(c) for c in original_data[:10]]) First 10 chars must all be decimal digits; any non-digit raises ValueError.
decryptor.py:195 key = session_key + 'dksyel' Suffix is a constant. The literal string dksyel is the static half of the key.
decryptor.py:199 pyDes.des(key, pyDes.ECB, padmode=pyDes.PAD_PKCS5) DES-ECB with PKCS#5 padding. Key length is typically 7–9 bytes (digits of the sum plus dksyel); pyDes truncates/pads internally.
decryptor.py:201 print plaintext Python-2 print statement again — fails under Python 3.

The algorithm is a toy cipher by modern standards: the entire key is recoverable from the prefix of the ciphertext, so "encryption" here is really obfuscation.

8.7 Control-flow summary

WireLurkerDetectorOSX.py  :  ~165 lines, 0 classes, 4 functions
decryptor.py              :  ~40 lines,  0 classes, 1 function

No classes. No decorators. No generators. No third-party imports in the detector. One third-party import (pyDes) in the decryptor.

8.8 Cyclomatic complexity (qualitative)

All functions are straight-line or shallowly-branched. The highest-complexity function is is_app_infected, which nests a try/except over three sequential if blocks — still trivial.


9. Code Quality and Patterns

Aspect Observation
Style PEP-8-ish. Spaces for indentation (4 spaces). Minimal docstrings (one module-level per file).
Error handling try/except Exception: return False in is_app_infected and is_file_hidden. A best-effort pattern: a bundle that raises an error is reported as not infected (see section 11).
Tests None. No tests/, no conftest.py, no CI configuration.
CI None — no .github/workflows/, no Travis/Circle config.
Packaging None — no setup.py, no pyproject.toml, not on PyPI.
Typing None — pre-PEP-484 Python 2 code.
Logging print only. No logging module use.
i18n English-only; WireLurker's original distribution vector was a Chinese app store, so some IOC paths (e.g. com.maiyadi.appinstaller) embed that locale.

Note

The code reads cleanly because the problem is small. Section 10 lists the upgrades a fork would make (argv flags, structured output, a test harness).


10. Security Hardening for Forks

If you are forking for internal use, these are the high-value upgrades:

  1. Port to Python 3. Replace every print x with print(x); replace plistlib.readPlist with plistlib.load(open(p, 'rb')); decode subprocess output with .decode('utf-8'). This also fixes every open SyntaxError bug in the upstream tracker.
  2. Remove the shell fallback. On any Python 2.6+ build, stat.UF_HIDDEN exists. Delete the ls -ldO branch entirely; if you want a fallback, use a list-argv subprocess.Popen with shell=False.
  3. Add a --json output flag. Structured output makes SIEM ingestion trivial.
  4. Add a --paths-file option. Externalize MALICIOUS_FILES / SUSPICIOUS_FILES so your SOC can update IOCs without editing source.
  5. Emit exit code 2 for "ran but could not read some paths". The current code conflates "clean" with "clean but privilege-denied on half the LaunchDaemons".
  6. Drop the decryptor. It's historical; ship it in a separate legacy/ package if at all.
  7. Re-audit the IOC list. Before running on a fleet, confirm none of the paths (/etc/manpath.d/, /usr/local/ipcc/) are legitimate on the macOS versions you manage.

11. Troubleshooting

Error: SyntaxError: Missing parentheses in call to 'print'

File "WireLurkerDetectorOSX.py", line 120
    print 'WireLurker Detector (version %s)' % __version__

Cause. Running with Python 3 (issues #17, #23, #25). Solution. Use python2. If unavailable, run the community Python-3 port in a fork, or translate the five print statements and the plistlib.readPlist call by hand. Prevention. Put a #!/usr/bin/env python2 shebang check in your deployment script.

Error: ERROR: The script should only be run in a Mac OS X system.

Cause. platform.system() did not return Darwin. Solution. Run on real macOS. No override flag exists. Prevention. Gate invocations on uname -s.

Error: ImportError: No module named pyDes

Cause. Missing pyDes in the Python 2 interpreter being used for decryptor.py. Solution. pip2 install pyDes. Prevention. Pre-seed the interpreter environment with a frozen requirements.txt containing pyDes==2.0.1.

Error: Prisma Cloud IaC scan fails on this repo

Cause. Community issue #26 — a Prisma Cloud IaC scanner crashes when scanning the repo. The repo contains no IaC; this is a scanner-side bug, not a repo issue. Solution. Exclude the repo from IaC scanning. Open a support ticket with the scanner vendor. Prevention. Configure IaC scan include-paths explicitly.

Error: Avast flags WireLurkerDetectorOSX.py

Cause. Issue #19 — signature scanners fire on the embedded IOC strings. Solution. Allowlist by SHA-256. Prevention. Store the hash in your configuration management system.

Error: curl: (60) SSL certificate problem

Cause. Issue #24 — outdated CA bundle on vintage macOS. Solution. Update curl and system roots, or side-load the script from a modern machine. Prevention. Prepare signed, hash-verified media in advance.

Error: script hangs during the .app scan phase

Cause. Many .app bundles on the target host; os.walk is I/O-bound. Solution. Wait. Use sudo fs_usage -f filesys python2 to confirm progress. Prevention. Run scans during maintenance windows.

Error: plistlib.InvalidFileException in one bundle

Cause. A malformed Info.plist. The exception is swallowed — the bundle is marked "not infected". Solution. Manually inspect with plutil -convert xml1 if you need certainty. Prevention. None at the tool level.

Error: Time Machine restore reintroduces WireLurker artifacts

Cause. Issue #18 — backups contain pre-cleanup state. Solution. Exclude known infection paths from the restore, then re-scan. Prevention. After confirmed cleanup, rotate or prune backups that contain infected snapshots.

Error: decryptor.py raises ValueError: invalid literal for int()

Cause. The first 10 characters of the supplied message are not all decimal digits. Solution. Verify the captured payload format — WireLurker-C prefixes with 10 decimal digits that seed the key. A non-conforming input is not a WireLurker-C message. Prevention. Capture complete, unmodified C&C payloads.

Error: exit code -1 with no output

Cause. The interpreter failed to load the script (syntax/imports) before the guard prints an error. Solution. Run python2 -m py_compile WireLurkerDetectorOSX.py to surface the underlying error. Prevention. CI-verify the script parses under the Python version you deploy.

Cause. An attacker with local write access planted a symlink loop under /Applications. Solution. os.walk(followlinks=False) is the default — verify you are not patching the argument. Prevention. Keep the default. Audit patches that change it.

Error: scan returns clean, but user-reported symptoms persist

Cause. The detector only sees 2014-era WireLurker IOCs. Modern macOS malware is not covered. Solution. Escalate to a full IR workflow using modern tools (KnockKnock, BlockBlock, EDR). Prevention. Do not use this tool as a sole endpoint defense.

Error: python2: command not found on macOS 12.3+

Cause. Apple removed system Python 2. Solution. Install from source (see python.org/ftp/python/2.7.18) or run in a VM with an older macOS. Prevention. Maintain a dedicated forensic VM image.


12. Integrations

12.1 Jamf / MDM script

Wrap the detector as a Jamf extension attribute returning <result>clean</result> or <result>infected</result>:

#!/bin/sh
OUTPUT=$(/usr/local/bin/python2 /opt/wirelurker/WireLurkerDetectorOSX.py 2>&1)
if echo "$OUTPUT" | grep -q 'Your OS X system isn'\''t infected'; then
    echo "<result>clean</result>"
else
    echo "<result>infected</result>"
fi

12.2 osquery external table

Expose findings as rows in an osquery extension (Python 2 + osquery-python). Each IOC path becomes a row; the infected column is a boolean.

12.3 Incident-response playbook snippet

Add a single step to your IR runbook for any legacy macOS host:

Step 4: Run WireLurker Detector (historical 2014 IOCs)
    host$ sudo python2 /opt/forensics/WireLurkerDetectorOSX.py \
              > /var/log/ir/$(hostname)-wirelurker.log
    Verify exit code. Capture log. Attach to ticket.

12.4 CI check for a fork

If you maintain a fork, a minimum CI step:

name: smoke
on: [push]
jobs:
  run:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install Python 2
        run: brew install python@2 || true
      - name: Compile check
        run: python2 -m py_compile WireLurkerDetectorOSX.py decryptor.py

macOS runners no longer ship Python 2; the fork should port to Python 3 before relying on CI.


13. Known Issues and Potential Improvements

13.1 Known issues (from the upstream tracker)

Issue Status Summary
#12 Open "This is NOT CROSS PLATFORM" — the OSX script only covers macOS. By design; see HOWTO-Windows.md for the Windows logic.
#17 Closed SyntaxError under Python 3 on line 75 (except Exception as e).
#18 Open Time Machine backups reintroduce WireLurker samples on restore.
#19 Closed Avast false positive on the script itself.
#20 Closed Typo in script output.
#23 Closed SyntaxError under Python > 2.7.
#24 Open curl SSL certificate error on old systems.
#25 Open SyntaxError invalid syntax (Python 3 root cause).
#26 Open Prisma Cloud IaC scan error (external scanner bug).
#27 Open Community health assessment — repo lacks CONTRIBUTING.md, issue templates, etc.

13.2 Potential improvements

  1. Python 3 migration. Single highest-impact change. Closes five bug reports.
  2. JSON / SARIF output. Makes automated ingestion trivial.
  3. Pluggable IOC files. External iocs.yaml read at runtime.
  4. Hash-based file matching. Current checks are path-based; adding SHA-256 or MD5 comparison against known WireLurker binary hashes would reduce false negatives from renamed artifacts.
  5. Parallel .app traversal. multiprocessing.Pool over the bundle list would cut scan time on fleets with many installed apps.
  6. Dry-run --cleanup flag. Print a proposed rm list for user review. Still requires human confirmation per the project's own HOWTO-Windows.md guidance.
  7. Unit tests. Start with a fixture directory emulating /Applications/Infected.app/Contents/... and assert on is_app_infected.

13.3 Known non-issues

  • The script does not check /System/Volumes/Data/... on sealed system volumes (macOS 10.15+). Those paths are canonicalized under the virtualized / root, so the existing absolute paths still resolve correctly via the firmlink layer.

14. Contributing

Since 2014 the repository has received metadata changes only. If you plan to contribute:

  1. Open an issue first describing what you plan to change.
  2. Match the existing code style (PEP-8, 4-space indent, minimal docstrings).
  3. Do not introduce new dependencies beyond pyDes.
  4. For any behavioral change to detection logic, attach a test fixture and a before/after output comparison.

The license is permissive; forks are common and legitimate.


15. Quick Reference

15.1 Files

File Role
WireLurkerDetectorOSX.py Main detector.
decryptor.py WireLurker-C C&C decryptor.
HOWTO-Windows.md Text spec for Windows detection.
README.md Upstream user-facing instructions.
LICENSE ISC-style license.
.gitignore Ignore rules (minimal).

15.2 Invocation

Script Command
Detector python2 WireLurkerDetectorOSX.py
Decryptor python2 decryptor.py '<base64 payload>'

15.3 Exit codes

Code Meaning
0 Clean
1 Indicators found
-1 Wrong OS, or interpreter/import failure

15.4 Key constants

Constant Location Count
MALICIOUS_FILES WireLurkerDetectorOSX.py:23 18 paths
SUSPICIOUS_FILES WireLurkerDetectorOSX.py:44 4 paths
Heuristic files is_app_infected start.sh, FontMap1.cfg, appinstaller, infoplistab

15.5 External references