Mascarade (100 points)
Description
Bob a le flag. Et il l’envoie à Alice en utilisant un échange de clé sécurisé et du chiffrement authentifié.
Le code qu’il utilise est dans le fichier ake_server.rs.
Comme on peut le voir, c’est du Rust, donc pas de vulnérabilité à exploiter sur le serveur...
Fichiers
ake_server.rs
Cargo.toml
En plus des fichiers, nous avons accès à un serveur TCP :
tcp://mascarade.chall.malicecyber.com:4999/
Solution
Première approche
Commençons par nous connecter au serveur :
Le serveur nous répond Hello Alice! et attend le bon message en retour. Pour cela analysons le ficher serveur. Ce dernier est composé de 3 fonctions, les 2 premières sont relativement simples.
- Tout d'abord la fonction main
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let addr = "0.0.0.0:7878";
server_loop(addr, FLAG_PATH).await
}
Cette fonction permet de spécifier au serveur un port d'écoute et de lancer la fonction server_loop
- La fonction serveur_loop
async fn server_loop(addr: &str, flag_loc: &str) -> Result<(), Box<dyn Error>> {
let flag =
Arc::new(fs::read_to_string(flag_loc).expect("Something went wrong reading the file"));
println!("Flag {}", flag);
let listener = TcpListener::bind(addr).await?;
println!("Listening on: {}", addr);
let counter = AtomicUsize::new(0);
loop {
// Asynchronously wait for an inbound socket.
let (stream, _) = listener.accept().await?;
let c = counter.fetch_add(1, Ordering::SeqCst) + 1;
if c % 1000 == 0 {
println!("{} connections", c);
}
let flag = flag.clone();
tokio::spawn(async move {
handle_connection_initiator(stream, flag).await.ok();
});
}
}
- La fonction principale d'échange client-serveur que nous détaillerons par petit bout.
const HELLO_ALICE: &str = "Hello Alice!\n";
const HELLO_BOB: &str = "Hello Bob!\n";
async fn handle_connection_initiator(
mut stream: TcpStream,
flag: Arc<String>,
) -> Result<(), Box<dyn Error>> {
let bob_static_secret: StaticSecret = StaticSecret::from([
128, 0, 20, 121, 100, 3, 92, 119, 70, 203, 20, 8, 122, 109, 231, 12, 103, 203, 231, 222,
127, 221, 171, 139, 176, 8, 114, 52, 61, 98, 3, 64,
]);
let alice_static_public: PublicKey = PublicKey::from([
20, 2, 29, 90, 241, 67, 52, 1, 217, 46, 238, 54, 248, 8, 227, 39, 81, 48, 215, 36, 220,
241, 207, 33, 186, 112, 32, 254, 188, 140, 12, 10,
]);
// Say Hello!
stream.write_all(&HELLO_ALICE.as_bytes()).await?;
let buffer_size = 1024;
let mut buffer = vec![0; buffer_size];
let size_read = stream.read(&mut buffer).await?;
let s = std::str::from_utf8(&buffer[..size_read])?;
if s != HELLO_BOB {
return Ok(()); // I don't want to implement annoying error management here. So we just stop
}
assert_eq!(
std::str::from_utf8(&buffer[..size_read]).unwrap(),
HELLO_BOB
);
// run the handshake.
// generate ephemerals
// spawn a blocking task for that (it is CPU-intensive)
let (bob_secret, bob_public) = task::spawn_blocking(move || {
let bob_secret = EphemeralSecret::new(OsRng);
let bob_public = PublicKey::from(&bob_secret);
(bob_secret, bob_public)
})
.await?;
// send the initiator message with our ephemeral public key
stream.write_all(bob_public.as_bytes()).await?;
let mut buffer = [0; 32]; // size of a public key
// get the responder message
let _ = stream.read_exact(&mut buffer).await?;
let alice_public = PublicKey::from(buffer);
// the next steps are CPU-intensive
let ct = task::spawn_blocking(move || {
// compute the shared secrets
let shared_ephemeral_secret = bob_secret.diffie_hellman(&alice_public);
let shared_static_secret = bob_static_secret.diffie_hellman(&alice_static_public);
let shared_static_ephemeral_secret = bob_static_secret.diffie_hellman(&alice_public);
// derive the key
let shared_secret = Blake2b::new()
.chain(shared_ephemeral_secret.as_bytes())
.chain(shared_static_secret.as_bytes())
.chain(shared_static_ephemeral_secret.as_bytes())
.finalize();
// construct the cipher and encrypt
let cipher = ChaCha20Poly1305::new(Key::from_slice(&shared_secret[..32]));
let nonce = Nonce::from_slice(&[0u8; 12]); // we only use one nonce, so pick something simple
cipher
.encrypt(nonce, flag.as_bytes())
.expect("encryption failure!") // NOTE: handle this error to avoid panics!
})
.await?;
stream.write_all(&ct).await?;
Ok(())
}
Hello Alice!
Hello Bob!
Si la réponse est incorrecte le serveur s'arrête ici.Diffie Hellman
Mascarade
Shared static secret
Shared static ephemeral secret
Client.rs
fn main() {
match TcpStream::connect("mascarade.chall.malicecyber.com:4999") {
Ok(mut stream) => {
println!("Successfully connected to server in port 4999");
let mut data = [0 as u8; 13]; // using 6 byte buffer
match stream.read(&mut data) {
Ok(_) => {
let text = from_utf8(&data).unwrap();
println!("{}", text);
},
Err(e) => {
println!("Failed to receive data: {}", e);
}
}
let msg = "Hello Bob!\n";
stream.write_all(msg.as_bytes());
},
Err(e) => {
println!("Failed to connect: {}", e);
}
}
println!("Terminated.");
}
On rajoute en suite toute la partie d'échange de clé :
let mut buffer = [0; 32]; // using 6 byte buffer
let bob_public;
match stream.read(&mut buffer) {
Ok(_) =>{
println!("{:?}", buffer);
}
Err(e) => {
}
}
bob_public = PublicKey::from(buffer);
let alice_secret = StaticSecret::new(OsRng);
let alice_public = PublicKey::from(&alice_secret);
println!("{:?}", alice_public.as_bytes());
stream.write(alice_public.as_bytes());
On récupère le flag chiffré et on crée les clés de déchiffrements :
let mut data = [0 as u8; 56]; // using 6 byte buffer
match stream.read_exact(&mut data) {
Ok(_) => {
println!("{:?}", data);
},
Err(e) => {
println!("Failed to receive data: {}", e);
}
}
let flag = data;
let bob_static_secret: StaticSecret = StaticSecret::from([
128, 0, 20, 121, 100, 3, 92, 119, 70, 203, 20, 8, 122, 109, 231, 12, 103, 203, 231, 222,
127, 221, 171, 139, 176, 8, 114, 52, 61, 98, 3, 64,
]);
let alice_static_public: PublicKey = PublicKey::from([
20, 2, 29, 90, 241, 67, 52, 1, 217, 46, 238, 54, 248, 8, 227, 39, 81, 48, 215, 36, 220,
241, 207, 33, 186, 112, 32, 254, 188, 140, 12, 10,
]);
let shared_ephemeral_secret = alice_secret.diffie_hellman(&bob_public);
let shared_static_secret = bob_static_secret.diffie_hellman(&alice_static_public);
let shared_static_ephemeral_secret = bob_static_secret.diffie_hellman(&alice_public);
On termine par déchiffrer le message et afficher le tout :
let shared_secret = Blake2b::new()
.chain(shared_ephemeral_secret.as_bytes())
.chain(shared_static_secret.as_bytes())
.chain(shared_static_ephemeral_secret.as_bytes())
.finalize();
let cipher = ChaCha20Poly1305::new(Key::from_slice(&shared_secret[..32]));
let nonce = Nonce::from_slice(&[0u8; 12]);
let plaintext = cipher
.decrypt(nonce, flag.as_ref())
.expect("decryption failure!");
let text = from_utf8(&plaintext).unwrap();
println!("{:?}", text);
Le code entier nous donne cela :
use std::net::{TcpStream};
use std::io::{Read, Write};
use std::str::from_utf8;
use blake2::{Blake2b, Digest};
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
use chacha20poly1305::aead::{Aead, NewAead};
use rand_core::OsRng;
use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
fn main() {
match TcpStream::connect("mascarade.chall.malicecyber.com:4999") {
Ok(mut stream) => {
println!("Successfully connected to server in port 4999");
let mut data = [0 as u8; 13]; // using 6 byte buffer
match stream.read(&mut data) {
Ok(_) => {
let text = from_utf8(&data).unwrap();
println!("{}", text);
},
Err(e) => {
println!("Failed to receive data: {}", e);
}
}
let msg = "Hello Bob!\n";
stream.write_all(msg.as_bytes());
let mut buffer = [0; 32]; // using 6 byte buffer
let bob_public;
match stream.read(&mut buffer) {
Ok(_) =>{
println!("{:?}", buffer);
}
Err(e) => {
}
}
bob_public = PublicKey::from(buffer);
let alice_secret = StaticSecret::new(OsRng);
let alice_public = PublicKey::from(&alice_secret);
println!("{:?}", alice_public.as_bytes());
stream.write(alice_public.as_bytes());
let mut data = [0 as u8; 56]; // using 6 byte buffer
match stream.read_exact(&mut data) {
Ok(_) => {
println!("{:?}", data);
},
Err(e) => {
println!("Failed to receive data: {}", e);
}
}
let flag = data;
let bob_static_secret: StaticSecret = StaticSecret::from([
128, 0, 20, 121, 100, 3, 92, 119, 70, 203, 20, 8, 122, 109, 231, 12, 103, 203, 231, 222,
127, 221, 171, 139, 176, 8, 114, 52, 61, 98, 3, 64,
]);
let alice_static_public: PublicKey = PublicKey::from([
20, 2, 29, 90, 241, 67, 52, 1, 217, 46, 238, 54, 248, 8, 227, 39, 81, 48, 215, 36, 220,
241, 207, 33, 186, 112, 32, 254, 188, 140, 12, 10,
]);
let shared_ephemeral_secret = alice_secret.diffie_hellman(&bob_public);
let shared_static_secret = bob_static_secret.diffie_hellman(&alice_static_public);
let shared_static_ephemeral_secret = bob_static_secret.diffie_hellman(&alice_public);
let shared_secret = Blake2b::new()
.chain(shared_ephemeral_secret.as_bytes())
.chain(shared_static_secret.as_bytes())
.chain(shared_static_ephemeral_secret.as_bytes())
.finalize();
let cipher = ChaCha20Poly1305::new(Key::from_slice(&shared_secret[..32]));
let nonce = Nonce::from_slice(&[0u8; 12]);
let plaintext = cipher
.decrypt(nonce, flag.as_ref())
.expect("decryption failure!");
let text = from_utf8(&plaintext).unwrap();
println!("{:?}", text);
},
Err(e) => {
println!("Failed to connect: {}", e);
}
}
println!("Terminated.");
}
Une fois compilé et exécuté on obtient bien notre flag du premier coup :
Flag : DGHACK{penurie_complete,penurie_basmati}