Repository navigation
kex: defer cipher/MAC/comp swap to kex_finish on rekey - #2681
Open
abdul-khaliq-khalid wants to merge 1 commit into
Open
abdul-khaliq-khalid wants to merge 1 commit into
abdul-khaliq-khalid wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.