Cryptopals zestaw 5 ćwiczenie 38

przez | 1 listopada 2021

W tym ćwiczeniu naszym celem jest atak na uproszczoną implementację SRP. Będziemy występowali w roli „man in the middle”, ale dla uproszczenia nie będę w tym wypadku implementował rozwiązania z udziałem trzech stron (klient <-> atakujący <-> serwer) i ograniczę się do komunikacji klient <-> atakujący.

Miałem spory problem z tym zadaniem ponieważ na początku nie bardzo rozumiałem o co w ogóle w nim chodzi tzn. na czym polega trudność. Ponieważ pierwsze rozwiązanie jakie przychodziło mi do głowy to implementacja typu „brute force” czyli generowanie haseł z wszystkich możliwych kombinacji znaków aż do skutku, ale zacznijmy od początku.

Nasze zadanie jest proste, musimy zasymulować sytuację kiedy atakujący podszywa się pod serwer którym klient chce się połączyć a następnie odgadnąć jakie jest hasło użytkownika. Komunikacją Klient <-> Serwer wygląda następująco:

S

x = SHA256(salt|password)     v = g**x % n

C->S

 I, A = g**a % n

S->C

salt, B = g**b % n, u = 128 bitowa liczba losowa

C

x = SHA256(salt|password)     
S = B**(a + ux) % n     
K = SHA256(S)

S

S = (A * v ** u)**b % n     K = SHA256(S)

C->S

HMAC-SHA256(K, salt)

S->C

"OK" jeżeli HMAC-SHA256(K, salt) ma prawidłową wartość

W związku z tym, że podszywamy się pod serwer nie znamy wartości x z kroku pierwszego, to nad czym mamy kontrole to wartości b,B oraz u a także wartość salt. Znajomość tych wartości jest wystarczająca, żeby odgadnąć hasło. Tak jak już pisałem na wstępie miałem duże wątpliwości czy prawidłowo rozwiązałem to zadanie, ponieważ rozwiązanie wydaje się prymitywne. Moje rozwiązanie poniżej:

def server38(config):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_ma:
        socket_ma.bind((config['host'], config['port']))
        socket_ma.listen()
        conn, addr = socket_ma.accept()
        with conn:
            print("Connected: ", addr)
            data = wait_and_recv_data(conn, 4*1024)
            chars_array = [chr(x) for x in range(ord('a'), ord('z')+1)]
            chars_array = chars_array + [chr(x) for x in range(ord('A'), ord('Z')+1)]
            SALT = random.randrange(1, config['N'])
            b = random.randrange(1, config['N'])
            B = power_mod(config['g'], b, config['N'])
            u = random.randrange(1, config['N'])
            json_data = json.loads(data)
            A = json_data['A']
            resp = json.dumps({
                'salt': SALT,
                'B': B,
                'u': u
            }).encode()
            conn.sendall(resp)
            sleep(1)
            data = wait_and_recv_data(conn, 4*1024)

            forged = []
            got_password = False
            while (got_password == False and len(forged) <=16):
                if len(forged) == 0:
                    forged = [0]
                else:
                    idx = 0
                    while idx < len(forged):
                        forged[idx] = forged[idx]+1
                        if forged[idx] >= len(chars_array):
                            forged[idx] = 0
                            if idx == (len(forged)-1):
                                forged.append(0)
                                break
                        else:
                            break
                        idx = idx + 1
                print("Forged: ", forged)
                tmp_pass = "".join([chars_array[x] for x in forged])
                print("Checking password: ", tmp_pass)
                data_to_hash = str(SALT)+tmp_pass
                xH = hashlib.sha256(data_to_hash.encode())
                x = int(xH.hexdigest(), 16)
                v = power_mod(config['g'], x, config['N'])
                S = power_mod(A*power_mod(v, u, config['N']), b, config['N'])
                K = hashlib.sha256(str(S).encode())
                hmac_obj = hmac.new(K.digest(), str(SALT).encode(), hashlib.sha256)
                tmp_hmac = hmac_obj.digest()
                if tmp_hmac == data:
                    print("Valid password: ", tmp_pass)
                    got_password = True

            data_to_hash = str(SALT)+config['P']
            xH = hashlib.sha256(data_to_hash.encode())
            x = int(xH.hexdigest(), 16)
            v = power_mod(config['g'], x, config['N'])
            S = power_mod(A*power_mod(v, u, config['N']), b, config['N'])
            K = hashlib.sha256(str(S).encode())
            hmac_obj = hmac.new(K.digest(), str(SALT).encode(), hashlib.sha256)
            valid_hmac = hmac_obj.digest()
            if (valid_hmac == data):
                conn.sendall(b'OK')
            else:
                conn.sendall(b'FAIL')

def s5challenge38():
    print("Challenge 38")
    config = {
        'N': 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff,
        'g': 2,
        'k': 3,
        'I': '[email protected]',
        'P': 'abc',
        'host': '127.0.0.1',
        'port': 40038
    }
    server_handle = threading.Thread(target=server38, args=(config,))
    server_handle.start()
    sleep(1)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_a:
        socket_a.connect((config["host"], config["port"]))
        # Send I, A=g**a % N (a la Diffie Hellman)
        a = random.randrange(1, config["N"])
        A = power_mod(config["g"], a, config["N"])
        session_init_data = json.dumps(
            {
                "I": config['I'],
                "A": A
            }
        ).encode()
        socket_a.sendall(session_init_data)
        resp = wait_and_recv_data(socket_a, 4096)
        json_data = json.loads(resp)
        B = json_data['B']
        SALT = json_data['salt']
        u = json_data['u']
        print("CLIENT::Pasword: ", config['P'])
        data_to_hash = str(SALT)+config['P']
        print("CLIENT::data_to_hash: ", data_to_hash)
        xH = hashlib.sha256(data_to_hash.encode())
        print("CLIENT::digest: ", xH.hexdigest())
        x = int(xH.hexdigest(), 16)
        S = power_mod(B, a+u*x, config['N'])
        K = hashlib.sha256(str(S).encode())
        hmac_obj = hmac.new(K.digest(), str(SALT).encode(), hashlib.sha256)
        socket_a.sendall(hmac_obj.digest())
        resp = wait_and_recv_data(socket_a, 4096)
        print("CLIENT::Srv response: ", resp)

    server_handle.join()

W związku tym, że wiemy w jaki sposób klient wylicza wartość HMAC, które jest przesyłana na serwer, jesteśmy w stanie spróbować podstawiać różne wartości hasła i sprawdzać czy HMAC, który powstanie przy użyciu danego hasła będzie taka sama jak ten otrzymany od klienta. Na potrzeby ćwiczenia założyłem, że hasło może się składać jedynie z małych i wielkich liter od a do z i nie może być dłuższe nić 16 znaków. Pierwszy test wykonałem dla hasła jednoliterowego a następnie sprawdzałem coraz dłuższe hasła. I wtedy zrozumiałem (a przynajmniej tak mi się zdaje) o co naprawdę chodzi w tym zadaniu. O ile hasło składające się z jednego znaku można bardzo szybko złamać to już przy trzech znakach (i to tylko literach) ten czas już znacznie się wydłuża. Dostatecznie długie i skomplikowane hasło zajęło by bardzo dużo czasu zanim zostało by złamane. Proces łamania hasła można przyśpieszyć i w pierwszej kolejności sprawdzać popularne hasła (https://en.wikipedia.org/wiki/List_of_the_most_common_passwords), jeżeli użytkownik korzysta z tego typu haseł (a istnienie listy najpopularniejszych haseł dowodzi, że tacy użytkownicy istnieją) to zadanie staje się znacznie prostsze, ponieważ w pierwszej kolejności sprawdzamy „pewniaki”. Moim zdaniem właśnie to było celem tego zadania – słabe hasła można łatwo złamać nawet kiedy są przepuszczone przez funkcję skrótu.

Całość dostępna w repozytorium: https://gitlab.com/akoltys/cryptopals.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *