Tornado Warning

Description: "Check out this alert that I received on a weather radio. Somebody transmitted a secret message via errors in the header! Fortunately, my radio corrected the errors and recovered the original data. But can you find out what the secret message says?\n\nNote: flag is not case sensitive."

For yet another signals challenge, this time we were given the file warning.wav, which seemed to contain a tornado alert broadcast. There were some free hints alongside the description:

  • The header is encoded with Specific Area Message Encoding.
  • The three buzzes are supposed to be identical, but in this challenge, they are different due to errors.

This already gave me some direction. First, I tried to understand something more about the message encoding: after searching for a while, I stumbled into the protocol’s Wikipedia page. From there I learned that SAME is a protocol used for broadcasting emergency warning messages, and such a message is composed by four parts, the first and last of which are digital and the middle two are audio. The first part is the header of the message, and can be heard as three buzzes with silence in between one another. Since the second hint pointed my attention towards such buzzes, I focused on understanding how a SAME header is formed. It gets transmitted as an AFSK data burst, and its structure is the following: <Preamble>ZCZC-ORG-EEE-PSSCCC+TTTT-JJJHHMM-LLLLLLLL- For the sake of the challenge we don’t really need to know what all these sections represent. What is important is the fact that the header gets transmitted thrice so that decoders can pick “best two out of three” for each byte, thereby eliminating most errors which can cause an activation to fail. This, alongside the description of the challenge and the second hint made me decide to try extracting data from each of the three buzzes, and then comparing them. After a (really) long time of trial and error (of which I will omit the details for keeping my and your sanity), I found this Rust crate, which seemingly permits to decode SAME messages. I then dived into its documentation, which eventually made me understand enough to write the following script:

use sameold::SameReceiverBuilder;
use hound::WavReader;
use std::fs::File;
use std::io::BufReader;

fn main() {
    let file = File::open("./warning.wav").unwrap();
    let reader = BufReader::new(file);
    let reader = WavReader::new(reader).unwrap();

    let mut rx = SameReceiverBuilder::new(44100).build();

    let samples: Vec<i32> = reader.into_samples().map(|s| s.unwrap()).collect();

    let audiosrc = samples.into_iter().map(|s| s as f32).collect::<Vec<f32>>();
    for msg in rx.iter_messages(audiosrc) {
        println!("msg: {:?}", msg);
    }
}

Executing this, I got the following output:

msg: StartOfMessage(MessageHeader { message: "ZCZC-WXR-TOR-018007+0045-0910115-KIND/NWS-", offset_time: 19, parity_error_count: 120, voting_byte_count: 42 })
msg: EndOfMessage

Which is nice, but not really what I’m looking for. After some more diving into the documentation, I found the iter_events() method, which outputs much more infos when used, compared to iter_messages(). I changed the code obtaining this:

use sameold::SameReceiverBuilder;
use hound::WavReader;
use std::fs::File;
use std::io::BufReader;

fn main() {
    let file = File::open("./warning.wav").unwrap();
    let reader = BufReader::new(file);
    let reader = WavReader::new(reader).unwrap();

    let mut rx = SameReceiverBuilder::new(44100).build();

    let samples: Vec<i32> = reader.into_samples().map(|s| s.unwrap()).collect();

    let audiosrc = samples.into_iter().map(|s| s as f32).collect::<Vec<f32>>();
    for msg in rx.iter_events(audiosrc) {
        println!("msg: {:?}", msg);
    }
}

This time I got this:

msg: SameEvent { what: Link(Searching), input_sample_counter: 2752 }
msg: SameEvent { what: Link(Reading), input_sample_counter: 15669 }
msg: SameEvent { what: Link(Burst([90, 67, 90, 67, 45, 85, 88, 85, 45, 84, 70, 82, 45, 82, 49, 56, 48, 48, 55, 83, 84, 95, 52, 53, 45, 48, 57, 49, 48, 66, 82, 53, 45, 75, 73, 78, 68, 51, 82, 87, 83, 45, 0, 0, 0])), input_sample_counter: 44059 }
msg: SameEvent { what: Transport(Assembling), input_sample_counter: 44059 }
msg: SameEvent { what: Link(NoCarrier), input_sample_counter: 44101 }
msg: SameEvent { what: Link(Searching), input_sample_counter: 86291 }
msg: SameEvent { what: Link(Reading), input_sample_counter: 99210 }
msg: SameEvent { what: Link(Burst([90, 67, 90, 67, 45, 87, 73, 82, 45, 84, 79, 123, 51, 48, 49, 56, 87, 48, 82, 43, 48, 48, 84, 53, 45, 48, 57, 85, 84, 49, 49, 53, 45, 75, 95, 69, 86, 47, 78, 87, 83, 45, 0, 0, 0])), input_sample_counter: 127594 }
msg: SameEvent { what: Link(NoCarrier), input_sample_counter: 127636 }
msg: SameEvent { what: Link(Searching), input_sample_counter: 169832 }
msg: SameEvent { what: Link(Reading), input_sample_counter: 182750 }
msg: SameEvent { what: Link(Burst([90, 67, 90, 67, 45, 87, 88, 82, 67, 84, 79, 82, 45, 48, 68, 95, 48, 48, 55, 43, 48, 48, 52, 79, 82, 95, 79, 49, 48, 49, 49, 69, 64, 75, 73, 78, 68, 47, 78, 125, 83, 45, 0, 0, 0])), input_sample_counter: 211137 }
msg: SameEvent { what: Link(NoCarrier), input_sample_counter: 211179 }
msg: SameEvent { what: Transport(Message(Ok(StartOfMessage(MessageHeader { message: "ZCZC-WXR-TOR-018007+0045-0910115-KIND/NWS-", offset_time: 19, parity_error_count: 120, voting_byte_count: 42 })))), input_sample_counter: 268894 }
msg: SameEvent { what: Transport(Assembling), input_sample_counter: 268979 }
msg: SameEvent { what: Transport(Idle), input_sample_counter: 689293 }
msg: SameEvent { what: Link(Searching), input_sample_counter: 3931839 }
msg: SameEvent { what: Link(Reading), input_sample_counter: 3944750 }
msg: SameEvent { what: Link(Burst([78, 78, 78, 78, 102, 102, 102])), input_sample_counter: 3947297 }
msg: SameEvent { what: Transport(Message(Ok(EndOfMessage))), input_sample_counter: 3947297 }
msg: SameEvent { what: Link(NoCarrier), input_sample_counter: 3947340 }
msg: SameEvent { what: Transport(Assembling), input_sample_counter: 3947340 }
msg: SameEvent { what: Link(Searching), input_sample_counter: 3989539 }
msg: SameEvent { what: Link(Reading), input_sample_counter: 4002454 }
msg: SameEvent { what: Link(Burst([78, 78, 78, 78, 102, 102, 102])), input_sample_counter: 4005003 }
msg: SameEvent { what: Link(NoCarrier), input_sample_counter: 4005045 }
msg: SameEvent { what: Link(Searching), input_sample_counter: 4055397 }
msg: SameEvent { what: Link(Reading), input_sample_counter: 4060152 }
msg: SameEvent { what: Link(Burst([78, 78, 78, 78, 102, 102, 102])), input_sample_counter: 4062692 }
msg: SameEvent { what: Link(NoCarrier), input_sample_counter: 4062735 }

This seems much more interesting! Indeed, if we ignore everything and we just focus on the three bursts near the top, it is easy to see that they carry the bytes composing the header. This is exactly what I was looking for. The only thing left is understanding what needs to be done with them. Since the challenge highlights the fact that the secret message lies within the errors, it seemed natural to compare the three bursts sequentially and choose the bytes in this way: for every triple (i,j,k) where every component is a byte of a burst, all taken at the same position, choose the bytes that is different from the other two if there is one, or choose randomly if they are all the same. I implemented this idea in the following script:

l1 = [90, 67, 90, 67, 45, 85, 88, 85, 45, 84, 70, 82, 45, 82, 49, 56, 48, 48, 55, 83, 84, 95, 52, 53, 45, 48, 57, 49, 48, 66, 82, 53, 45, 75, 73, 78, 68, 51, 82, 87, 83, 45, 0, 0, 0] 
l2 = [90, 67, 90, 67, 45, 87, 73, 82, 45, 84, 79, 123, 51, 48, 49, 56, 87, 48, 82, 43, 48, 48, 84, 53, 45, 48, 57, 85, 84, 49, 49, 53, 45, 75, 95, 69, 86, 47, 78, 87, 83, 45, 0, 0, 0] 
l3 = [90, 67, 90, 67, 45, 87, 88, 82, 67, 84, 79, 82, 45, 48, 68, 95, 48, 48, 55, 43, 48, 48, 52, 79, 82, 95, 79, 49, 48, 49, 49, 69, 64, 75, 73, 78, 68, 47, 78, 125, 83, 45, 0, 0, 0] 

for i,j,k in zip(l1,l2,l3):
    if i != j and i != k:
        print(chr(i), end="")
    elif j != i and j != k:
        print(chr(j), end="")
    elif k != i and k != j:
        print(chr(k), end="")
    else:
        print(chr(i), end="")

After executing it, we can see the flag at the center of the message: ZCZC-UIUCTF{3RD_W0RST_TOR_OUTBRE@K_EV3R}S-

Untrue

Thinking sand goes brrr | MS in Engineering in Computer Science @ Sapienza University of Rome | pwning w/ TRX & mhackeroni


2023-07-02