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
printstatements.
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 inos.path.join(expanduser('~'), ...)forscan_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:
- Ad-hoc incident response. Copy the
.pyto the target Mac, run once, read output, delete. - Fleet scan via MDM. Use Jamf / Kandji / Mosyle to push the script, execute it, and collect stdout.
- 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).
- [ ]
python2is installed andpython2 --versionprints 2.7.x. - [ ] Script is pinned to a known commit and SHA-256 verified.
- [ ] Scan runs under
sudoif 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_HIDDENis 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 BSDls -ldOoutput; on other Unixes the column differs or the flag isn't printed. proc.communicate()is called afterproc.stdout.read(), which on Python 2 is safe but unusual — the canonical form would beproc.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:
- Port to Python 3. Replace every
print xwithprint(x); replaceplistlib.readPlistwithplistlib.load(open(p, 'rb')); decodesubprocessoutput with.decode('utf-8'). This also fixes every open SyntaxError bug in the upstream tracker. - Remove the shell fallback. On any Python 2.6+ build,
stat.UF_HIDDENexists. Delete thels -ldObranch entirely; if you want a fallback, use a list-argvsubprocess.Popenwithshell=False. - Add a
--jsonoutput flag. Structured output makes SIEM ingestion trivial. - Add a
--paths-fileoption. ExternalizeMALICIOUS_FILES/SUSPICIOUS_FILESso your SOC can update IOCs without editing source. - Emit exit code
2for "ran but could not read some paths". The current code conflates "clean" with "clean but privilege-denied on half the LaunchDaemons". - Drop the decryptor. It's historical; ship it in a separate
legacy/package if at all. - 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.
Error: os.walk follows a symlink loop
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
- Python 3 migration. Single highest-impact change. Closes five bug reports.
- JSON / SARIF output. Makes automated ingestion trivial.
- Pluggable IOC files. External
iocs.yamlread at runtime. - 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.
- Parallel
.apptraversal.multiprocessing.Poolover the bundle list would cut scan time on fleets with many installed apps. - Dry-run
--cleanupflag. Print a proposedrmlist for user review. Still requires human confirmation per the project's ownHOWTO-Windows.mdguidance. - Unit tests. Start with a fixture directory emulating
/Applications/Infected.app/Contents/...and assert onis_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:
- Open an issue first describing what you plan to change.
- Match the existing code style (PEP-8, 4-space indent, minimal docstrings).
- Do not introduce new dependencies beyond
pyDes. - 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
- Palo Alto Networks disclosure: http://researchcenter.paloaltonetworks.com/2014/11/wirelurker-new-era-os-x-ios-malware/
- Windows variant: http://researchcenter.paloaltonetworks.com/2014/11/wirelurker-windows/
- Community Windows cleaner: https://github.com/ltfish/WireLurkerCleaner
- Community Windows detector: https://github.com/kaustubhsant/WireLurkerDetector