Critical fix: KEXINIT exchange hash encoding (prepend SSH_MSG_KEXINIT byte)
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

OpenSSH kexgex.c source code analysis:
- KEXINIT payload stored without SSH_MSG_KEXINIT type byte
- Exchange hash prepends SSH_MSG_KEXINIT byte (20) with adjusted length

Before fix:
- client_kexinit_payload included SSH_MSG_KEXINIT byte
- Direct use without prepending

After fix:
- Remove SSH_MSG_KEXINIT byte from payload
- Prepend byte (20) in exchange hash with length+1
- Both kex_exchange.rs and kex_complete.rs updated

Testing result: MAC still fails, indicating additional encoding issues
Next: Detailed comparison of all exchange hash components
This commit is contained in:
Warren
2026-06-14 23:14:14 +08:00
parent 9e4b14a2b7
commit 4778081866
3 changed files with 30 additions and 8 deletions

View File

@@ -71,11 +71,19 @@ impl KexState {
// V_S: 服务器版本字符串SSH string格式
write_ssh_string_to_hash(&mut hasher, &self.server_version)?;
// I_C: 客户端KEXINIT payloadSSH string格式
write_ssh_string_to_hash(&mut hasher, &String::from_utf8_lossy(&self.client_kexinit_payload))?;
// OpenSSH kexgex.c: "kexinit messages: fake header: len+SSH2_MSG_KEXINIT"
// Remove SSH_MSG_KEXINIT type byte from payloads and prepend it in exchange hash
// I_S: 服务器KEXINIT payloadSSH string格式
write_ssh_string_to_hash(&mut hasher, &String::from_utf8_lossy(&self.server_kexinit_payload))?;
let client_kexinit_without_type = &self.client_kexinit_payload[1..];
let server_kexinit_without_type = &self.server_kexinit_payload[1..];
hasher.update(&((client_kexinit_without_type.len() + 1) as u32).to_be_bytes());
hasher.update(&[20]); // SSH_MSG_KEXINIT type byte
hasher.update(client_kexinit_without_type);
hasher.update(&((server_kexinit_without_type.len() + 1) as u32).to_be_bytes());
hasher.update(&[20]); // SSH_MSG_KEXINIT type byte
hasher.update(server_kexinit_without_type);
// K_S: 服务器主机密钥blobSSH string格式
hasher.update(server_host_key_blob);

View File

@@ -215,11 +215,25 @@ impl KexExchangeHandler {
hasher.update(&(server_version.len() as u32).to_be_bytes());
hasher.update(server_version.as_bytes());
hasher.update(&(client_kexinit_payload.len() as u32).to_be_bytes());
hasher.update(client_kexinit_payload);
// OpenSSH kexgex.c: "kexinit messages: fake header: len+SSH2_MSG_KEXINIT"
// KEXINIT payload should NOT include SSH_MSG_KEXINIT type byte
// OpenSSH stores payload starting from cookie, prepends SSH_MSG_KEXINIT in exchange hash
hasher.update(&(server_kexinit_payload.len() as u32).to_be_bytes());
hasher.update(server_kexinit_payload);
// Remove SSH_MSG_KEXINIT type byte from payloads (our payload includes it)
let client_kexinit_without_type = &client_kexinit_payload[1..];
let server_kexinit_without_type = &server_kexinit_payload[1..];
info!("I_C (client KEXINIT without type byte): {} bytes (first byte should be cookie)", client_kexinit_without_type.len());
info!("I_S (server KEXINIT without type byte): {} bytes", server_kexinit_without_type.len());
// Exchange hash: uint32(len+1) + uint8(SSH_MSG_KEXINIT) + payload_without_type
hasher.update(&((client_kexinit_without_type.len() + 1) as u32).to_be_bytes());
hasher.update(&[20]); // SSH_MSG_KEXINIT type byte
hasher.update(client_kexinit_without_type);
hasher.update(&((server_kexinit_without_type.len() + 1) as u32).to_be_bytes());
hasher.update(&[20]); // SSH_MSG_KEXINIT type byte
hasher.update(server_kexinit_without_type);
hasher.update(&(host_key_blob.len() as u32).to_be_bytes());
hasher.update(host_key_blob);