mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
i can decrypt the length
This commit is contained in:
parent
adff1f593b
commit
08d28a152f
6 changed files with 102 additions and 50 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -152,16 +152,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crypto-bigint"
|
|
||||||
version = "0.5.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
|
||||||
dependencies = [
|
|
||||||
"rand_core",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
@ -701,8 +691,8 @@ dependencies = [
|
||||||
name = "ssh-transport"
|
name = "ssh-transport"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"crypto-bigint",
|
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"eyre",
|
"eyre",
|
||||||
"hex-literal",
|
"hex-literal",
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chacha20 = "0.9.1"
|
||||||
chacha20poly1305 = "0.10.1"
|
chacha20poly1305 = "0.10.1"
|
||||||
crypto-bigint = "0.5.5"
|
|
||||||
ed25519-dalek = { version = "2.1.1" }
|
ed25519-dalek = { version = "2.1.1" }
|
||||||
eyre = "0.6.12"
|
eyre = "0.6.12"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
|
|
||||||
12
ssh-transport/README.md
Normal file
12
ssh-transport/README.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# ssh-transport
|
||||||
|
|
||||||
|
Transport layer of SSH.
|
||||||
|
|
||||||
|
Based on [RFC 4253 The Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc4253)
|
||||||
|
and [RFC 4251 The Secure Shell (SSH) Protocol Architecture](https://datatracker.ietf.org/doc/html/rfc4251).
|
||||||
|
|
||||||
|
Other relevant RFCs:
|
||||||
|
- [RFC 5649 AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc5647)
|
||||||
|
- [RFC 5656 Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://datatracker.ietf.org/doc/html/rfc5656)
|
||||||
|
- [RFC 6668 SHA-2 Data Integrity Verification for the Secure Shell (SSH) Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc6668)
|
||||||
|
- [RFC 8709 Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH) Protocol](https://datatracker.ietf.org/doc/html/rfc8709)
|
||||||
|
|
@ -8,12 +8,8 @@ use crate::Result;
|
||||||
|
|
||||||
pub(crate) struct Session {
|
pub(crate) struct Session {
|
||||||
session_id: [u8; 32],
|
session_id: [u8; 32],
|
||||||
client_to_server_iv: [u8; 32],
|
encryption_key_client_to_server: SshChaCha20Poly1305,
|
||||||
server_to_client_iv: [u8; 32],
|
encryption_key_server_to_client: SshChaCha20Poly1305,
|
||||||
encryption_key_client_to_server: ChaCha20Poly1305,
|
|
||||||
encryption_key_server_to_client: ChaCha20Poly1305,
|
|
||||||
integrity_key_server_to_client: [u8; 32],
|
|
||||||
integrity_key_client_to_server: [u8; 32],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
|
|
@ -27,36 +23,58 @@ impl Session {
|
||||||
|
|
||||||
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
|
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
|
||||||
fn from_keys(session_id: [u8; 32], h: [u8; 32], k: [u8; 32]) -> Self {
|
fn from_keys(session_id: [u8; 32], h: [u8; 32], k: [u8; 32]) -> Self {
|
||||||
let derive = |letter: &str| {
|
let encryption_key_client_to_server =
|
||||||
let mut hash = sha2::Sha256::new();
|
SshChaCha20Poly1305::new(derive_key(k, h, "C", session_id));
|
||||||
encode_mpint_for_hash(&k, |data| hash.update(data));
|
let encryption_key_server_to_client =
|
||||||
hash.update(h);
|
SshChaCha20Poly1305::new(derive_key(k, h, "D", session_id));
|
||||||
hash.update(letter.as_bytes());
|
|
||||||
hash.update(session_id);
|
|
||||||
hash.finalize()
|
|
||||||
};
|
|
||||||
|
|
||||||
let encryption_key_client_to_server = ChaCha20Poly1305::new(&derive("C"));
|
|
||||||
let encryption_key_server_to_client = ChaCha20Poly1305::new(&derive("D"));
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
session_id,
|
session_id,
|
||||||
client_to_server_iv: derive("A").into(),
|
// client_to_server_iv: derive("A").into(),
|
||||||
server_to_client_iv: derive("B").into(),
|
// server_to_client_iv: derive("B").into(),
|
||||||
encryption_key_client_to_server,
|
encryption_key_client_to_server,
|
||||||
encryption_key_server_to_client,
|
encryption_key_server_to_client,
|
||||||
integrity_key_client_to_server: derive("E").into(),
|
// integrity_key_client_to_server: derive("E").into(),
|
||||||
integrity_key_server_to_client: derive("F").into(),
|
// integrity_key_server_to_client: derive("F").into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn decrypt_bytes(&mut self, bytes: &[u8]) -> Result<Vec<u8>> {
|
pub(crate) fn decrypt_len(&mut self, bytes: &mut [u8], packet_number: u64) {
|
||||||
self.encryption_key_client_to_server
|
self.encryption_key_client_to_server
|
||||||
.decrypt(&[0; 12].into(), bytes)
|
.decrypt_len(bytes, packet_number);
|
||||||
.map_err(|_| crate::client_error!("failed to decrypt, invalid message"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derive a key from the shared secret K and exchange hash H.
|
||||||
|
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
|
||||||
|
fn derive_key<const KEY_LEN: usize>(
|
||||||
|
k: [u8; 32],
|
||||||
|
h: [u8; 32],
|
||||||
|
letter: &str,
|
||||||
|
session_id: [u8; 32],
|
||||||
|
) -> [u8; KEY_LEN] {
|
||||||
|
let sha2len = sha2::Sha256::output_size();
|
||||||
|
|
||||||
|
let mut output = [0; KEY_LEN];
|
||||||
|
|
||||||
|
for i in 0..(KEY_LEN / sha2len) {
|
||||||
|
let mut hash = sha2::Sha256::new();
|
||||||
|
encode_mpint_for_hash(&k, |data| hash.update(data));
|
||||||
|
hash.update(h);
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
hash.update(letter.as_bytes());
|
||||||
|
hash.update(session_id);
|
||||||
|
}
|
||||||
|
hash.update(&output[..(i * sha2len)]);
|
||||||
|
|
||||||
|
|
||||||
|
output[(i * sha2len)..][..sha2len].copy_from_slice(&hash.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn encode_mpint_for_hash(mut key: &[u8], mut add_to_hash: impl FnMut(&[u8])) {
|
pub(crate) fn encode_mpint_for_hash(mut key: &[u8], mut add_to_hash: impl FnMut(&[u8])) {
|
||||||
while key[0] == 0 {
|
while key[0] == 0 {
|
||||||
key = &key[1..];
|
key = &key[1..];
|
||||||
|
|
@ -69,3 +87,29 @@ pub(crate) fn encode_mpint_for_hash(mut key: &[u8], mut add_to_hash: impl FnMut(
|
||||||
}
|
}
|
||||||
add_to_hash(key);
|
add_to_hash(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `chacha20-poly1305@openssh.com` uses a 64-bit nonce, not the 96-bit one in the IETF version.
|
||||||
|
type SshChaCha20 = chacha20::ChaCha20Legacy;
|
||||||
|
|
||||||
|
struct SshChaCha20Poly1305 {
|
||||||
|
header_key: [u8; 32],
|
||||||
|
main: ChaCha20Poly1305,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshChaCha20Poly1305 {
|
||||||
|
fn new(key: [u8; 64]) -> Self {
|
||||||
|
Self {
|
||||||
|
main: ChaCha20Poly1305::new(&<[u8; 32]>::try_from(&key[..32]).unwrap().into()),
|
||||||
|
header_key: key[32..].try_into().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_len(&self, bytes: &mut [u8], packet_number: u64) {
|
||||||
|
use chacha20::cipher::{KeyIvInit, StreamCipher};
|
||||||
|
|
||||||
|
// <https://github.com/openssh/openssh-portable/blob/1ec0a64c5dc57b8a2053a93b5ef0d02ff8598e5c/PROTOCOL.chacha20poly1305>
|
||||||
|
let mut cipher =
|
||||||
|
SshChaCha20::new(&self.header_key.into(), &packet_number.to_be_bytes().into());
|
||||||
|
cipher.apply_keystream(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,12 +221,12 @@ impl ServerConnection {
|
||||||
|
|
||||||
let secret =
|
let secret =
|
||||||
EphemeralSecret::random_from_rng(SshRngRandAdapter(&mut *self.rng));
|
EphemeralSecret::random_from_rng(SshRngRandAdapter(&mut *self.rng));
|
||||||
let server_public_key = PublicKey::from(&secret); // f
|
let server_public_key = PublicKey::from(&secret); // Q_S
|
||||||
|
|
||||||
let client_public_key = dh.e; // e
|
let client_public_key = dh.e; // Q_C
|
||||||
|
|
||||||
let shared_secret =
|
let shared_secret =
|
||||||
secret.diffie_hellman(&client_public_key.to_x25519_public_key()?); // k
|
secret.diffie_hellman(&client_public_key.to_x25519_public_key()?); // K
|
||||||
|
|
||||||
let pub_hostkey = SshPublicKey {
|
let pub_hostkey = SshPublicKey {
|
||||||
format: b"ssh-ed25519",
|
format: b"ssh-ed25519",
|
||||||
|
|
@ -256,11 +256,11 @@ impl ServerConnection {
|
||||||
hash_string(&mut hash, client_kexinit); // I_C
|
hash_string(&mut hash, client_kexinit); // I_C
|
||||||
hash_string(&mut hash, server_kexinit); // I_S
|
hash_string(&mut hash, server_kexinit); // I_S
|
||||||
add_hash(&mut hash, &pub_hostkey.to_bytes()); // K_S
|
add_hash(&mut hash, &pub_hostkey.to_bytes()); // K_S
|
||||||
|
// For normal DH as in RFC4253, e and f are mpints.
|
||||||
// While the RFC says that e and f are mpints, we need to *NOT* treat them as mpints here.
|
// But for ECDH as defined in RFC5656, Q_C and Q_S are strings.
|
||||||
// Neither RFC4253 nor RFC8709 mention this.
|
// <https://datatracker.ietf.org/doc/html/rfc5656#section-4>
|
||||||
hash_string(&mut hash, &client_public_key.0); // e
|
hash_string(&mut hash, &client_public_key.0); // Q_C
|
||||||
hash_string(&mut hash, server_public_key.as_bytes()); // f
|
hash_string(&mut hash, server_public_key.as_bytes()); // Q_S
|
||||||
hash_mpint(&mut hash, shared_secret.as_bytes()); // K
|
hash_mpint(&mut hash, shared_secret.as_bytes()); // K
|
||||||
|
|
||||||
let hash = hash.finalize();
|
let hash = hash.finalize();
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use crate::Result;
|
||||||
pub(crate) struct PacketTransport {
|
pub(crate) struct PacketTransport {
|
||||||
state: PacketTransportState,
|
state: PacketTransportState,
|
||||||
packets: VecDeque<Packet>,
|
packets: VecDeque<Packet>,
|
||||||
next_recv_seq_nr: u32,
|
next_recv_seq_nr: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PacketTransportState {
|
enum PacketTransportState {
|
||||||
|
|
@ -62,11 +62,17 @@ impl PacketTransport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PacketTransportState::Keyed { session } => {
|
PacketTransportState::Keyed { session } => {
|
||||||
// TODO: don't yolo?...
|
let mut len = [0_u8; 4];
|
||||||
let encrypted_len = &bytes[..4];
|
let Some(len_bytes) = bytes.get(0..4) else {
|
||||||
// TODO: all of this is nonsense. how does AEAD even work with these partial decryptions?
|
return Err(client_error!(
|
||||||
// should i just validate it by hand?? i will find out tomorrow!
|
"packet too short, not enough bytes for length"
|
||||||
let decrypted_len = session.decrypt_bytes(encrypted_len)?;
|
));
|
||||||
|
};
|
||||||
|
len.copy_from_slice(len_bytes);
|
||||||
|
session.decrypt_len(&mut len, self.next_recv_seq_nr);
|
||||||
|
let len = u32::from_be_bytes(len);
|
||||||
|
dbg!(len);
|
||||||
|
// TODO: dont assume we get it all as one.... AAaAAA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue