FV25.20 - SantaOS Communications

Difficulty
medium

Categories
crypto forsensics

Description
Santa's IT team developed a special operating system used to process wishes securely.
In a recent incident, the key for decrypting messages got lost and all backups were destroyed.
The only thing left are the following two files - Can you help Santa?

Author
lu161
Flagvent 2025 - Day 20 - santaos-communications.tar.gz

Solution

Initially, we are given three files: intercepted_SantaOS.txt, intercepted_message.txt and santaOS_logo.jpeg.

Analysing the intercepted SantaOS

The intercepted_SantaOS.txt contains a Base64-encoded payload. Here’s some truncated output once decoded from Base64:

$timescale 10 us $end
$var wire 1 ! D0 $end
$var wire 1 " D1 $end
$var wire 1 # D2 $end
$var wire 1 $ D3 $end
$var wire 1 % D4 $end
$var wire 1 & D5 $end
$var wire 1 ' D6 $end
$var wire 1 ( D7 $end
$upscope $end
$enddefinitions $end
#0 0! 1" 0# 1$ 1% 1& 1' 1(
#415477 1! 1#
#480392 0!
#480396 1!
#586717 0#
#586759 1#
#586769 0#
#586790 1#
#586800 0#
#586811 1#
...

After some research, we discovered this is a VCD (Value Change Dump) file generated by a Verilog simulation.

A .vcd file is essentially a massive timeline of voltage changes. The file logs raw timestamps of voltage changes, which we mapped to characters using standard UART decoding logic. UART transmits the least significant bit (LSB) first. We collect 8 bits and reverse them to form a byte.

By examining the shortest pulses in the file (10 units apart), we determined a bit width of roughly 10.4 units. We also identified wire # as the active data line.

Here’s an example of decoding the first character (starting at time #586717):

  • Start Sequence (#586717 to #586759):
    • Line drops to 0 and stays there for 42 units.
    • Calculation: 42 / 10.4 ≈ 4 bits.
    • This covers the Start Bit plus Bits 0, 1, and 2.
    • Read: Start(0), 0, 0, 0
  • Bit 3 (#586759 to #586769):
    • Line rises to 1 for 10 units.
    • Calculation: 10 / 10.4 ≈ 1 bit.
    • Read: 1
  • Bits 4-5 (#586769 to #586790):
    • Line drops to 0 for 21 units.
    • Calculation: 21 / 10.4 ≈ 2 bits.
    • Read: 0, 0
  • Bit 6 (#586790 to #586800):
    • Line rises to 1 for 10 units.
    • Calculation: 10 / 10.4 ≈ 1 bit.
    • Read: 1
  • Bit 7 (#586800 to #586811):
    • Line drops to 0 for 11 units.
    • Calculation: 11 / 10.4 ≈ 1 bit.
    • Read: 0
  • Stop Bit (#586811):
    • Line rises to 1 (Idle/Stop state).

Raw Bits (LSB): 00010010
Reversed (MSB): 01001000 (0x48)

Which is the ASCII character H.

Processing the VCD dump via script

It’s time to write a script that can process the entire VCD dump and output the hidden message.

The script reads intercepted_SantaOS.txt (handling Base64 decoding), extracts the timestamped transitions for the target wire, and applies UART timing logic to reconstruct the ASCII string.

import base64
import sys

def solve_uart():
    print("[*] Parsing SantaOS VCD for UART data...")

    # 1. Load File
    try:
        with open('intercepted_SantaOS.txt', 'r') as f:
            vcd_content = f.read().strip()
        # Decode Base64 if needed, else use raw
        if "timescale" not in vcd_content: 
            vcd_text = base64.b64decode(vcd_content).decode('utf-8')
        else:
            vcd_text = vcd_content
    except Exception as e:
        print(f"[!] Error reading file: {e}")
        return

    # 2. Extract Transitions for Wire 2 (Symbol '#')
    transitions = []
    current_time = 0
    
    # Robust token parsing handles "#123 1#" on same line or separate lines
    tokens = vcd_text.replace('\n', ' ').split()
    
    for token in tokens:
        if token.startswith('#'):
            try:
                # Remove '#' and parse time
                current_time = int(token[1:])
            except ValueError:
                continue
        elif token.endswith('#'):
            # This is a value change for Wire D2
            try:
                val = int(token[:-1])
                transitions.append((current_time, val))
            except ValueError:
                continue

    if not transitions:
        print("[!] No transitions found. Check file format.")
        return

    print(f"[+] Found {len(transitions)} transitions. Decoding...")

    # 3. Decode UART
    # Parameters derived from trace analysis:
    # Smallest pulse is ~10 units. This is our bit period.
    bit_period = 10.4 
    
    decoded_chars = []
    i = 0
    
    # Iterate through transitions to find Start Bits
    while i < len(transitions) - 1:
        t_current, val = transitions[i]
        
        # UART Start Bit is a falling edge (1 -> 0)
        # We assume line is Idle High (1)
        if val == 0:
            # We found a start bit at t_current.
            # Sample the 8 data bits.
            # Center of first data bit = Start + 1.5 * period
            byte_val = 0
            
            for bit_idx in range(8):
                sample_time = t_current + (bit_period * 1.5) + (bit_period * bit_idx)
                
                # Determine state of line at sample_time
                # We scan forward from current transition
                sample_state = 1 # Default idle
                
                for k in range(i, len(transitions)):
                    t_k, v_k = transitions[k]
                    if t_k > sample_time:
                        # The state is whatever the line was *before* this future transition
                        # So we look at the previous transition's value
                        if k > 0:
                            sample_state = transitions[k-1][1]
                        break
                    # If we run out of transitions, the state remains the last known value
                    sample_state = v_k
                
                # UART sends LSB first
                if sample_state == 1:
                    byte_val |= (1 << bit_idx)
            
            decoded_chars.append(chr(byte_val))
            
            # Move index past this character to search for next start bit
            # Character takes ~10 bits (1 start + 8 data + 1 stop)
            end_of_char_time = t_current + (bit_period * 10)
            
            # Fast forward loop index
            while i < len(transitions) - 1 and transitions[i][0] < end_of_char_time:
                i += 1
        else:
            i += 1

    # 4. Print Result
    raw_key = "".join(decoded_chars)
    print("-" * 40)
    print(f"{raw_key}")
    print("-" * 40)

if __name__ == "__main__":
    solve_uart()

Running the script produces the following output:

[*] Parsing SantaOS VCD for UART data...
[+] Found 10720 transitions. Decoding...
----------------------------------------
Ho! Ho! Ho! Welcome to *SantaOS* - Version 2025.12.25
Command line: BOOT_IMAGE=/boot/vmlinuz-christmas root=UUID=fa1c0f83-a32d-4d4e-bfbf-d2369b17b32e ro quiet
Loading the sleigh... Please hold on while we gather the reindeer...
Kernel: Sleigh bells ring... are you listening?
Booting from the North Pole
ACPI: Reindeer fuel gauge check complete. All systems go!
BIOS: Powered by holiday magic! Christmas Eve 2025
Memory: 8GB of Christmas joy loaded and ready to go!
DMI: Reindeer Inc. SleighMaster 9000, BIOS Snowfall Edition 1.0
e820: Warming up the North Pole... All presents accounted for.
e820: [mem 0x0000000100000000-0x000000024fffffff] usable (Candy Cane Vault)
efi: ELF Loader: Powered by the Christmas Spirit
[Holiday Bug]: the boot CPU is running at full Christmas magic frequency.
Random init: random: twinkling lights initialized.
Random: Christmas cheer generated for all processes.
early console: Sleigh bells heard faintly in the distance...
Ho! Ho! Ho! Booting Christmas Kernel...
Loading the Reindeer Operating System from the North Poleâs initrd...
Freeing unused kernel memory: Wrapping paper discarded.
Write protecting the cookie storage: 8192 cookies saved.
Rudolph: Filesystem reindeer_root mounted successfully.
Starting systemd... The elves are busy assembling presents.
systemd[1]: Time synchronized with Santa's Workshop clock.
systemd[1]: Loaded Encryption Service - **AES-256 with CBC mode**
systemd[1]: Encryption Key (UPPERCASE): ><^v<<<^^v^^^>v>><^v<<<^^vvvv>v>
Starting the Holiday-themed GNOME Display Manager (gdm)...
gdm[456]: **Merry Christmas!** GNOME Display Manager started.
Login: Welcome to SantaOS! Type your secret holiday wish to begin. All messages are encrypted.

----------------------------------------

The output gives us some critical information about the encryption service: it uses AES-256 in CBC mode, with ><^v<<<^^v^^^>v>><^v<<<^^vvvv>v> as the encryption key.

Pigpen Cipher

Next, we look at the santaOS_logo.jpeg file:

Flagvent 2025 - Day 20 - santaOS_logo.jpeg

After some research, we figured out the image is hinting at the Pigpen cipher, a simple substitution cipher. The encryption key retrieved from the OS, ><^v<<<^^v^^^>v>><^v<<<^^vvvv>v>, also looks like it has been encoded using the Pigpen cipher.

We used the dCode.fr Pigpen Cipher Decoder and entered the input above in the UI to generate the following potential solutions:

#0	TUVSUUUVVSVVVTSTTUVSUUUVVSSSSTST
#1	TVUSVVVUUSUUUTSTTVUSVVVUUSSSSTST
#2	KLMJLLLMMJMMMKJKKLMJLLLMMJJJJKJK
#3	GHIFHHHIIFIIIGFGGHIFHHHIIFFFFGFG
#4	BCDACCCDDADDDBABBCDACCCDDAAAABAB
#5	UWYSWWWYYSYYYUSUUWYSWWWYYSSSSUSU
#6	UYWSYYYWWSWWWUSUUYWSYYYWWSSSSUSU
#7	--------------------------------
#8	-V-TVVV--T----T--V-TVVV--TTTT-T-

Decrypting the intercepted message

At this stage, we put the intercepted message from intercepted_message.txt into an intercepted_message.bin file, without any surrounding text:

U2FsdGVkX18IDMhwQMG9HpiCBLdo1SuQWoHNWRkzgaNDnDlEBruweUk70Bc/Kmds
DRFkbzUtkWOpG69ULK9Htp8LwINUfnwGQ3cMt/P2tnJfIeRUO0RDFryjUxZgUznL
2VT3wYZli3jWsiqAzEskyfHR8V5qGQjhJwnaV1vsfvxddogdukNBBA1UFYdXK9u+
D87DfWseLg33Zn4eGMy59GOrnB1rq2mXpLYHHKGDOQGpC7gwQ63RQEmlFki5CxS6
5ekYT7jCPG9XuuH+qP9WeHcj7Jjz6zxerz/7Jr+UaHco9h+sRcD0caWTFPu/OYfI
jMts0YZjosFOGTWPOw0Ly2ZlZlwPdgUmnKgG2F2j44YuuIVuBHVuGHoLBrz0mvjT
zcJxoE4wNxuGR3IFFbWDXIncbea/JYSGQ1ORuBIdXAGAlXfezJj+mYkf169nN6Jq
bWwpThzkGvexBMbMG6qAPqCkz3is0KU6QLs/gL+idkUJC96vfXsafMVJ9NACRjD3
HyUqqLJv8FG2sMwCGFZTXOvapHJxqizb7RzmRlv8lZ95oIu7ujqEMYb4p9SI2/MR
TZ+fU3tAPtx20/sFyWjJau0EL8BvQ1VjtTC/IJG6oe0NRlRU6b34vRxBa+AuARHx
GGSugTA0fp3X0pAeLb/R2g==

Base64-decoding the message reveals the Salted__ header, which indicates OpenSSL’s salted enc format.

Next, we attempted to decrypt the message using OpenSSL, making sure to use the -a flag so the input is Base64-decoded before being processed. We tried several different passphrases based on our Pigpen decoding, and TUVSUUUVVSVVVTSTTUVSUUUVVSSSSTST turned out to be the correct one.

openssl enc -d -aes-256-cbc -a -in intercepted_message.bin -k TUVSUUUVVSVVVTSTTUVSUUUVVSSSSTST

Running it gives:

*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
On a snowy night, so crisp and bright,
A challenge arose in the cold moonlight.
With hidden clues, through riddles and rhyme,
You searched for the flag, beyond space and time.

Through encrypted snowflakes and icy streams,
You followed the path of a hacker’s dreams.
In code and ciphers, you found the way,
To uncover the secret that lay in the fray.

Now at last, you’ve cracked the code tight,
And here’s your prize, shining in the night:

FV25{W3lc0m3_t0_S4nt4_OS}

This gives us our daily flag!

Flag:

FV25{W3lc0m3_t0_S4nt4_OS}

External discussions


Leave a comment

(required)(will not be published)(required)

Comments

There are no comments yet. Be the first to add one!