Skip to main content

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...

image-1648675385510.png


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 :

image-1648675677985.png

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();
});
}
}
Cette fonction permet au serveur de charger en mémoire le flag à partir d'un fichier local puis de lancer de manière asynchrone une écoute sur le port choisie.
À chaque connexion, un nouveau thread est lancé avec un appel à la fonction handle_connection_initiator
  • 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(())
}
Les premières lignes représentent des lignes d'initialisations. Uns fois cela passé, le serveur envoie un premier message Hello Alice!
Le serveur attend alors la réponse Hello Bob! Si la réponse est incorrecte le serveur s'arrête ici.
Sinon le serveur génère un couple de clé (publique, privée) et envoie sa clé publique au client.
Il s'attend par la suite à recevoir de la même manière la clé publique du client.

Diffie Hellman

Le principe de cet échange est celui du chiffrement par secret partagé ou plus connue sous le nom de Diffie Hellman. Dans un chiffrement classique symétrique, un utilisateur génère une clé et la transmet de manière sécurisée à une personne. Les deux interlocuteurs ayant la même clé ils peuvent alors communiquer de manière sécurisée.
Malheureusement si un attaquant intercepte la clé lors de l'échange alors il pourra lui aussi écrire, modifier et déchiffrer des messages.
La méthode de Diffie Hellman veut rendre l'attaque plus compliquée. Alice et Bob vont tous les deux générer un couple de clé (privée, publique) et vont s'échanger leurs clés publiques.
Alice va alors chiffrer et déchiffrer les messages avec une clé qui est un mélange de sa clé privée et de la clé publique de Bob. Et inversement pour Bob. Ainsi même si l'attaquant intercepte les deux clés publiques il ne pourra rien faire car il ne connaîtra jamais une des deux clés privée.

image-1648675945552.png

Mascarade

Une fois que le serveur est en possession de la clé publique d'Alice, il va générer le secret partagé en réalisant un Diffie Hellman entre sa clé privée et la clé publique d'Alice (ligne 59)
Mais le serveur ne s'arrête pas là. Il réalise aussi deux autres Diffie Hellman, le premier entre une clé statique publique d'Alice et une clé statique privée de Bob et une seconde entre la clé publique d'Alice et la clé statique privée de Bob.
Au premier abord on se dit que pour qu'Alice puisse déchiffrer le message, elle va elle aussi devoir faire les opérations inverses à s'avoir un Diffie Hellman entre une clé publique statique de Bob et une clé statique privée d'Alice et un second avec une clé publique de Bob et la clé statique privée d'Alice.
Or nous n'avons que 2 clés statiques. La clé privée de Bob et la clé publique d'Alice.

Shared static secret

Le secret partagé à l'aide des clés statiques stockées en brut dans le fichier serveur n'as pas besoin d'être remplacé du côté client. En effet Diffie Hellman est un chiffrement symétrique ainsi le but est d'obtenir la même clé de chiffrement pour chiffrer et déchiffrer donc il nous suffit de récupérer les clé statiques du serveur pour le déchiffrement.

Shared static ephemeral secret

Ce secret utilise la clé statique de bob ainsi que la clé publique d'Alice. Une fois de plus nous avons accès à ces deux clés donc nous pouvons directement les réutiliser pour créer la clé de déchiffrement.

Client.rs

Maintenant que toute la partie théorique est bonne voyons le côté pratique. On commence par ce connecter au serveur, recevoir son message de bienvenu et lui envoyer le notre.

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 :

image-1648676102857.png

Flag : DGHACK{penurie_complete,penurie_basmati}