# DG'hAck 2021 - Electro ## Ranking - 22nd on the general with 1400 pts, - 13th on the professional category. ![](https://i.imgur.com/riyY5Yy.png) ## Achievement - First Blood on "Crypto Be Crushed" ![](https://i.imgur.com/8dADoNI.png) # Cryptography ## Crypto Be Crushed - 1st Blood I have just noticed the hint in the name of the challenge "CBC" during the redaction of this write-up ... ### Description ``` Vous participez à un CTF, ainsi que votre colocataire, Alice, avec laquelle vous partagez la même connexion internet et le Wifi. Reconnaissons-le, vous êtes un peu paresseux : pourquoi résoudre un challenge si elle l’a déjà terminé ? Cependant, comme expliqué dans les règles du CTF, les soumissions de flag sont chiffrées avec une clé propre à chaque utilisateur (voir le fichier CTF_Submission.md) et même si vous avez réussi à capturer la solution du dixième challenge, celui-ci est chiffré (en hexadécimal) : 61499b3f31cee611a72eaf3cbfcf7d1cebb228a44db94e7b0504c145fcf00e57d2e0b9e24c7259bbeebccd03c100a645f418f2f58cc073cc71f214eb64a3b20ddfb406f6ebbd6781119efe13116af3abfe52609961727213ea69b8f8f1e4298ed3a42bc9ae4b8f1785184153ee3e113a8c9d55ddec48c85c53d5aa4a4089e47c3026a0bdb4d5d2659e57c31a76cca407ea0a92430d8540b8ef677405e8c4b193 Heureusement, Alice a décidé d’adapter le script de chiffrement fourni par l’organisateur du CTF et l’a transformé en un serveur de chiffrement. Par chance, Alice a oublié de configurer correctement son pare-feu et vous êtes capable d’y accéder ``` ### CTF Submission ``` # CTF Submission instructions Welcome to the CTF, player! This year, for security purposes, we decided to use specific procedure for you to use when submitting a flag. Indeed, it must be submitted in a CBC-encrypted-JSON with a per-user key. We sent you the key at registration. If you are not comfy with cryptography, no worries, you will find the code in the FAQ (`encrypt_challenge.py`, which uses the `PyCryptodome` library -- *don't roll your own crypto*). You will just have to put your provided key in a `key.txt` file. To be properly processed, the JSON file must have the following format : - a `"sig"` value, with a random 32 bytes values encoded as a hex string which is provided to you for each challenge; - a `"flag"` value containing the submitted flag; - a `"user"` value, the user's login; - and a `"cid"` value, which is the challenge id. An example JSON is provided by the `ex_flag.json` file. Happy hacking! ``` ### Encrypt Challenge ``` #!/usr/bin/env python3 from Crypto.Cipher import AES from Crypto import Random import secrets def pkcs7_padding(m): # There is padding and there is PKCS#7 padding 🤮 l = len(m) pad_len = 16 - (l % 16) pad_len_hex = pad_len.to_bytes(1, byteorder="little") padding = bytes([pad_len_hex[0] for i in range(0, pad_len)]) return m+padding # Prevent IV replays iv_list = set() def encrypt(iv, m): if iv in iv_list: print("ERROR: REPLAYED IV") return bytes([]) iv_list.add(iv) m = pkcs7_padding(m) cipher = AES.new(KEY, AES.MODE_CBC, iv) return iv + cipher.encrypt(m) def get_iv(): return Random.new().read(AES.block_size) KEY = open('key.txt', 'rb').read() # stdin/stdout version if __name__ == "__main__": while True: iv = get_iv() print("IV for encryption (hex):") print(iv.hex()) print("Enter message (as a hex bytes string):") x = bytes.fromhex(input()) # print("Message: " + str(x) + "\n") print("Ciphertext (hex): " + str(encrypt(iv, x).hex()) + "\n") ``` ### ex_flag ``` {"sig":"80a7ccd5aa2f3b0f917267640c6ff37c50e7f3673a30d20c0e133fe8c20d5cd1","flag":"DG'hAck-{{b51613f7}}","user":"JohnDoe","cid":4} ``` ### Solution In the md file, there is an interesting comment ***don't roll your own crypto***, which may indicates a crypto methodology related weakness. Within the encrypt_challenge.py file, we have an AES-CBC encryption server. AES-CBC is dependent of an IV which is provided as an output. From the ex_flag file, we are provided with a flag sample, for John Doe and a cid of 4, whereas we are looking for Alice and a cid of 10 (see md file). The objective is clearly to transform the AES-CBC encryption server into an AES-CBC encryption oracle. This is possible ***because Alice own crypto*** has been implemented ***and the IV is provided before the encryption***. As the IV is provided before encryption, it is possible to nullify its effects and to used the iv of the intercepted flag (nullification is performed with a xor operation). By filling the flag example with Alice data, we have a well aligned (16 bytes) flag including the future padding. ``` {"sig":"80a7ccd5aa2f3b0f917267640c6ff37c50e7f3673a30d20c0e133fe8c20d5cd1","flag":"DG'hAck-{{b51613f7}}","user":"Alice","cid":10} ``` Lets break it into 16 bytes chunk, and bruteforce the flag ! ``` from pwn import * import binascii def getiv(io): io.recvuntil(b"IV for encryption (hex):\n") return io.recvline().strip() def send(io,txt): io.recvuntil(b"Enter message (as a hex bytes string):\n") io.sendline(txt.hex().encode()) def getct(io): io.recvuntil(b"Ciphertext (hex): ") return io.recvline().strip() def xor(a,b,order="little"): tbs = b"" for j,elt in enumerate(a): tbs += (elt ^b[j]).to_bytes(1, byteorder=order) return tbs def template(k,n): if n==4: _ = b':"DG\'hAck-{{'+binascii.hexlify(k.to_bytes(2, byteorder="big")) if n==3: _ = binascii.hexlify(k.to_bytes(2, byteorder="big"))+b'}}","user":"' if n==2: _ = b"""Alice","cid":10}""" if n==1: _ = b"\x10"*16 return _ flag="61499b3f31cee611a72eaf3cbfcf7d1cebb228a44db94e7b0504c145fcf00e57d2e0b9e24c7259bbeebccd03c100a645f418f2f58cc073cc71f214eb64a3b20ddfb406f6ebbd6781119efe13116af3abfe52609961727213ea69b8f8f1e4298ed3a42bc9ae4b8f1785184153ee3e113a8c9d55ddec48c85c53d5aa4a4089e47c3026a0bdb4d5d2659e57c31a76cca407ea0a92430d8540b8ef677405e8c4b193" iv = flag[:32] ct = flag[32:] print("Alive IV : ") print("======================") print(iv) print("Alice FLAG : ") print("======================") print(ct) print("Defining Target") print("======================") target = [] for i in range(0,len(flag),32): if ct[i:i+32] != b"": target.append(binascii.unhexlify(ct[i:i+32])) del target[-1] print(target) print("=============") # Connection io = remote("cryptobecrushed.chall.malicecyber.com",4242) # For all interesting segment for n in range(1,4+1,1): for k in range(0,256*256): # Get the flag segment found = template(k,n) # getiv iv=binascii.unhexlify(getiv(io)) # Xor __ =found s = xor(xor(__,target[-(n+1)],order="little"),iv,"little") # Interact with the encryption oracle send(io,s) res =binascii.unhexlify(getct(io)) # Check if we found the string if res.find(target[-n])>-1: print(target[-n]) print([res[i:i+16] for i in range(0,len(res),16)]) print(found) print("=========") break ``` Execution of the script: ``` Alive IV : ====================== 61499b3f31cee611a72eaf3cbfcf7d1c Alice FLAG : ====================== ebb228a44db94e7b0504c145fcf00e57d2e0b9e24c7259bbeebccd03c100a645f418f2f58cc073cc71f214eb64a3b20ddfb406f6ebbd6781119efe13116af3abfe52609961727213ea69b8f8f1e4298ed3a42bc9ae4b8f1785184153ee3e113a8c9d55ddec48c85c53d5aa4a4089e47c3026a0bdb4d5d2659e57c31a76cca407ea0a92430d8540b8ef677405e8c4b193 Defining Target ====================== [b'\xeb\xb2(\xa4M\xb9N{\x05\x04\xc1E\xfc\xf0\x0eW', b'\xd2\xe0\xb9\xe2LrY\xbb\xee\xbc\xcd\x03\xc1\x00\xa6E', b'\xf4\x18\xf2\xf5\x8c\xc0s\xccq\xf2\x14\xebd\xa3\xb2\r', b'\xdf\xb4\x06\xf6\xeb\xbdg\x81\x11\x9e\xfe\x13\x11j\xf3\xab', b'\xfeR`\x99arr\x13\xeai\xb8\xf8\xf1\xe4)\x8e', b'\xd3\xa4+\xc9\xaeK\x8f\x17\x85\x18AS\xee>\x11:', b'\x8c\x9dU\xdd\xecH\xc8\\S\xd5\xaaJ@\x89\xe4|', b'0&\xa0\xbd\xb4\xd5\xd2e\x9eW\xc3\x1av\xcc\xa4\x07', b'\xea\n\x92C\r\x85@\xb8\xefgt\x05\xe8\xc4\xb1\x93'] ============= [+] Opening connection to cryptobecrushed.chall.malicecyber.com on port 4242: Done b'\xea\n\x92C\r\x85@\xb8\xefgt\x05\xe8\xc4\xb1\x93' [b'\xc51f\xde\x0f\xa7\x91W\x05\x84\x08[kT\xb1\x95', b'\xea\n\x92C\r\x85@\xb8\xefgt\x05\xe8\xc4\xb1\x93', b'\t\x90\xb9Q\xbd`\x1a\xdd\x8c\xd4\x9bK\xd8@\xa0\x15'] b'\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10' ========= b'0&\xa0\xbd\xb4\xd5\xd2e\x9eW\xc3\x1av\xcc\xa4\x07' [b'[\xc6\xbf"\x1aT\'\xf1\x0cG\'wF\xd7\xbb\xc4', b'0&\xa0\xbd\xb4\xd5\xd2e\x9eW\xc3\x1av\xcc\xa4\x07', b'\xea\n\x92C\r\x85@\xb8\xefgt\x05\xe8\xc4\xb1\x93'] b'Alice","cid":10}' ========= b'\x8c\x9dU\xdd\xecH\xc8\\S\xd5\xaaJ@\x89\xe4|' [b'\xc1\x9e\x05\xf9\xd7Q\xed\xff\xceN\xac\xa1\xd3\x15\x99\x80', b'\x8c\x9dU\xdd\xecH\xc8\\S\xd5\xaaJ@\x89\xe4|', b'\x85SFJ\x9ei\x1c\x01\xc4\x908}\nY\x00X'] b'b967}}","user":"' ========= b'\xd3\xa4+\xc9\xaeK\x8f\x17\x85\x18AS\xee>\x11:' [b'`\\`\x1a?^\xbf{\x1a\x99\xe6f\x08&\x0e\xfc', b'\xd3\xa4+\xc9\xaeK\x8f\x17\x85\x18AS\xee>\x11:', b"?\x0e\xee\x0e\\\xae\xb9'%\x9a%9\xa8\xab'\xb3"] b':"DG\'hAck-{{e20e' ========= [*] Closed connection to cryptobecrushed.chall.malicecyber.com port 4242 real 11m36,058s user 1m14,446s sys 0m6,693s ``` Job Done in less than 12 minutes! ***DG'hAck-{{e20eb967}}*** ## Mascarade ### 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... ``` ### Cargo.toml ``` [package] name = "chall_ake" version = "0.1.0" edition = "2018" # [[bin]] # name = "noise_clien-server" # path = "src/noise.rs" [[bin]] name = "ake_server" path = "src/ake_server.rs" [[bin]] name = "ake_client" path = "src/ake_client.rs" [[bin]] name = "ake_attack" path = "exploit/ake_attack.rs" [dependencies] # noise-protocol = "0.1.3" # noise-rust-crypto = "0.4.1" # noise-sodiumoxide = "0.1.1" chacha20poly1305 = "0.8.0" blake2 = "0.9.1" x25519-dalek = "1.1.1" rand_core = "0.5" futures = "0.3" tokio = { version = "1", features = ["full"] } ``` ### ake_server.rs ``` use blake2::{Blake2b, Digest}; use chacha20poly1305::aead::{Aead, NewAead}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; use rand_core::OsRng; use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret}; use std::{fs, sync::Arc}; use tokio::net::TcpListener; use tokio::net::TcpStream; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, task, }; use std::{ error::Error, sync::atomic::{AtomicUsize, Ordering}, }; const FLAG_PATH: &str = "flag.txt"; #[tokio::main] async fn main() -> Result<(), Box<dyn Error>> { let addr = "0.0.0.0:7878"; server_loop(addr, FLAG_PATH).await } 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(); }); } } 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(()) } ``` ### Solution Here, we are part of a Diffie-Ellman exchange on an elliptic curve, and we can use the shared knowledge to perform additional hashing/encryption operations (Blakeb/ChaCha20Poly1305). Once the Rust code is translated into another langage, we can decipher the flag. ``` from pwn import * from hashlib import blake2b import pysodium from chacha20poly1305 import ChaCha20Poly1305 import hashlib import time # conversion function from rust to python def convert(s): _=b"" for elt in s: _+=elt.to_bytes(1,"little") return _ # Store key as on rust file alice_private_key =convert([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]) bob_static_secret =convert([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]) alice_static_public =convert([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]) # Calculate alice public key alice_public_key = pysodium.crypto_scalarmult_curve25519_base(alice_private_key) #print(alice_public_key,alice_public_key.hex()) print("Alice",[i for i in alice_public_key]) time.sleep(1) # Do handshake wiht the server server = remote("mascarade.chall.malicecyber.com",4999) #server = remote("127.0.0.1",7878) print(server.recv()) server.sendline(b"Hello Bob!") print("Getting Bob") bob_public_key =server.recv(32).strip() print("Bob_public",[i for i in bob_public_key],len(bob_public_key)) server.send(alice_public_key) print("Getting Flag") flag =server.recvall().strip() print(flag,len(flag)) # Do Diffie Hellman ECC Calculation shared_ephemeral_secret = pysodium.crypto_scalarmult_curve25519(alice_private_key, bob_public_key) shared_static_secret = pysodium.crypto_scalarmult_curve25519(bob_static_secret, alice_static_public) shared_static_ephemeral_secret = pysodium.crypto_scalarmult_curve25519(bob_static_secret, alice_public_key) print([i for i in shared_ephemeral_secret]) sharedsecret = shared_ephemeral_secret +shared_static_secret +shared_static_ephemeral_secret # Do Blake2b hashing print(len(sharedsecret)) h = blake2b(sharedsecret).digest() g = blake2b() g.update(shared_ephemeral_secret) g.update(shared_static_secret) g.update(shared_ephemeral_secret) g =g.digest() key =h print([i for i in sharedsecret]) # Do ChaCha20Poly1305 cip = ChaCha20Poly1305(key[:32]) nonce = b"\x00"*12 plaintext = cip.decrypt(nonce, flag) print(plaintext) ``` and executing the script: ``` python3 test.py Alice [165, 154, 157, 189, 14, 3, 59, 231, 9, 61, 115, 119, 7, 84, 221, 178, 90, 71, 156, 128, 80, 190, 39, 152, 45, 165, 220, 160, 57, 50, 255, 9] [+] Opening connection to mascarade.chall.malicecyber.com on port 4999: Done b'Hello Alice!\n' Getting Bob Bob_public [50, 173, 174, 25, 209, 54, 43, 133, 36, 98, 102, 202, 1, 217, 34, 22, 194, 245, 159, 58, 236, 253, 223, 211, 182, 99, 76, 84, 220, 204, 71, 107] 32 Getting Flag [+] Receiving all data: Done (56B) [*] Closed connection to mascarade.chall.malicecyber.com port 4999 b'\xc7AVZ\x16u\'\xc4|\xe3"\xdf\xfe*t%d\xc3i<\xe0C\xab\x9e{\xeb\x1a\x96\x04\'B\x8dWy\xacP\xf7\xebr\x00XtG\x1bR\xf8M\x8d\x81\xd7\xc1\xb9\'\xea\xa5\xf6' 56 [117, 61, 160, 223, 16, 203, 209, 85, 30, 40, 20, 90, 242, 53, 122, 174, 91, 94, 94, 96, 213, 249, 213, 64, 13, 118, 212, 157, 104, 160, 214, 42] 96 [117, 61, 160, 223, 16, 203, 209, 85, 30, 40, 20, 90, 242, 53, 122, 174, 91, 94, 94, 96, 213, 249, 213, 64, 13, 118, 212, 157, 104, 160, 214, 42, 59, 139, 168, 244, 116, 60, 183, 239, 4, 6, 177, 231, 69, 203, 243, 35, 102, 54, 215, 51, 237, 15, 14, 94, 72, 240, 16, 145, 127, 4, 1, 112, 3, 148, 116, 44, 225, 84, 92, 116, 68, 2, 93, 70, 160, 181, 98, 222, 167, 67, 18, 72, 49, 121, 108, 61, 32, 80, 220, 237, 40, 62, 59, 115] bytearray(b'DGHACK{penurie_complete,penurie_basmati}') ``` ***DGHACK{penurie_complete,penurie_basmati}*** ## Hashsig ### Description ``` La DGA met à votre disposition un serveur sur lequel vous pouvez créer un compte et enregistrer des informations à mémoriser. Votre collègue a croisé l’administrateur du site au stand DGA lors de l’European Cyber Week. Il vous apprend que celui-ci utilise le serveur pour conserver des informations sensibles. Vous êtes intrigué, et une curiosité excessive vous tiraille : vous souhaitez en savoir plus. Vous demandez alors à votre collègue de vous décrire le personnage en question, et vous déployez des trésors d’ingénierie sociale pour boire un verre avec lui rue sainte Anne. L’occasion rêvée de déposer un logiciel espion dans son ordinateur personnel ! Au bout de quelques jours, vous récupérez les données ci-dessous, contenant la librairie client utilisée pour se connecter au serveur, le logiciel cryptographique utilisé pour s’authentifier, et un échantillon des échanges entre le portable de l’administrateur et le serveur. Note : pour résoudre ce challenge sous Windows, les messages de 1 octet n’étant pas acceptés avec la configuration native (NAT), vous devrez vous mettre en Bridge ou utiliser une machine Linux. ``` ### Hash_based ``` from hashlib import sha256 import secrets digest_size = 32 #bytes = 256 bits # must be doubled in hexa. # # Winternitz parameters: # w = 8 # one byte W = 256 #2^w l1 = 32 l2 = 2 lW = l1+l2 # # Encoding functions # def _int_from_bytes(xbytes: bytes) -> int: return int.from_bytes(xbytes, 'big') def _even_hex(x): y = hex(x)[2:] if (len(y)%2 == 1): y = "0"+y return y def _hex_length(x,l0): y = hex(x)[2:] while len(y)<l0: y = "0"+y return y def _signature_encoding(sign): acc = "" for s in sign: acc = acc+s return acc def _signature_decoding(sign_enc): sign = [] if len(sign_enc) == lW * digest_size * 2 : for i in range(lW): sign.append(sign_enc[2*i*digest_size:2*(i+1)*digest_size]) return True,sign else: return False,sign # # # Internal functions of the scheme # # def _sequence(digest): #digest must be a string representing a hex value #_sequence outputs a list of int seq = [] s = 0 for i in range(l1): x = int(digest[i:i+2],16) seq.append(x) s += x check = (l1*W - s) new_d = hex(check)[2:] while (len(new_d) < 2*l2): new_d = "0"+new_d for i in range(l2): x = int(digest[i:i+2],16) seq.append(x) return seq def _main(key,seq): # key is a list of hex strings # seq is a list of int res = [] for i in range(lW): x = bytes.fromhex(key[i]) e = seq[i] for j in range(e): x = sha256(x).digest() res.append(_hex_length(_int_from_bytes(x),2*digest_size)) return res def _keysequence(sk): #sk must be a hex string #returns a list of hex strings def key_derivation(sk,i): #returns a hex-string ski = sk + _even_hex(i) y = sha256(bytes.fromhex(ski)).hexdigest() while (len(y) < 2*digest_size): y = "0"+y return y return [key_derivation(sk,i) for i in range(lW)] def _get_key(pks): acc = "" for i in range(lW): acc = acc + pks[i] y= sha256(bytes.fromhex(acc)).hexdigest() while (len(y) < 2*digest_size) : y = "0" + y return y # # # Functions of the signature scheme # # def sign(sk,message): digest = sha256(message).hexdigest() seq = _sequence(digest) keys = _keysequence(sk) sign = _main(keys,seq) return _signature_encoding(sign) def verify(pk,message,sign_enc): formated,sign = _signature_decoding(sign_enc) if formated: digest = sha256(message).hexdigest() seq = _sequence(digest) true_seq = [(W - e) for e in seq ] pks = _main(sign,true_seq) return (pk == _get_key(pks)) else: return False def key_regen(sk): keys = _keysequence(sk) e_max = [W for i in range(lW)] pks = _main(keys,e_max) pk = _get_key(pks) return pk def key_gen(): x = secrets.randbits(8*digest_size) sk = _hex_length(x,2*digest_size) return (sk,key_regen(sk)) # # Nonce generation function # def nonce(): x = secrets.randbits(8*digest_size) return _hex_length(x,2*digest_size) # # # Test # #(sk,pk) = key_gen() #sign = sign(sk,b'toto') #print("message signed") #b = verify(pk,b'toto',sign) #if b: # print("verified") #else: # print("not verified") # #b = verify(pk,b'toro',sign) #if b: # print("verified") #else: # print("not verified") ``` ### client_lib ``` import socket import hash_based import sys #s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM,protocol=0) def add_user(login,pk): s = socket.socket() # Create a socket object host = socket.gethostname() # Get local machine name s.connect((host,port)) s.send(b'a') s.recv(1024) s.send(bytes(login,"utf-8")) s.recv(1024) s.send(bytes(pk,"utf-8")) r = s.recv(1024) if r == b'r': print("User already exists. Operation forbidden.") elif r == b'a': print("Success") s.close() def connect(login,sk): s = socket.socket() # Create a socket object host = socket.gethostname() # Get local machine name s.connect((host,port)) s.send(b'c') s.recv(1024) # should receive byte b'l' s.send(bytes(login,"utf-8")) chall_b = s.recv(1024) #receives the challenge # chall = chall_b.hex() sign = hash_based.sign(sk,chall_b) sign_b = bytes.fromhex(sign) s.send(sign_b) r = s.recv(1024) if r == b'r': print("Authentication failed. Connection refused.") elif r == b'm': print("Connection success.") return s def add_secret(s,sec): s.send(b'a') s.recv(1024) s.send(bytes(sec,"utf-8")) r = s.recv(1024) if r == b'm' : print("Secret added.") return "" def get_secrets(s): s.send(b'g') r = s.recv(1024).decode("utf-8") print(r) def stops_connection(s): s.send(b's') def close_connection(s): s.close() # Reserve a port for your service if len(sys.argv) < 3: host='localhost' port=12345 else: host=sys.argv[1] port=int(sys.argv[2]) #Usage example: #(sk,pk) = hash_based.key_gen() #add_user("username",pk) #s = connect("username",sk) #add_secret(s,"Super secret") #get_secrets(s) #s.close() # ``` ### Solution In this challenge, we are dealing with the ***Winternitz One Time Signature***, and we are provided a pcap file where we can observe several transaction with the server. Clearly, the objective is to re-use the knowledge acquired observing the transactions and to forge a signature. The Winternitz OTS is based on the iteration of hashing functions (sha256), onto 32x private values forming a private key. A public key is generated by hashing iteratively these 32 private values up to the 256 iteration. Based on a common protocol, to generate a one-time signature, the "prover" will generate a sha256 hash of its message, and provide for each of the 32x private values (i) their iterative hashes up according to 256-msg_hash(i). On the verification side, the server complete the iterative hashing up to 256 for each of the 32x partial public key. Then, the server compares with the known public and verify. ![](https://i.imgur.com/EwuSTKI.png) As we observe a **re-use** of the ***one time*** signature, we can find the lowest iteration and use those values until all the byte of the challenge are above the recovered iterations. It is important to mention in this challenge 34 values are used, and some strange things are done in the code, however see below the recovered values after analysis the transaction in the pcap file. ![](https://i.imgur.com/19Gzzbf.png) So let's just had a forge function in hash_based.py ``` # Added def forge(message,hashes,known_seq): digest = sha256(message).hexdigest() seq = _sequence(digest) #print(seq) #print(known_seq) _seq=[] for i,elt in enumerate(seq): _seq.append(elt-known_seq[i]) #print(min(_seq),_seq) if min(_seq) <0: return False,"" #print(_seq) sign = _main(hashes,_seq) return True,_signature_encoding(sign) ``` and running the following code to solve: ``` from pwn import * import hash_based import binascii from hashlib import sha256 challs =[ b"51dccd2a76757ed416c2475f630aac1b1ae0b0a754e196223fd13d68c3b8b5df", b"b66158d1852d75d6e62c90f0b43bd1ea519c3c36cbe9b60d9590bd041a014c62", b"8a5a253cad5b50a4f6d6820c89f6013a9ac7c9e0d02f90da337fe4c4a1373367", b"f8d742ecea0699d56a3599eaa87c68d1c2a0d117ad4ab1ff8afdb2bba99e810c", b"6c556a795e614569fb1b61acb500fe82965a8fab998306beee99e221e80cb4ff", ] proofs=[ b"de0fa2c1b4b49bce4a2a53b7301e2cb70239aeab336b7d7296e5d4cda2b63907102e960c65f207f1cd5a42aca37912f8ccb3e7a36cea0dce13c1aced5f7e69d10462a2599b1072a128137b07489c37dc8ebd859068a0f1ddbc5545fb212ee81f3899f0abc70fc8d03c82c16383be5bafe7ecab055245e6de46b7b8dee103bee470e15aab8998cb8c5d37e0565dd80e8cdde15d3d99bd058c50113ad32698f4f33642c555dd1bdc13415a693bcc2a9c1e1ed97039b1f31f4455b98fe8e652da0c473881072f5bb66c726633b0bcfd431d46bb2bbac3a1a53d49d431cd885a0c4898aae6dd18cccf45925e431efb4f48e1dd5ac21a17f8fbf88471b6f03c148c7995d25c7aa28f913fbbbdb177f0dede916dfecd882f044cecd1d06a98b4cd64c578cb6a38db84d72628e97765071b01d3c1aec047c13712dbde56f1f1ac996f863e2b42caa936109e3ef7e9a4b03734034f978439af9303662a8684427c190907639651a5538ffa13690d2be0916b038b84e4812b495a0f63ee8033bee58270a0b6df06f98a4e52c23a5d43801cc66d7d5ea3ccf64426b6a7ff22022123fddb0565747a429356b37195046034568c9a5b8326d0543cb6bca4505ad4067692640e9eb281aad18a99f13d4dc1dd37a61268cbdfb80ed81a9b3dca71e2070b18f5a544d4d101badaf9826a4448438bb0b97f547615095bfc4bd7591ca53bca411d1b46112a8254668e3af4d876c1e604e8dd637cab16da0c3b892dd6cbbb52529d5bdcdd99b10f02a1b2614c6ce90f754170cf0c7cd5d39c4e9c4a0dc7393643553c6d74b68cb0fe2a8266313aa3fed523d8fb4ed587d655b0ea4c28ec2d50d6893700829c5f157e09ce72ff73d512dfb83e99762c80c2e5a5b443cbf48120454fc081a9289fa801900818ef28c72f5650a399fbc59156127335886f9617e2c64ac4d210fd20b416aa2f01a839566aa1b95fbd17aef10142770b12114c554608226833a2cd0ea7217fc5231e4fd6c36f004e259cee8478cf20ded30dc2b2db63b0f91b57bac59cf2a1398360d6e1c99fbc2f5df9c3d7d9a40d179b6c37460a758605c6af8ce6ae01b2a159e3e069ac003b8156b37888626057ad1fa98bf4c343e707f452bf68340fd97db8b5e7312f4ebc2fa2ab55b354d5adeac2f0c3a87cb89e3e13b146f18b8874d537e176928b1eb6ebb7d7aeb075332527249a88e7954da9c17551a6c1f84af0eb821020b8d87f5c73a0b1d9ddde620a31de5380c25c9fdf5ea382ea5e4228d2ab8dd80505b773b18834aa32eb1f999007435f676da4b2e0ae01b3c0675fb2e5a3ec7bf91cf22be8031871fc8bcedaef9ba0cbf84d93a5d4e40891d6ac8be326bed6f72b9040e164e56be6d3566093c69498e4e8e78e895231959d9edb341874352b09d5484cd02d1b0a72530db516c474d802a7be2f67d1eb91d83aabec5e3df34c569e6db418e140f93604b8c9631f5303fd5de845bca8214f61b3f1170aa10d4fd17b791677114d1c07723ae8d9aaf7705a47f7d1133a32", b"0f892c0301604f83b943d3557c61057840960f1c72f73da69fd14d3baa029bd698ca338dd9adbbda1f5eeb5bba3825b7cba9fec876522213c3ee2885d146d24b3ac932a5425824d6bcf7f6579706b358146720e9a8f9a6703908e1c7c1e3c6e8ffcdad7bfd002ea9f3ba580849c6b7cdc892b7afed0c298c0c39203b8fbe438cf89fd866538e10003f4b078d56fd0ab3e0b4793066bfe1a038d682d91d936ad94801a749516d914b01ccb2b8d476ed19b2bee677e3d9f099c302608e3e1e59cda1702118237523aa50f8abdfd7a0812fc92e32c732e35398b0ec7789ca37afb1ab9bd416a4a4c72a7e621181ecb80447d194526427dd7ba5e597774adbbac106081868213ff226049937df44fc714c5b91a711a27f8e201a9772b748579408c34a182eb22b3d257115fae9a45ccc85b3d5af6e8ebeda8a6aa6dfb9228717d1a62b059014d7991ded41fdacefa4a3d6ff75b190cdf155161e8bccd4a72ebc4dfdb5d7f68d14ee0c40bc2e91ea00c9cfe5bfc4bc78f168fd1d1ee74ee58712117d73fb550f1f48bf1195f5036e11c7531c32a8107c035acd197f084dae4f66c48c8d5076962d991579901354cfe11577add54915696da1b73632f99ce6256481acb8abf2ec996c3245ed4589d3f28514bd9ab85e8c04666c4762693f51073b2df89b7fadfb22b21873e476e1be0db62068689d83a85e71b4ef48133c1d2a47b2ea499b5a3ddd4e6eadbb208066c9bba3171aacf1e107b4f699bb9f9dc9c02115350dc58223ec08722bf48e6bdb9615ef167e92ac68bf43efbe1f7914cb2310ce08def0aa66d8a70f3b6ff7c184d12f644232f6fa7fcd199a324e6ad9ad11b713030c8ad40869bfd46828f1b6839461f02c4892d608d01d3e83c75a5021429942dfd2cb41b96e835e122674fd86f9b88a1afbe153a444a9487841f26def568f99bb47e8a763e2a9f2e63be1a61833d28b7f8847f7ff219143468640e72060b01d0d45453307c98a5806f11a162740b1935000bcd8cefb7873a56a326c6116abd3e53fffb262c88536002115591f1511c73665fa9368991a9275488f221fdf844e2dab1fd27c49db8c7fbb72a34ff977ed71ea9b1f7bf38a569914a179ae067ba4d3c047ac725af02032500616018e2bb35f6b3a4f8cea8017449996cb2c044d241778a344ec1604aa39087f78671c41f020f69b8c9a1e6ad14d34c44d56285c6433d0e310ee720ba60fd6c4c0242ef01499b30a764f3dc4e50f1910a3b6209819dddc5d84b68434f421685b40f913ebecd4847f2a457279c5d19f7ac84f48a45e434a282cf778bd2a4cffe45ac97d60fac95a67acbd6bd46b8ac0eb97fb04f3471002153a8b39695ceb8c7a1bc5f7e53a2d4bc22505b1146ad453db4004860701e033df042fd8f30fda07bd0071c037d812f763945176defe70dd1634767c0e13b060c34ab3f9c988ae2cc4caaa5ef7a20761d29b0aa99b3baca26d913c550a8609fdd429b84e676ffed70f021d81821ccbe963bc626f4a530e04bef1b446e32615", b"859235209668766eaa014b00ebba6fded69404ecf70d668ffea316e86c7e0442d2f622b30e2a5e6e46756875888686c6cb924d5ef0e87252e1432746d17ec75476b867a85e23728f606b1b5edb6bbd44d27f34d7cac4e17c3aad514b7084be9ebc80f9a694ba7a3d1e275992821d579b415d117f1ee9efd08c22eef752ca9094dcba679ffa4ad8425ff08e99607ed1b0615569042e1b66cff13678b3264695d41ef34e07a68382f2c9dd1aa4d199fa0308d08512b7f99b78b031802ec9443d22ad5dce27e5398ee59ed6545353700197a076f09b90ff9723cc169a75438ec287cbf7c5d25d124e67a0ce24b777995663b44934de9705dca9f7fdc5da3ef2ffec8f0a0456b3c3d16686a5565a114a527dc815ff1c2bc7471c9299db1b921e89cfa051d85c0cf4c52e9dc71a98dd6ed2468f2feaa39f1b0e606bdb39a7e9350efd50f164a0a9a0a3d78c6701e01346a5c8748113d7677c51b0df14b7ace8c7485eb5d7f68d14ee0c40bc2e91ea00c9cfe5bfc4bc78f168fd1d1ee74ee58712117d79da08680cac547cf87dc3ff27931e68c7c60347a5d2273e40860ccba130b3fad071f41c6520e4396fbd9919dce274e90262475db045a0168ceaf531c3a609a141fe5c49c2d0128c84d8dd8bc9b533a2d0eec207cbbf733944cd98130800036dc34ecfcdd2cb26ca982a90e497e7a369cff1f60635e6254bd73ac25db7bd568099c3e56ef6dd3282b7548f65b208982de27d3cc8f2bb7e5ff04ce1d47f7630cdb2750d063e23e4f6bca5d70c03fa63a0e30ed8f8aace0e55cb9046c223cd3c81108a1dea0e7256c0013e5944aca8621da20fdd2b35bf9a770c40c742575cf00ac2626c1786bf06f506c73ebbbb809f6ad9e928922010963fe837aff4a794b07190e724f957aa19bb691139dd7484c36b4b874820c5bf64c4805e7f0aad37c41b86ce5a6a6bb046ee728d806af6dd7ec06b63b8d8c750e07f31fff745aa1d852da38290deecdfdd07ba69fc9ebb6e5a974980f0bafb2b40a84e477a2615f888f79c4afbede31c9f98c042fed8e53dae72f252bab739844f313ab096be711e9f1826eadcf52d558ab4b34695fdfbf71d8dad8655d38b5f6344a8f5a14bb0b45b19a74b18e3801af3bcf101b23df5c24167347024c55a4bfad2a53b11aa41442e73139be9b41d6d6b63e791d58bddd68ad2cd0bed00e3d364571c377620690bf06213ea2b3c114d255b9de4dffed31b96efb477d0a5058a6849f2edf6c424589687d05d2a8fefbdf72471d5f1f94c365db23f049e4a8b956448835cefa99744b67839ec27ca99f15c883482f74d83e096b9bd61ad16c1e4269e35174a9182386ca590750e591554d997d29a356e06ddcd333fcefc715bb00767d5343e635edc497f4cc3b448c822746410da03bc0e0a98dc110fcc0d76aea2671adc5fb5236fdd76bc37f10399e86ce431cfd0e7b13faaae68ff0cd9cc6ef6f048c3a8e22719665f779035dfff12c7064ef92143f7c626426027b2b31e2cfb37453fb69596212df8", b"7599049a79972b1f3c5f9cc30f67d09f6ebc14ef1e2283121d79bacdc4e5e54b87e2fb04bda41e434ce76ed613f2ff7f6cb29c96509bee15d2fe4d329a1f2559a6e8addd8c2b188a0b1191619d96f99d0e71d5642a7d548580948d5f9204070ae8a0b2aec7714e234c0df324f7e8c23fb7d58dad8c596dcabd2b83bd5ecf7ee3fea7a3324a033fd0edc085446af3884c13a2bc59b0fa683fa3f3e0eff40a2c9adf8aff3ef4513f0f1ec6cfd864a51f66518608818aa503e1445733887731a188e27f6643afd9515e574a9666a8b886d30b727a770e3c01bee0bb544c4cefd5f496bac3c70d6f2be538dbc0f899e55ada287c07eec8652dfacf6f7137f8c1c7240a62c9236bb9e59763d71a1d8c0038b0f8eeb250a1a0123c1c115a5ff4b2d9b6fc25d01134e5b11fb958eae7e4f7bd2dd8abe466b19768048bd34fca2e34acbd11d3ff2e4f27cd1a39dfed5e35a9daddada20e93dbbed7cda6965857951c63a38a6810fc7bbc474643db9ab7295191a0252b047ac01607b1c8379129e7becacca969d7b5a80c7a32dd6ddfa6382560b40888b1381782e45fb4b4c32307d850df4469ba2dece48e3b78892728f0197f1a898be482b55bc4c6047e7a9fad8cf94b7fcdd01a37b810426d4808fbaa7ecf562b9a796e88a9bf97be43f2ae66abb2ac71ccc13e1f98c5f020f4cf57d1e920085a1cba729ccef9acfea71dfb3f81da7d67ec027e0f45c27195ad9979e06685f4e2b06fea4cbd1132aa3d5b4528af7d0eb3ec6ed26594f87ab53ab61b131b1b6ced53f65c2788d79f66f37c616c4c0c3abdd52a4654359b047a4caa148285c775934c0dfd884f8ee7f21ca606ff0777cd2a26f799ecc1865359173075e451a76dd6461c5f2141ae83e9531d824c8ed216e80d6187f0138023b94ccef65caf8c948e344534b4069fece83d93582d7828bd5a0dc4d99009cbabd31c1e82cf79fca22ac7ebadbbe1d2b601d7178a34f99ac61bafe3f6e2b081e3a5a1726df441905638440d2872c1835fbfcaa0e5bbc0054fa25edcf1256bbb312fb0bbe419a85de29df69032859120ea444f396d2eed136ace3e34f5ca830576472048f7a2c709c9546429b5cc75ed259ee0697d5f175af5ae351b5ff3abdb4f71e78005313d680c831f7c8ff05c14cddc26d6fb4ad80a6720cf1cd1938fe71f6fd4bcc71cda2ed7cdf6b6b792f91d25c36b80acadcfb6436d46dd6ed413b60873a7cd7a23927d0ab656257b0dd134e4f812c146dcd17e68ec0ff78fceec49dbfb45db861a561c100678ca68be00b912df9df3680f216e91b25301a2f92ceff1282a85e0825a0f02d05384f5a7dbb8bc17fcbc4c96b8e67e6423abba2bc9643498deda344dd5b6865634d481e5e312e4d1a2489bebccadf6dd3bde12737925493c5493389a3e79e56495013cbf0aebc4d5f836134119ddc6580aa555892d0c93a64e571bd4f39c20d720fc7cabf04ca7653d6a83adbedb94df4ad46605bdbc172f417015fbd8cd6402f4ac3d7abf55222ac759fc3888854b", b"1322e6ac9c21bd93cce046b5ca1cd5832d8f71d3c2308fc5f64ddecd59b05d71ef93d7cfa83f4f73dc32b3bfe6004962cc1bdb69233d5cc3f8424ed4fcf6892fad6730aab296031505fd0d54ba05973fe917298e5de6def434399d3ea2ad6a97caf4570cfcf89c02af237da482f87ecdc2565af01b296612faced38cda3ff04022d4e66ddd08b7026c0ab12bc072dbbb7dc5dd850d14f19e3ee28c5fe481f2d48209b93911c309528d904177b43930e9dca48040417bd1d713bf8fdc6b46220c3ad1ef9f8c7f52aed0920886a129981d3d930135a01e473c838bfd47544e002e5816f1665327d4ffebc76ce6b8a81756227f1f4ed924df72b13edd3719df89ad461ee175ecaea51c47625c86b531bbd8225ddc8350677877264283bd4411f5b16e92835ef9163b821e6a354ece850310fe228d0464b029ac8c80717a5ac45a96a49e76187e88a825a51095c9e0803ed7f298632cb3559165c07d15cddc08461b432dde857fbb03933527e5ddc287c8248a16c80d2ac1bc1a0c0eda7e153b971acf7bf968d049a6c13004fde80d47915f5dbca092586cfcbfcee6f4dbd9075dad64e6b37d7ccfac7cd18c01a40a34ce711476e217e9b2d65923cbdcc9c70036844e9f2624af0eff20e0cc3a464ca8e080a1b85cb6bf305f24c9e9123bb3334e63ea0cd7abb4569ea1c217692bed35e4e8e423202b4290d103406a5833bfa561b429f15b05a29c67e3a8196038cd83565e1157be8b80ab4f1d0b38a15f019b5ecc71b516b2dddfc1f9caf333775dad678894d5bce227c7b39a1da514e6657f68247a01f1064a466b13b7aa58ec7dbeeaf7ae79abe789e9747af52898027dbb8e601f18d3624726828875b1ed714b2f04ba1f5448233f3813a7741b2c0226af16c7cbb5efa49c0b1649014afff05557c159e9cdbc873a349038129b2a31fa8210d64e5f1bdbe905d0dca4528b68be9b0c67bb88463982492f3991489e473844307617d16084e6bffb5180fe045be176391b47eeb30089373836f889982c13319718b6f378bab1d14e4ae0093905c1d0511ba8da616dd20b29c395eadc0f8eda92369898339ba916edc188dd8efbf2412ad53a996f9e5febf361bad73833cfe348b95ae1d4124ce5dc6a734446a45fbf23396e2559c6a06d50f286a01ee589476ca37c4b7abed1053e3d7fb29a234779d3024970b6060ffd6d76b44b3da0de9cc90da59ae3354b3bc7c7861849dcf095a6ccb9e4e4a4ff0d828650a8ba0b44924f52d6ca82bc46b9fd007bda66226544cf2826e984fb9f5475d25a65a6f045db7638c7012deb4f7af394ec388eb88d3da74329d3626d1ea75bd40edb106c9f9821edc9c5b65f8060b57b6b44e2e33ec31bf913b607e4408484cbf18c32628588c259b22c061a34334aea7cd17ae185289835a6833a3fb433961a6a4a194b4207cf7a6570ed9c9a57f9eeaad0570b889c3c9a82591d621859cb83b38b2df000ba43b3fea9113d2fbdec41460991864df26441381a87c56eba01bc007bc11b2f581698" ] def observe(challs,proofs): seqs=[] for elt in challs: digest = sha256(binascii.unhexlify(elt)).hexdigest() seq = hash_based._sequence(digest) seqs.append(seq) _ = [] hashes=[] for j in range(len(seqs[0])): _.append(257) hashes.append(0) for i in range(len(seqs)): if seqs[i][j] < _[-1]: _[-1]=seqs[i][j] hashes[-1]=proofs[i][j*32*2:(j+1)*32*2].decode() return _,hashes known_seq,hashes=observe(challs,proofs) print(known_seq,hashes) for i in range(len(challs)): forged = hash_based.forge(binascii.unhexlify(challs[i]),hashes,known_seq) assert forged[-1].encode() == proofs[i] print(forged[-1].encode()) print(proofs[i]) print("========") #print(len(proofs[0]),len(forged[-1])) #print(hash_based._sequence(sha256(b"toto").hexdigest())) print("......") print("Going Live....") context.log_level = 'error' i=1 while 1: io = remote("hashsig.chall.malicecyber.com",4998) io.send(b"c") #receive "l" io.recv(1) #print(bytes("admin","utf-8")) io.send(bytes("admin","utf-8")) chall = io.recv(timeout=1) #print("***",chall) print(i,end="\r") what,forged = hash_based.forge(chall,hashes,known_seq) #print(what,binascii.unhexlify(forged)) if what: io.send(binascii.unhexlify(forged)) io.send(b"g") #print(io.recvall()) io.interactive() input(">>>") else: io.close() del io i+=1 #io.interactive() ``` gives after 200 iterations (in average) ``` e33ec31bf913b607e4408484cbf18c32628588c259b22c061a34334aea7cd17ae185289835a6833a3fb433961a6a4a194b4207cf7a6570ed9c9a57f9eeaad0570b889c3c9a82591d621859cb83b38b2df000ba43b3fea9113d2fbdec41460991864df26441381a87c56eba01bc007bc11b2f581698' ======== ...... Going Live.... m[DGA{CrYpt0#_#7&}]$ ```