Writeup by erdnaxe for Lapin Blanc

hardware side channel attacks

November 5, 2023

Table of contents

Exploration

Let’s connect to the remote server using Netcat, we are greeted with the following prompt:

$ nc localhost 4000
[0000014054] Initializing Wonderland...
[0001326130] Searching for a tiny golden key...
[0001678450] Looking for a door...
[0001990520] Trying to unlock the door...

    __________________
    ||              ||
    ||   THE DOOR   ||
    ||              ||  .--------------------------.
    |)              ||  | What's the magic phrase? |
    ||              ||  /--------------------------'
    ||         ^_^  ||
    ||              ||
    |)              ||
    ||              ||
    ||              ||
____||____.----.____||_______________________________________

Answer: a
[0005255197] The door is thinking...
[0005258256] Your magic phrase is invalid, the door refuses to open.
Answer: b
[0006648825] The door is thinking...
[0006651889] Your magic phrase is invalid, the door refuses to open.
Answer: c
[0007208286] The door is thinking...
[0007211348] Your magic phrase is invalid, the door refuses to open.
Answer:

We observe that each received line is annotated with a timestamp in square brackets. This timestamp is precise down to the microseconds.

We also notice that each time we try an answer, two messages follow with a delay between.

Proposed solution

We implement a timing attack to recover the secret magic phrase.

As the answer seems to be “a magic phrase”, we may expect only printable characters. We use Python string.printable, but without \t\n\r\x0b\x0c characters which wouldn’t make sense in this situation.

Let’s measure the delay for the first character:

#!/usr/bin/env python3
import string
import socket

def recv_until(s: socket.socket) -> bytes:
    """Receive data until prompt"""
    rx = s.recv(1024)
    while not rx.endswith(b"Answer: "):
        rx += s.recv(1024)
    return rx

# Connect to remote server
s = socket.socket()
s.connect(("localhost", 4000))
recv_until(s)  # Skip intro

characters = string.printable[:-5]  # remove \t\n\r\x0b\x0c

# Measure timing for each hypothesis
hypo_timings = []
for i, c in enumerate(characters):
    # Try hypothesis 'c'
    print("Trying", c)
    s.send(c.encode() + b"\n")
    b = recv_until(s)
    lines = b.split(b"\n")
    start_t, end_t = int(lines[0][1:11]), int(lines[1][1:11])
    hypo_timings.append(end_t - start_t)

print(hypo_timings)

This script outputs:

[3058, 3061, 3059, 3059, 3059, 3062, 3061, 3057, 3058, 3060, 3059, 3061, 3060, 3061, 3060, 3064, 3059, 3060, 3060, 3059, 3066, 3058, 3058, 3057, 3058, 3060, 3059, 3059, 3063, 3059, 3060, 3059, 3058, 3061, 3057, 3059, 3059, 3062, 3059, 3060, 3072, 3059, 3056, 3060, 53116, 3055, 3058, 3069, 3059, 3059, 3057, 3060, 3060, 3060, 3059, 3059, 3060, 3061, 3062, 3061, 3061, 3062, 3060, 3060, 3059, 3060, 3061, 3061, 3060, 3059, 3062, 3060, 3060, 3060, 3061, 3059, 3061, 3059, 3061, 3061, 3073, 3060, 3060, 3061, 3059, 3060, 3060, 3063, 3059, 3061, 3059, 3063, 3059, 3058, 3059]

We observe a clear outlier for the character I (53116 » 3062). We make the hypothesis that the good character is always the one taking the longest to compute:

#!/usr/bin/env python3
import string
import socket

def recv_until(s: socket.socket) -> bytes:
    """Receive data until prompt"""
    rx = s.recv(1024)
    while not rx.endswith(b"Answer: "):
        rx += s.recv(1024)
    return rx

# Connect to remote server
s = socket.socket()
s.connect(("localhost", 4000))
recv_until(s)  # Skip intro

characters = string.printable[:-5]  # remove \t\n\r\x0b\x0c
magic_phrase_prefix = b""
while True:
    # Measure timing for each hypothesis
    hypo_timings = []
    for i, c in enumerate(characters):
        # Try known good prefix 'magic_phrase_prefix + hypothesis 'c'
        magic_phrase = magic_phrase_prefix + c.encode()
        print(magic_phrase.decode())

        # Measure timing
        s.send(magic_phrase + b"\n")
        b = recv_until(s)
        lines = b.split(b"\n")
        start_t, end_t = int(lines[0][1:11]), int(lines[1][1:11])
        hypo_timings.append(end_t - start_t)

    # Keep the hypothesis that caused the longest delay
    c = characters[hypo_timings.index(max(hypo_timings))].encode()
    magic_phrase_prefix += c

This script outputs the following before freezing:

[...]
I'm late, I'm late! For a very important dateS
I'm late, I'm late! For a very important dateT
I'm late, I'm late! For a very important dateU
I'm late, I'm late! For a very important dateV
I'm late, I'm late! For a very important dateW
I'm late, I'm late! For a very important dateX
I'm late, I'm late! For a very important dateY
I'm late, I'm late! For a very important dateZ
I'm late, I'm late! For a very important date!

We recognise a quote from Alice in Wonderland. We connect and try this magic phrase:

[0000014056] Initializing Wonderland...
[0001326133] Searching for a tiny golden key...
[0001678450] Looking for a door...
[0001990521] Trying to unlock the door...

    __________________
    ||              ||
    ||   THE DOOR   ||
    ||              ||  .--------------------------.
    |)              ||  | What's the magic phrase? |
    ||              ||  /--------------------------'
    ||         ^_^  ||
    ||              ||
    |)              ||
    ||              ||
    ||              ||
____||____.----.____||_______________________________________

Answer: I'm late, I'm late! For a very important date!
[0003733869] The door is thinking...
[0003922008] FCSC{t1m1Ng_1s_K3y_8u7_74K1nG_u00r_t1mE_is_NEce554rY}