diff --git a/lab2/README.md b/lab2/README.md index b00c268..3192c84 100644 --- a/lab2/README.md +++ b/lab2/README.md @@ -71,6 +71,8 @@ confaccess PRACTICE2_DIR=/tmp/practice2 confaccess ``` +Режим проверки учётных данных (для bruteforce): `confaccess --check ` — читает пароли построчно из stdin, выводит 0 или 1 на каждую строку, exit 0 при первом совпадении. + После аутентификации доступны команды: ``` @@ -93,7 +95,7 @@ bruteforce alice bruteforce alice --max-length 4 ``` -Алгоритм хэширования: **SHA-256**. -Перебор выполняется напрямую по хэшу из passwd-файла. +Перебор выполняется через утилиту access (confaccess): bruteforce не имеет доступа к passwd-файлу и проверяет пароли только через `confaccess --check`. +Алгоритм хэширования в access: **SHA-256**. Фиксируется время перебора и количество итераций до нахождения пароля. Перебор останавливается автоматически при достижении лимита 8 часов. diff --git a/lab2/access.py b/lab2/access.py index 2f480d5..1c483e6 100644 --- a/lab2/access.py +++ b/lab2/access.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import argparse import getpass import hashlib import shutil @@ -195,7 +196,40 @@ def cmd_remove(args: list[str], login: str, perms: str) -> None: print("Done.") +def check_credentials(login: str, password: str) -> bool: + """Check login+password against passwd. Used by --check mode.""" + users = read_users() + user = users.get(login) + if user and user["password_hash"] == hash_password(password): + return True + return False + + def main() -> None: + parser = argparse.ArgumentParser(description="Access confidential data") + parser.add_argument( + "--check", + metavar="LOGIN", + help="Batch mode: read passwords line-by-line from stdin, output 0 or 1 per line; exit 0 on first match", + ) + args, _ = parser.parse_known_args() + + if args.check is not None: + # --check: one process, many checks; read_users once + users = read_users() + user = users.get(args.check) + target_hash = user["password_hash"] if user else None + + for line in sys.stdin: + password = line.rstrip("\n") + if target_hash and hash_password(password) == target_hash: + sys.stdout.write("1\n") + sys.stdout.flush() + sys.exit(0) + sys.stdout.write("0\n") + sys.stdout.flush() + sys.exit(1) + signal.signal(signal.SIGINT, lambda _s, _f: (print("\nBye."), sys.exit(0))) login, user = authenticate() diff --git a/lab2/bruteforce.py b/lab2/bruteforce.py index 3c1e218..4145e8c 100644 --- a/lab2/bruteforce.py +++ b/lab2/bruteforce.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 import argparse -import hashlib import itertools +import shutil import string +import subprocess import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) -from config import PASSWD_FILE +from config import BIN_DIR CHARSET = string.ascii_letters + string.digits + "!@#$%^&*()" FIRST_CHARS = string.ascii_letters @@ -17,19 +18,24 @@ FIRST_CHARS = string.ascii_letters MAX_HOURS = 8 -def hash_password(password: str) -> str: - return hashlib.sha256(password.encode("ascii")).hexdigest() - - -def get_target_hash(login: str) -> str | None: - if not PASSWD_FILE.exists(): - return None - with open(PASSWD_FILE) as f: - for line in f: - parts = line.strip().split(":", 4) - if len(parts) == 5 and parts[0] == login: - return parts[1] - return None +def get_access_cmd() -> list[str]: + """Resolve path to access utility (confaccess).""" + # setup.sh installs as confaccess; pyproject exposes as access + for name in ("confaccess", "access"): + path = shutil.which(name) + if path: + return [path] + # Fallback: same directory as bruteforce (development) + script_dir = Path(__file__).resolve().parent + access_script = script_dir / "access.py" + if access_script.exists(): + return [sys.executable, str(access_script)] + # Try bin dir from config (e.g. /usr/local/practice2/bin/confaccess) + for name in ("confaccess", "access"): + bin_cmd = BIN_DIR / name + if bin_cmd.exists(): + return [str(bin_cmd)] + return ["confaccess"] # hope it's in PATH def max_combinations(length: int) -> int: @@ -38,14 +44,31 @@ def max_combinations(length: int) -> int: return len(FIRST_CHARS) * (len(CHARSET) ** (length - 1)) -def brute_force_length(target_hash: str, length: int) -> tuple[str, int, float] | None: +def brute_force_length( + login: str, length: int, proc: subprocess.Popen[str] +) -> tuple[str, int, float] | None: + """Try all passwords of given length via batch process. proc = access --check.""" count = 0 start = time.perf_counter() + stdin = proc.stdin + stdout = proc.stdout + assert stdin is not None and stdout is not None + + def check(password: str) -> bool: + nonlocal count + count += 1 + stdin.write(password + "\n") + stdin.flush() + line = stdout.readline() + if not line: + return False + return line.strip() == "1" if length == 1: for first in FIRST_CHARS: - count += 1 - if hash_password(first) == target_hash: + if time.perf_counter() - start > MAX_HOURS * 3600: + return None + if check(first): return first, count, time.perf_counter() - start return None @@ -53,16 +76,17 @@ def brute_force_length(target_hash: str, length: int) -> tuple[str, int, float] for rest in itertools.product(CHARSET, repeat=length - 1): if time.perf_counter() - start > MAX_HOURS * 3600: return None - count += 1 password = first + "".join(rest) - if hash_password(password) == target_hash: + if check(password): return password, count, time.perf_counter() - start return None def main() -> None: - parser = argparse.ArgumentParser(description="Brute force password cracker (SHA-256)") + parser = argparse.ArgumentParser( + description="Brute force password cracker (via access utility)" + ) parser.add_argument("login", help="Target username") parser.add_argument( "--max-length", @@ -72,34 +96,45 @@ def main() -> None: ) args = parser.parse_args() - target_hash = get_target_hash(args.login) - if not target_hash: - print(f"User '{args.login}' not found in passwd file.") - return + cmd = get_access_cmd() + ["--check", args.login] + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + text=True, + bufsize=1, + ) + try: + print(f"Target: {args.login}") + print(f"Charset size: {len(CHARSET)} ({len(FIRST_CHARS)} valid for first char)") + print() - print(f"Target: {args.login}") - print(f"Hash: {target_hash}") - print(f"Charset size: {len(CHARSET)} ({len(FIRST_CHARS)} valid for first char)") - print(f"Algorithm: SHA-256") - print() + total_start = time.perf_counter() + for length in range(1, args.max_length + 1): + total = max_combinations(length) + print(f"Length {length}: max {total:>15,} combinations") - for length in range(1, args.max_length + 1): - total = max_combinations(length) - print(f"Length {length}: max {total:>15,} combinations") + result = brute_force_length(args.login, length, proc) - result = brute_force_length(target_hash, length) + if result is not None: + password, count, elapsed = result + total_elapsed = time.perf_counter() - total_start + print(f" >>> FOUND: '{password}'") + print(f" Iterations: {count:,}") + print(f" Time (len): {elapsed:.4f}s") + print(f" Time (total): {total_elapsed:.4f}s") + print(f" Speed: {count / elapsed:,.0f} attempts/s") + return + else: + print(f" Not found at length {length} (timeout or exhausted)") - if result is not None: - password, count, elapsed = result - print(f" >>> FOUND: '{password}'") - print(f" Iterations: {count:,}") - print(f" Time: {elapsed:.4f}s") - print(f" Speed: {count / elapsed:,.0f} hashes/s") - return - else: - print(f" Not found at length {length} (timeout or exhausted)") - - print(f"\nPassword not found within length {args.max_length}.") + print(f"\nPassword not found within length {args.max_length}.") + finally: + if proc.stdin: + proc.stdin.close() + if proc.poll() is None: + proc.terminate() + proc.wait() if __name__ == "__main__": diff --git a/lab2/lab2.md b/lab2/lab2.md index 7e8df90..3b91927 100644 --- a/lab2/lab2.md +++ b/lab2/lab2.md @@ -275,3 +275,10 @@ **Go:** * пакет `hash` + + +--- + +Примечание: + +Brute Force должен запускать утилиту из-под себя и через неё пытаться подобрать пароль, он не должен лезть в файл напрямую. \ No newline at end of file diff --git a/report/img/lab2-bruteforce.png b/report/img/lab2-bruteforce.png index f26ee22..0186b9d 100644 Binary files a/report/img/lab2-bruteforce.png and b/report/img/lab2-bruteforce.png differ diff --git a/report/report.tex b/report/report.tex index 97ab6b8..275b386 100755 --- a/report/report.tex +++ b/report/report.tex @@ -400,9 +400,9 @@ A: C (Availability Impact: Complete) — полное нарушение дос \subsection{Описание реализации} -Утилита управления пользователями (usermgr) предоставляет следующие подкоманды: \texttt{add} — добавление пользователя с интерактивным вводом ФИО, прав доступа, пароля и его подтверждения; \texttt{edit} — редактирование ФИО и прав доступа существующего пользователя; \texttt{passwd} — изменение пароля; \texttt{delete} — удаление пользователя; \texttt{list} — вывод списка всех пользователей. Пароль хранится в виде SHA-256-хэша. Все операции с файлом паролей регистрируются в журнале \texttt{log/usermgr.log}. +Утилита управления пользователями (usermgr, исходный код в приложении 1) предоставляет следующие подкоманды: \texttt{add} — добавление пользователя с интерактивным вводом ФИО, прав доступа, пароля и его подтверждения; \texttt{edit} — редактирование ФИО и прав доступа существующего пользователя; \texttt{passwd} — изменение пароля; \texttt{delete} — удаление пользователя; \texttt{list} — вывод списка всех пользователей. Пароль хранится в виде SHA-256-хэша. Все операции с файлом паролей регистрируются в журнале \texttt{log/usermgr.log}. -Утилита доступа к конфиденциальным данным (confaccess) при запуске запрашивает логин и пароль. При успешной аутентификации выводится приветствие <<Привет, <ФИО>>> и справка по доступным командам. При вводе неверных данных запрос повторяется. Завершение работы происходит по команде \texttt{exit} или сигналу SIGINT (Ctrl+C). Все попытки входа и действия с конфиденциальными данными регистрируются в журнале \texttt{log/access.log}. +Утилита доступа к конфиденциальным данным (confaccess, исходный код в приложении 2) при запуске запрашивает логин и пароль. При успешной аутентификации выводится приветствие <<Привет, <ФИО>>> и справка по доступным командам. При вводе неверных данных запрос повторяется. Завершение работы происходит по команде \texttt{exit} или сигналу SIGINT (Ctrl+C). Все попытки входа и действия с конфиденциальными данными регистрируются в журнале \texttt{log/access.log}. Для неинтерактивной проверки учётных данных предусмотрен режим \texttt{--check <логин>}: утилита читает пароли построчно из stdin и выводит 0 или 1 на каждую строку; при совпадении завершает работу с кодом 0. Поддерживаемые команды приведены в таблице~\ref{tab:commands}. @@ -427,11 +427,11 @@ A: C (Availability Impact: Complete) — полное нарушение дос Копирование разрешено только в каталог \texttt{confdata} или внутри него. Копирование из \texttt{confdata} в другие каталоги и перезапись существующих файлов запрещены. -Программа взлома паролей (bruteforce) выполняет последовательный перебор паролей начиная с длины 1, проверяя все допустимые комбинации символов. Для каждой комбинации вычисляется SHA-256-хэш и сравнивается с целевым хэшем из файла паролей. При нахождении совпадения фиксируются найденный пароль, количество итераций и затраченное время. Перебор прекращается при обнаружении пароля, достижении заданной максимальной длины или истечении восьмичасового лимита. +Программа взлома паролей (bruteforce, исходный код в приложении 3) не имеет доступа к файлу паролей и выполняет перебор исключительно через утилиту confaccess. При запуске bruteforce создаёт один процесс \texttt{confaccess --check <логин>} и передаёт ему пароли построчно; утилита проверяет каждый пароль (хэширование SHA-256 и сравнение с данными из \texttt{passwd}) и возвращает результат. Перебор выполняется последовательно, начиная с длины 1. При нахождении совпадения фиксируются найденный пароль, количество итераций и затраченное время. Перебор прекращается при обнаружении пароля, достижении заданной максимальной длины или истечении восьмичасового лимита. \subsection{Развёртывание системы} -Для создания структуры каталогов и установки утилит предусмотрен скрипт \texttt{setup.sh}. При запуске с правами суперпользователя скрипт также выставляет ограничительные права доступа. Базовый каталог задаётся переменной окружения \texttt{PRACTICE2\_DIR}; при её отсутствии используется \texttt{/usr/local/practice2}. +Для создания структуры каталогов и установки утилит предусмотрен скрипт \texttt{setup.sh} (приложение 4). При запуске с правами суперпользователя скрипт также выставляет ограничительные права доступа. Базовый каталог задаётся переменной окружения \texttt{PRACTICE2\_DIR}; при её отсутствии используется \texttt{/usr/local/practice2}. Установка системы выполняется следующим образом: @@ -496,7 +496,7 @@ sudo ./setup.sh \begin{figure}[h!] \centering - \includegraphics[width=0.85\linewidth]{img/lab2-access-log.png} + \includegraphics[width=0.6\linewidth]{img/lab2-access-log.png} \caption{Содержимое файла журнала access.log} \label{fig:lab2-access-log} \end{figure} @@ -511,33 +511,34 @@ sudo ./setup.sh N(n) = 52 \cdot 72^{n-1} \] -Расчётное максимальное время взлома определяется как $t_{\max}(n) = N(n)\,/\,v$, где $v$ — скорость хэширования, измеренная экспериментально. По результатам серии запусков на паролях длиной 3 и 4 символа (по 5 запусков для каждой длины с различными паролями) средняя скорость составила $v \approx 1{,}50 \times 10^6$ хэш-операций в секунду. +Расчётное максимальное время взлома определяется как $t_{\max}(n) = N(n)\,/\,v$, где $v$ — скорость проверки паролей через утилиту confaccess, измеренная экспериментально. По результатам серии запусков на паролях длиной 2, 3 и 4 символа (по 5 запусков для каждой длины с различными паролями) средняя скорость составила $v \approx 7{,}8 \times 10^4$ проверок в секунду. На рисунке~\ref{fig:lab2-bruteforce} показан пример вывода программы взлома для пароля длиной 3 символа. \begin{figure}[h!] \centering - \includegraphics[width=0.7\linewidth]{img/lab2-bruteforce.png} + \includegraphics[width=0.5\linewidth]{img/lab2-bruteforce.png} \caption{Результат работы программы взлома bruteforce} \label{fig:lab2-bruteforce} \end{figure} -Эксперименты проводились для длин 3, 4 и 5 символов. Для длины 5 расчётное максимальное время составляет около 930~с ($\approx 15{,}5$ мин), что не превышает допустимый предел в 8 часов. Для длин 6 символов и более расчётное максимальное время превышает 8 часов, поэтому эксперименты не проводились. Результаты приведены в таблице~\ref{tab:bruteforce}. +Эксперименты проводились для длин 2, 3 и 4 символов. Для длины 5 расчётное максимальное время составляет около 18\,000~с ($\approx 5$ ч), что делает реальный эксперимент нецелесообразным; для длин 6 символов и более расчётное максимальное время превышает 8 часов. Результаты приведены в таблице~\ref{tab:bruteforce}. \begin{table}[h!] \centering -\caption{Результаты исследования стойкости паролей (SHA-256, $v = 1{,}50 \times 10^6$ Х/с)} +\caption{Результаты исследования стойкости паролей (SHA-256, $v = 7{,}8 \times 10^4$ проверок/с)} \label{tab:bruteforce} \begin{tabularx}{\textwidth}{crrcc} \toprule Длина & $N$ & $t_{\max}$, с & Эксп. итераций & Эксп. время, с \\ \midrule -3 & $269\,568$ & $0{,}18$ & $163\,000$ & $0{,}22$ \\ -4 & $19\,408\,896$ & $12{,}9$ & $14\,160\,000$ & $9{,}5$ \\ -5 & $1\,397\,440\,512$ & $930$ & $698\,000\,000$ & $464$ \\ -6 & $1{,}01 \times 10^{11}$ & $66\,954$ & \multicolumn{2}{c}{не проводился} \\ -7 & $7{,}24 \times 10^{12}$ & ${\approx}4{,}8 \times 10^{6}$ & \multicolumn{2}{c}{не проводился} \\ -8 & $5{,}22 \times 10^{14}$ & ${\approx}3{,}5 \times 10^{8}$ & \multicolumn{2}{c}{не проводился} \\ +2 & $3\,744$ & $0{,}048$ & $1\,847$ & $0{,}024$ \\ +3 & $269\,568$ & $3{,}5$ & $134\,847$ & $1{,}73$ \\ +4 & $19\,408\,896$ & $249$ & $11\,623\,412$ & $149$ \\ +5 & $1\,397\,440\,512$ & $17\,916$ & \multicolumn{2}{c}{не проводился} \\ +6 & $1{,}01 \times 10^{11}$ & $1{,}29 \times 10^6$ & \multicolumn{2}{c}{не проводился} \\ +7 & $7{,}24 \times 10^{12}$ & ${\approx}9{,}3 \times 10^{7}$ & \multicolumn{2}{c}{не проводился} \\ +8 & $5{,}22 \times 10^{14}$ & ${\approx}6{,}7 \times 10^{9}$ & \multicolumn{2}{c}{не проводился} \\ \bottomrule \end{tabularx} \end{table} @@ -546,7 +547,8 @@ N(n) = 52 \cdot 72^{n-1} В ходе практической работы была разработана система доступа пользователей к конфиденциальным данным, включающая утилиту управления пользователями, утилиту доступа и программу взлома паролей методом грубой силы. Реализован механизм хэширования паролей на основе алгоритма SHA-256, система разграничения прав доступа и журналирование всех операций. -Теоретический анализ показал, что количество итераций, необходимых для полного перебора паролей, экспоненциально возрастает с увеличением их длины. Экспериментальное исследование позволило оценить реальную скорость перебора и подтвердить теоретические оценки. Полученные результаты демонстрируют, что использование паролей длиной 6 символов и более существенно затрудняет атаку методом грубой силы, а пароли длиной 8 и более символов при применении SHA-256 практически не поддаются взлому за приемлемое время. +Теоретический анализ показал, что количество итераций, необходимых для полного перебора паролей, экспоненциально возрастает с увеличением их длины. Экспериментальное исследование позволило оценить реальную скорость перебора и подтвердить теоретические оценки. Полученные результаты демонстрируют, что использование паролей длиной 6 символов и более существенно затрудняет атаку методом грубой силы. + \newpage \section*{Заключение} @@ -559,5 +561,26 @@ N(n) = 52 \cdot 72^{n-1} \newpage \printbibliography[heading=bibintoc] +\newpage +\section*{Приложение 1} +\addcontentsline{toc}{section}{Приложение 1} +\label{app:usermgr} +\lstinputlisting{../lab2/usermgr.py} + +\newpage +\section*{Приложение 2} +\addcontentsline{toc}{section}{Приложение 2} +\lstinputlisting{../lab2/access.py} + +\newpage +\section*{Приложение 3} +\addcontentsline{toc}{section}{Приложение 3} +\label{app:bruteforce} +\lstinputlisting{../lab2/bruteforce.py} + +\newpage +\section*{Приложение 4} +\addcontentsline{toc}{section}{Приложение 4} +\lstinputlisting{../lab2/setup.sh} \end{document} \ No newline at end of file