Visitar URL original
kex: defer cipher/MAC/comp swap to kex_finish on rekey by abdul-khaliq-khalid · Pull Request #2681 · libssh2/libssh2 · GitHub
Skip to content

kex: defer cipher/MAC/comp swap to kex_finish on rekey - #2681

Open
abdul-khaliq-khalid wants to merge 1 commit into
libssh2:masterfrom
abdul-khaliq-khalid:kex-rekey-method-swap
Open

abdul-khaliq-khalid wants to merge 1 commit into
libssh2:masterfrom
abdul-khaliq-khalid:kex-rekey-method-swap

Conversation

@abdul-khaliq-khalid

Copy link
Copy Markdown
Contributor

During a key re-exchange, kex_agree_methods() swaps session->local/remote .crypt, .mac and .comp to the newly negotiated methods as soon as the peer's KEXINIT is parsed, but the matching crypt_abstract/mac_abstract/comp_abstract contexts still belong to the previous methods until kex_finish() runs after NEWKEYS. In that window the KEX packets get encrypted and decrypted with the new method's callbacks on the old method's context. If a server rekeys from chacha20-poly1305@openssh.com to aes128-ctr, crypt_encrypt() ends up calling EVP_Cipher() on crypt_ctx.h, a field the chacha init never set, so the client dereferences an uninitialized pointer and crashes. The same mismatch also lets kex_finish() and session teardown run the new method's dtor on the old context, and the MAC path reads the new digest_len from a key buffer sized for the old method.

The fix records the negotiated methods in new crypt_next/mac_next/comp_next slots. kex_finish() keeps using the active methods to tear down the old context and only swaps in the *_next methods right before init, so KEX traffic always uses the still-active epoch. Any malicious server can trigger the crash against a connected client by offering a different cipher in its rekey KEXINIT.

I reproduced it with an asyncssh server that rekeys to a different cipher while streaming data to a small libssh2 client under AddressSanitizer: on master it is a SEGV in EVP_Cipher from crypt_encrypt (crypt.c:120) as the client sends ECDH_INIT for the rekey; with the patch the rekey completes, the cipher switches from chacha20-poly1305 to aes128-ctr, and the stream reads back cleanly. A normal same-cipher rekey (driven by asyncssh's own rekey_bytes) keeps working unchanged.

This is the same root cause the maintainers were puzzling over in #2407 / #2412 ("Why isn't the dtor called on the existing mac on rekey, before rekeying?"). #2412 papers over the MAC key leak by making the AEAD-stub dtor defensive, but it does not address the memory corruption from the crypt method mismatch, and the same mismatch affects comp too. Deferring the swap removes the init/dtor-mismatch entirely, which looked like the cleaner direction suggested in that thread, so I think it is worth preferring over the narrower fix.

I did not add a test: the repo's suite runs against a fixed OpenSSH server, which never negotiates a different cipher on rekey, so this adversarial path is not expressible there. Build is clean with ENABLE_WERROR and PICKY_COMPILER (OpenSSL backend); checksrc and spacecheck add no warnings.

kex_agree_methods() overwrites session->local/remote .crypt, .mac and
.comp with the newly negotiated methods the moment the peer's KEXINIT is
parsed, but the matching crypt_abstract/mac_abstract/comp_abstract
contexts still belong to the previous methods until kex_finish() runs
after NEWKEYS. During a rekey this makes the KEX packets be encrypted
and decrypted with the new method's callbacks on the old method's
context. Switching from chacha20-poly1305@openssh.com to aes128-ctr then
has crypt_encrypt() call EVP_Cipher() on crypt_ctx.h, which the chacha
init never set, so the client dereferences an uninitialized pointer and
crashes (SEGV in EVP_Cipher from crypt_encrypt). kex_finish() and
session teardown also run the new method's dtor on the old context, and
the MAC side reads the new digest_len from a key buffer sized for the
old method.

Record the negotiated methods in new crypt_next/mac_next/comp_next slots
instead. kex_finish() keeps using the active methods to tear down the
old context and only swaps in the *_next methods just before init, so
KEX traffic always uses the still-active epoch. A malicious server can
trigger this against any connected client by offering a different cipher
in its rekey KEXINIT.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant