[GreHack 2023][Write Up – Crypto] The Fist Of The Hacker
I always forget my passwords, and passwords managers are insecure, so I decided to implement an authentication method using behavioral biometrics: I can authenticate myself using keystroke dynamics just by typing my public passphrase. Since keystroke dynamics are proper to each person, you’ll never be able to usurp my identity and recover my flag !
The sources attached to the challenge are used to connect to the server. Remplace localhost with 10.0.201.101 and 1234 with 20004 then run client.py.
Connection : nc 10.0.201.101 20004
Author : Olivier
Découverte
On se retrouve donc avec un client en python :
import json import socket from measure import Measure class Client: def __init__(self, host, port) -> None: self.__measure = Measure('Strong authentication') self.__host = host self.__port = port def authenticate(self) -> None: print('Authenticate with keystroke dynamics!') with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((self.__host, self.__port)) template = self.__measure.measure() serialized = template.serialize() sock.sendall(serialized + b'\n') data = sock.recv(1024) resp = json.loads(data) if 'error' in resp: print(f'An error occurred: {resp["error"]}') else: if resp['authenticated']: print('The flag is:', resp['flag']) else: print('Authentication failed') if __name__ == "__main__": client = Client('10.0.201.101', 20004) client.authenticate()
La classe Measure implémente la fonction measure qui permet de calculer une métrique à envoyer au serveur.
Cette fonction semble calculer des délais entre des appuis et relâchements de touches, mais en réalité une compréhension précise de ce que fait cette fonction n’est pas nécessaire pour flag.
La première chose à faire est évidemment de voir ce que le client envoie au serveur et ce que le serveur lui répond.
On va donc mettre des print et on obtient quelque chose du genre :
ENVOI : {"passphrase": "Strong authentication", "times": [0.1028287410736084, 0.17987561225891113, 0.10290122032165527, 0.08740973472595215, 0.05573439598083496, 0.12426257133483887, 0.08687615394592285, 0.13477826118469238, 0.0952138900756836, 0.08710360527038574, 0.0001575946807861328, 0.0868988037109375, 0.00011897087097167969, 0.09469747543334961, 0.10299205780029297, 0.07918906211853027, 0.0789339542388916, 0.09472036361694336, 0.10296082496643066, 0.023270368576049805, 0.04672122001647949, 0.10877394676208496, 0.9473681449890137, 0.951209306716919, 0.1866776943206787, 0.25628042221069336, 8.225440979003906e-05, 0.5041184425354004, 0.4807918071746826, 0.13070917129516602, 0.25031399726867676, 0.320664644241333, 0.1983351707458496, 0.2773408889770508, 0.27114319801330566, 0.08945155143737793, 0.14119696617126465, 0.12580084800720215, 0.12322163581848145, 0.01673436164855957, 0.1036074161529541, 0.18971800804138184]} RECEPTION : {"score": 2036.16557538910666, "authenticated": 0, "flag": "Nope"}
La passphrase est donnée, la seule chose à modifier pour récupérer le flag est donc le tableau de temps.
Résolution
On s’aperçoit assez vite en modifiant ce tableau qu’en y mettant des valeurs extravagantes, le score monte très vite, il doit donc s’agir d’une métrique calculant un taux de ressemblance avec un tableau de référence, notre objectif va donc être de modifier les valeurs du tableau une par une afin de faire baisser le score au maximum, jusque ce que la correspondance soit assez bonne et que le flag nous soit envoyé.
On ne sait cependant pas à quel point le score doit être bas pour arriver à s’authentifier, on va donc faire décimale par décimale, les valeurs semblent toutes êtres entre 0 et 1, pour chaque valeur, on va donc commencer à 0 et monter de 0.1 en 0.1 et garder celle qui correspond à un score minimum :
import pwn import json a = {"passphrase": "Strong authentication"} def test(c, d, times): times[c] = d a["times"] = times res = json.dumps(a).encode() print(f"RES : {res.decode()}") r = pwn.remote("10.0.201.101", 20004, level="error") r.sendline(res) out = r.recvline().decode()[:-1] print(f"OUT : {out}") score = float(json.loads(out)["score"]) print(f"SCORE : {score}") r.close() return score if __name__ == "__main__": times = 42 * [0.0] for i in range(42): last_score = test(i, 0.0, times) for j in range(1, 11): score = test(i, j / 10, times) if score > last_score: best = (j - 1) / 10 break else: last_score = score times[i] = best
Avec cette méthode, on arrive bien avec un tableau qui correspond au mieux que nous puissions faire en affinant au dixième :
[0.1, 0.1, 0.0, 0.1, 0.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.0, 0.1, 0.1, 0.1, 0.1, 0.0, 0.0, 0.0, 0.1, 0.1, 0.1, 0.2, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.1, 0.1, 0.1, 0.0, 0.1, 0.1, 0.2, 0.0, 0.0, 0.1]
Mais ce n’est pas suffisant, on va donc passer aux millièmes et refaire la même chose en repartant du tableau ainsi obtenu.
À noter que les valeurs obtenues sont optimales au dixième près, pour une valeur de i, on va donc tout tester de i-0.1 à i+0.1, en allant de 0.01 en 0.01 (sauf quand i est 0 étant donné que les valeurs doivent être positives).
if __name__ == "__main__": times = [0.1, 0.1, 0.0, 0.1, 0.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.0, 0.1, 0.1, 0.1, 0.1, 0.0, 0.0, 0.0, 0.1, 0.1, 0.1, 0.2, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.1, 0.1, 0.1, 0.0, 0.1, 0.1, 0.2, 0.0, 0.0, 0.1] for i in range(42): current = times[i] if current == 0.0: last_score = test(i, current, times) for j in range(1, 11): score = test(i, current + j / 100, times) if score > last_score: best = current + ((j - 1) / 100) break else: last_score = score times[i] = best else: last_score = test(i, current - 0.1, times) for j in range(1, 21): score = test(i, (current - 0.1) + (j / 100), times) if score > last_score: best = (current - 0.1) + ((j - 1) / 100) break else: last_score = score times[i] = best
Il se trouve qu’en étant précis au millième sur une bonne partie des valeurs suffit à avoir un score inférieur à 500, ce qui permet donc de s’authentifier et de flag :
RES : {"passphrase": "Strong authentication", "times": [0.07, 0.12, 0.05, 0.07, 0.05, 0.08, 0.07, 0.07, 0.08, 0.07, 0.07, 0.07, 0.02, 0.07, 0.07, 0.05, 0.07, 0.03, 0.02, 0.03, 0.04, 0.1, 0.1, 0.2, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.1, 0.1, 0.1, 0.0, 0.1, 0.1, 0.2, 0.0, 0.0, 0.1]} OUT : {"score": 490.16557538910666, "authenticated": 1, "flag": "GH{7yp1ng_15_4ll_y0u_n33d}"}