
Prova de conceito e laboratório demonstrando um ataque DoS de handshake com envenenamento de chave na negociação de versão compatível com QUIC no MsQuic v2.6.1, com uma testemunha de injeção UDP e análise de correção.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · msquic-compat-vn-key-poison
MsQuic v2.6.1 (a01333cf) - Microsoft
A negociação de versão compatível deveria esperar até que o cliente consiga de fato processar um pacote da nova versão. Esta árvore confirma a versão e recria as chaves Initial no momento em que um long header parece compatível, e então continua. O "desfazer" por falha de descriptografia restaura o número da versão e deixa as chaves em v2. Um pacote do tipo Handshake ou truncado sequer chega a esse desfazer.
Um único datagrama UDP vindo da 4-tupla do servidor interrompe o handshake de um cliente padrão. O processo permanece ativo. Sem RCE.
| ID | sem CVE ainda |
| CWE | CWE-345, CWE-347, CWE-670 |
| CVSS | Alto: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| Produto | MsQuic |
| Afetado | v2.6.1 (a01333cf); ainda em main quando verifiquei |
| Autenticação | UDP não autenticado, a origem deve ser a 4-tupla do servidor (on-path, ou spoof de IP do servidor mais a porta do cliente) |
| Licença | GNU Affero GPL v3.0 |
| Lab | somente 127.0.0.1 |
Durante um handshake QUIC v1, envie um datagrama de long header originado do endereço e porta do servidor. A versão é QUIC v2 (RFC 9369, wire 6b 33 43 cf). DestCidLength é 0. Clientes padrão usam um socket UDP conectado exclusivo, então aceitam um DCID de comprimento zero. A biblioteca trata isso como negociação de versão compatível antes do AEAD, deriva novas chaves Initial, e então ou nunca chega ao revert por falha de descriptografia (o tipo Handshake é adiado; Initial truncado retorna FALSE primeiro) ou reverte Stats.QuicVersion e deixa as chaves em v2.
O servidor real continua falando v1. O cliente retransmite com salts v2. Essa tentativa nunca chega a Connected. Outras conexões no mesmo processo não são afetadas. Isto é DoS de handshake, não RCE.
Off-path exige spoof de IP do servidor mais a porta UDP efêmera do cliente. On-path é suficiente. O binding exclusivo significa que o atacante não precisa do CID.
Li primeiro a onda de GHSAs de 2026 em microsoft/msquic: MITM de hostname (GHSA-w5f4-fx9m-m4q7, esta tag é o cherry-pick), RCE por path-UAF (GHSA-92f5-vc22-8j33), underflow de ACK (GHSA-gvvw-8j96-8g5r). Esses sinks estão fechados na v2.6.1.
Depois, cabeçalhos de pacote, CID, negociação de versão, retry, stateless reset, CIBIR. A VN compatível do RFC 9368 é o que sobrou. QuicConnRecvHeader vê um mismatch de versão, pergunta se é compatível, escreve OriginalQuicVersion, define CompatibleVerNegotiationAttempted, atribui Stats.QuicVersion, chama QuicCryptoOnVersionChange e continua. O comentário diz que os transport parameters verificarão ChosenVersion depois. Esses TPs nunca chegam se o handshake nunca descriptografar.
A falha de descriptografia parece uma correção até você lê-la:
Connection->Stats.QuicVersion = Connection->OriginalQuicVersion;
Connection->State.CompatibleVerNegotiationAttempted = FALSE;
Sem um segundo QuicCryptoOnVersionChange. Os labels do HKDF permanecem em v2. Pacotes do tipo Handshake são adiados (HANDSHAKE > ReadKey=INITIAL) e RecvHeader retorna FALSE, então esse desfazer nunca executa.
Não precisei de src/tools/attack. Sete bytes bastam.
Caminhos errados já registrados: injetar na porta de escuta do servidor (o cliente é exclusive-connected, não verá isso); encaminhar o Initial antes do poison no loopback (o handshake termina primeiro, o poison chega tarde); getchar() como espera do servidor de exemplo (Docker não é um teclado); clone shallow com submódulo openssl vazio (a imagem precisa buscar v2.6.1 e inicializar o openssl); qualquer long header lixo como prova (o 11223344 não compatível ainda fica Connected - a parte interessante é a v2 compatível). Teatro: um payload CRYPTO, um reverse shell, RCE. A testemunha é o resultado do handshake.
cd lab
./run.sh
O proxy UDP 127.0.0.1:18150 é o endereço ao qual o cliente se conecta. O servidor quicsample real é 127.0.0.1:18151. No primeiro datagrama do cliente, o PoC envia um Handshake v2 truncado f06b3343cf0000 a partir do socket do proxy, e então encaminha. Vincule-o ao loopback.
control_connected=1
inject_connected=0
negative_connected=1
inject_hex=f06b3343cf0000
SUCCESS MSQUIC-COMPAT-VN-KEY-POISON-WITNESS
CONTROL é apenas encaminhamento e deve ficar Connected. NEGATIVE injeta f0112233440000 primeiro e ainda deve ficar Connected. INJECT é a única fase que morre.
Não chame QuicCryptoOnVersionChange até que o pacote que a disparou seja descriptografado. Em toda falha após um commit (RecvHeader FALSE e falha de descriptografia) restaure a versão e derive as chaves de volta para OriginalQuicVersion. Ignore Handshake / 0-RTT como gatilhos de VN compatível. Somente um Initial descriptografável da nova versão deve trocar.
connection.c · crypto.c · binding.c · version_neg.c