diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a5d40d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +.*.swp +venv + diff --git a/docs/lab3.md b/docs/lab3.md index 733581b..e406706 100644 --- a/docs/lab3.md +++ b/docs/lab3.md @@ -170,36 +170,42 @@ command. ## Problem 2: Timing side-channel attack -In this problem you will mount your own attack to extract a secret token from a "VeRY Secur3 SerVer". +In this problem you will mount your own attack to +extract a secret password from a server using an +insecure authentication scheme. -The code for this assignment is in the zip file -[`lab3-code.zip`](lab3/code.zip). +The code for this assignment is in [`https://github.com/mit-pdos/6.1600-labs/tree/main/time/problem2`](time/problem2). # Scenario -The scenario of this lab is simple. -You are now the attacker. +Bob runs a payments service that, after Bob +authenticates by sending a password to the server, +runs the `send_money` routine to process +a payment. (In this toy example, `send_money` is +a no-op.) -Bob is trying to save his favorite cryptocoin $ecret key on a server so he can easily verify if he is remembering it correctly. -To do this, Bob sends a request containing his key on a secure connection to the server. -The server replies with a bit that indicates whether the secret in the request matches the one on the server. +In a secure implementation, Bob's server would use +a robust off-the-shelf authenticated transport +protocol (SSH, TLS 1.3 with pre-shared keys, +etc.). But since Bob has not taken 6.1600 yet, he +cooked up his own scheme. -Even though the server does not authenticate the party making the request, Bob believes that he is safe as the API is very restricted: an attacker trying to guess the secret by querying the API learns no information other than whether it was correct. - -Prove him wrong! +Bob's server accepts requests from the network, +where each request contains a password. Bob's +server checks the request's password against +the true password and calls the `send_money` +function only if the passwords match. # More specifically -On initialization, a `SecureServer` instance generates a secret token using fresh randomness and saves it as a hexadecimal string i.e., one with characters from `0` to `9` or `a` to `f`. Note that you need **two** hexadecimal characters to represent one byte of data. +On initialization, a `BadServer` instance generates a secret password using fresh randomness and saves it as a hexadecimal string i.e., one with characters from `0` to `9` or `a` to `f`. Note that you need **two** hexadecimal characters to represent one byte of data. -The `SecureServer` allows any user to submit a `VerifyTokenRequest` with some token. +The `BadServer` allows any user to submit a `VerifyTokenRequest` with some password. The server responds with a `VerifyTokenResponse`, which contains a single boolean value. -This value is `True` if the token in the request matches the server's secret. +This value is `True` if the password in the request matches the server's secret. Otherwise, the value is `False`. -In addition to this correctness property, Bob claims that the server has the following security property: there is a negligible probability that an attacker can recover the secret token from the server in polynomial time (with respect to the length of the token). - -Unfortunately, implementation errors make it possible for you, the attacker, to violate this property. +Implementation errors make it possible for you, the attacker, to violate this property. In particular, software side channels (specifically, timing side channels) foil Bob's attempt to achieve this property. # Your job @@ -219,9 +225,5 @@ Finaly, note that you are expected to respect the python conventions and you sho For the rest, have fun and good luck! -# Submit lab 5 - -Upload your -[`problem2/attacker.py`](https://github.com/mit-pdos/6.1600-labs/tree/main/time/problem2/attacker.py) to Gradescope. diff --git a/time/problem2/api.py b/time/problem2/api.py index 075679b..901ce37 100644 --- a/time/problem2/api.py +++ b/time/problem2/api.py @@ -2,10 +2,10 @@ A common API between the client and the server. """ -class VerifyTokenRequest(): - def __init__(self, token: str): - self.token = token +class VerifyRequest(): + def __init__(self, password: str): + self.password = password -class VerifyTokenResponse(): +class VerifyResponse(): def __init__(self, ret: bool): self.ret = ret diff --git a/time/problem2/attacker.py b/time/problem2/attacker.py index 8fcaf93..6b7a091 100644 --- a/time/problem2/attacker.py +++ b/time/problem2/attacker.py @@ -1,23 +1,23 @@ -import secure_server +import bad_server import api import secrets from typing import Optional class Client: - def __init__(self, remote: secure_server.VerySecureServer): + def __init__(self, remote: bad_server.BadServer): self._remote = remote - def steal_secret_token(self, l: int) -> Optional[str]: - secret_token = secrets.token_hex(l) - req = api.VerifyTokenRequest(secret_token) - if self._remote.verify_token(req).ret: - return secret_token + def steal_password (self, l: int) -> Optional[str]: + password = secrets.token_hex(l) + req = api.VerifyRequest(password) + if self._remote.verify_password(req).ret: + return password else: return None if __name__ == "__main__": - token = '37a4e5bf847630173da7e6d19991bb8d' - nbytes = len(token) // 2 - server = secure_server.VerySecureServer(token) + passwd = '37a4e5bf847630173da7e6d19991bb8d' + nbytes = len(passwd) // 2 + server = bad_server.BadServer(passwd) alice = Client(server) print(alice.steal_secret_token(nbytes)) diff --git a/time/problem2/bad_server.py b/time/problem2/bad_server.py new file mode 100644 index 0000000..00db5df --- /dev/null +++ b/time/problem2/bad_server.py @@ -0,0 +1,34 @@ +import secrets +import api +from typing import Optional + +class BadServer: + def __init__(self, password: Optional[str] = None): + if password is None: + self._password = secrets.token_hex(256) + else: + self._password = password + + def send_money(self): + # This is where the protected code would run. + pass + + def verify_password(self, request: api.VerifyRequest) -> api.VerifyResponse: + try: + s = request.password + for i in range(len(self._password)): + if len(s) <= i: + return api.VerifyResponse(False) + elif s[i] != self._password[i]: + return api.VerifyResponse(False) + + # At this point, the user is authenticated. + self.send_money() + + return api.VerifyResponse(True) + except: + return api.VerifyResponse(False) + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/time/problem2/grade-lab.py b/time/problem2/grade-lab.py index 8696cbb..e0f0db9 100755 --- a/time/problem2/grade-lab.py +++ b/time/problem2/grade-lab.py @@ -1,4 +1,4 @@ -import secure_server +import bad_server import attacker import secrets import argparse @@ -13,9 +13,9 @@ def extract_bytes(n_bytes): secret = secrets.token_hex(n_bytes) - server = secure_server.VerySecureServer(secret) + server = bad_server.BadServer(secret) attack = attacker.Client(server) - res = attack.steal_secret_token(n_bytes) + res = attack.steal_password(n_bytes) return res == secret tests = [ diff --git a/time/problem2/secure_server.py b/time/problem2/secure_server.py deleted file mode 100644 index 855186f..0000000 --- a/time/problem2/secure_server.py +++ /dev/null @@ -1,26 +0,0 @@ -import secrets -import api -from typing import Optional - -class VerySecureServer: - def __init__(self, secret: Optional[str] = None): - if secret is None: - self._secret = secrets.token_hex(256) - else: - self._secret = secret - - def verify_token(self, request: api.VerifyTokenRequest) -> api.VerifyTokenResponse: - try: - s = request.token - for i in range(len(self._secret)): - if len(s) <= i: - return api.VerifyTokenResponse(False) - elif s[i] != self._secret[i]: - return api.VerifyTokenResponse(False) - return api.VerifyTokenResponse(True) - except: - return api.VerifyTokenResponse(False) - -if __name__ == "__main__": - import doctest - doctest.testmod()