Код для lab2
This commit is contained in:
1
lab2/.gitignore
vendored
Normal file
1
lab2/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.pyc
|
||||||
1
lab2/.python-version
Normal file
1
lab2/.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.14
|
||||||
99
lab2/README.md
Normal file
99
lab2/README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Lab 2 — Authentication, Authorization & Brute Force Research
|
||||||
|
|
||||||
|
Система доступа к конфиденциальным данным с управлением пользователями и исследованием стойкости паролей.
|
||||||
|
|
||||||
|
## Структура каталогов
|
||||||
|
|
||||||
|
```
|
||||||
|
$PRACTICE2_DIR/ # по умолчанию /usr/local/practice2
|
||||||
|
├── etc/passwd # логин:sha256:id:права:ФИО
|
||||||
|
├── confdata/ # конфиденциальные файлы
|
||||||
|
├── bin/ # утилиты (usermgr, confaccess, bruteforce)
|
||||||
|
└── log/ # usermgr.log, access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Базовый каталог задаётся через переменную окружения `PRACTICE2_DIR`.
|
||||||
|
Если переменная не задана — используется `/usr/local/practice2`.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x setup.sh
|
||||||
|
|
||||||
|
# для пути по умолчанию (/usr/local/practice2) нужен root:
|
||||||
|
sudo ./setup.sh
|
||||||
|
|
||||||
|
# для тестирования без root:
|
||||||
|
PRACTICE2_DIR=/tmp/practice2 ./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт создаёт структуру каталогов, копирует утилиты в `bin/` и выставляет права доступа.
|
||||||
|
|
||||||
|
### Добавить bin во временный PATH
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/usr/local/practice2/bin:$PATH"
|
||||||
|
|
||||||
|
# или для тестовой директории:
|
||||||
|
export PATH="/tmp/practice2/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого утилиты доступны без полного пути:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
usermgr add alice
|
||||||
|
confaccess
|
||||||
|
bruteforce alice
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### usermgr — управление пользователями
|
||||||
|
|
||||||
|
```bash
|
||||||
|
usermgr add alice # добавить пользователя (интерактивный ввод)
|
||||||
|
usermgr list # список пользователей
|
||||||
|
usermgr edit alice --permissions rw
|
||||||
|
usermgr edit alice --full-name "Иванов Иван"
|
||||||
|
usermgr passwd alice # сменить пароль
|
||||||
|
usermgr delete alice # удалить пользователя
|
||||||
|
```
|
||||||
|
|
||||||
|
Права: `r` — чтение, `w` — запись, `d` — удаление.
|
||||||
|
|
||||||
|
Требования к паролю: первый символ — буква (A–Z, a–z), далее — буквы, цифры и `!@#$%^&*()`.
|
||||||
|
|
||||||
|
### confaccess — доступ к данным
|
||||||
|
|
||||||
|
```bash
|
||||||
|
confaccess
|
||||||
|
# или с явным указанием базовой директории:
|
||||||
|
PRACTICE2_DIR=/tmp/practice2 confaccess
|
||||||
|
```
|
||||||
|
|
||||||
|
После аутентификации доступны команды:
|
||||||
|
|
||||||
|
```
|
||||||
|
create <file> создать новый пустой файл в confdata [requires: w]
|
||||||
|
read <file> вывести содержимое файла [requires: r]
|
||||||
|
append <file> <text> дописать строку в файл [requires: w]
|
||||||
|
copy <src> <dst> скопировать файл в confdata [requires: r, w]
|
||||||
|
remove <file> удалить файл из confdata [requires: d]
|
||||||
|
help / exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Пути к файлам указываются относительно `confdata/` (или абсолютные).
|
||||||
|
Для `copy`: src — любой путь, dst — внутри confdata, перезапись запрещена.
|
||||||
|
Выход — `exit` или Ctrl+C.
|
||||||
|
|
||||||
|
### bruteforce — взлом пароля
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bruteforce alice
|
||||||
|
bruteforce alice --max-length 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Алгоритм хэширования: **SHA-256**.
|
||||||
|
Перебор выполняется напрямую по хэшу из passwd-файла.
|
||||||
|
Фиксируется время перебора и количество итераций до нахождения пароля.
|
||||||
|
Перебор останавливается автоматически при достижении лимита 8 часов.
|
||||||
244
lab2/access.py
Normal file
244
lab2/access.py
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import getpass
|
||||||
|
import hashlib
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from config import CONFDATA_DIR, LOG_DIR, PASSWD_FILE
|
||||||
|
|
||||||
|
HELP_TEXT = """\
|
||||||
|
Commands:
|
||||||
|
create <file> create new empty file [requires: w]
|
||||||
|
read <file> print file contents [requires: r]
|
||||||
|
append <file> <text> append text line to file [requires: w]
|
||||||
|
copy <src> <dst> copy file into confdata [requires: r, w]
|
||||||
|
remove <file> delete file from confdata [requires: d]
|
||||||
|
help show this help
|
||||||
|
exit exit"""
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return hashlib.sha256(password.encode("ascii")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def log_action(login: str, action: str) -> None:
|
||||||
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
timestamp = datetime.now().isoformat(sep=" ", timespec="seconds")
|
||||||
|
with open(LOG_DIR / "access.log", "a") as f:
|
||||||
|
f.write(f"{timestamp} [{login}] {action}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def read_users() -> dict[str, dict]:
|
||||||
|
users: dict[str, dict] = {}
|
||||||
|
if not PASSWD_FILE.exists():
|
||||||
|
return users
|
||||||
|
with open(PASSWD_FILE) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(":", 4)
|
||||||
|
if len(parts) != 5:
|
||||||
|
continue
|
||||||
|
users[parts[0]] = {
|
||||||
|
"password_hash": parts[1],
|
||||||
|
"id": parts[2],
|
||||||
|
"permissions": parts[3],
|
||||||
|
"full_name": parts[4],
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate() -> tuple[str, dict]:
|
||||||
|
users = read_users()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
login = input("Login: ").strip()
|
||||||
|
password = getpass.getpass("Password: ")
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nBye.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
user = users.get(login)
|
||||||
|
if user and user["password_hash"] == hash_password(password):
|
||||||
|
return login, user
|
||||||
|
log_action(login if login else "-", "LOGIN_FAILED")
|
||||||
|
print("Invalid credentials. Try again.")
|
||||||
|
|
||||||
|
|
||||||
|
def confdata_path(arg: str) -> Path:
|
||||||
|
p = Path(arg)
|
||||||
|
if not p.is_absolute():
|
||||||
|
p = CONFDATA_DIR / p
|
||||||
|
return p.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def is_in_confdata(path: Path) -> bool:
|
||||||
|
try:
|
||||||
|
path.relative_to(CONFDATA_DIR.resolve())
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_read(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "r" not in perms:
|
||||||
|
print("Permission denied (requires: r)")
|
||||||
|
return
|
||||||
|
if len(args) != 1:
|
||||||
|
print("Usage: read <file>")
|
||||||
|
return
|
||||||
|
path = confdata_path(args[0])
|
||||||
|
if not is_in_confdata(path):
|
||||||
|
print("Access denied: file must be inside confdata")
|
||||||
|
return
|
||||||
|
if not path.exists():
|
||||||
|
print(f"File not found: {path.name}")
|
||||||
|
return
|
||||||
|
print(path.read_text(), end="")
|
||||||
|
log_action(login, f"READ {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_create(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "w" not in perms:
|
||||||
|
print("Permission denied (requires: w)")
|
||||||
|
return
|
||||||
|
if len(args) != 1:
|
||||||
|
print("Usage: create <file>")
|
||||||
|
return
|
||||||
|
path = confdata_path(args[0])
|
||||||
|
if not is_in_confdata(path):
|
||||||
|
print("Access denied: file must be inside confdata")
|
||||||
|
return
|
||||||
|
if path.exists():
|
||||||
|
print(f"File already exists: {path.name}")
|
||||||
|
return
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.touch()
|
||||||
|
log_action(login, f"CREATE {path}")
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_append(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "w" not in perms:
|
||||||
|
print("Permission denied (requires: w)")
|
||||||
|
return
|
||||||
|
if len(args) < 2:
|
||||||
|
print("Usage: append <file> <text>")
|
||||||
|
return
|
||||||
|
path = confdata_path(args[0])
|
||||||
|
if not is_in_confdata(path):
|
||||||
|
print("Access denied: file must be inside confdata")
|
||||||
|
return
|
||||||
|
if not path.exists():
|
||||||
|
print(f"File not found: {path.name}. Use 'create' first.")
|
||||||
|
return
|
||||||
|
text = " ".join(args[1:])
|
||||||
|
with open(path, "a") as f:
|
||||||
|
f.write(text + "\n")
|
||||||
|
log_action(login, f"APPEND {path}")
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_copy(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "r" not in perms or "w" not in perms:
|
||||||
|
print("Permission denied (requires: r, w)")
|
||||||
|
return
|
||||||
|
if len(args) != 2:
|
||||||
|
print("Usage: copy <src> <dst>")
|
||||||
|
return
|
||||||
|
|
||||||
|
src = Path(args[0]).resolve()
|
||||||
|
dst = confdata_path(args[1])
|
||||||
|
|
||||||
|
if not is_in_confdata(dst):
|
||||||
|
print("Access denied: destination must be inside confdata")
|
||||||
|
return
|
||||||
|
if dst.is_dir():
|
||||||
|
dst = dst / src.name
|
||||||
|
if dst.exists():
|
||||||
|
print(f"Destination already exists: {dst.name}")
|
||||||
|
return
|
||||||
|
if not src.exists():
|
||||||
|
print(f"Source not found: {args[0]}")
|
||||||
|
return
|
||||||
|
if src.is_dir():
|
||||||
|
print("Copying directories is not supported")
|
||||||
|
return
|
||||||
|
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
log_action(login, f"COPY {src} -> {dst}")
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_remove(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "d" not in perms:
|
||||||
|
print("Permission denied (requires: d)")
|
||||||
|
return
|
||||||
|
if len(args) != 1:
|
||||||
|
print("Usage: remove <file>")
|
||||||
|
return
|
||||||
|
path = confdata_path(args[0])
|
||||||
|
if not is_in_confdata(path):
|
||||||
|
print("Access denied: file must be inside confdata")
|
||||||
|
return
|
||||||
|
if not path.exists():
|
||||||
|
print(f"File not found: {path.name}")
|
||||||
|
return
|
||||||
|
path.unlink()
|
||||||
|
log_action(login, f"REMOVE {path}")
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
signal.signal(signal.SIGINT, lambda _s, _f: (print("\nBye."), sys.exit(0)))
|
||||||
|
|
||||||
|
login, user = authenticate()
|
||||||
|
perms = user["permissions"]
|
||||||
|
full_name = user["full_name"]
|
||||||
|
|
||||||
|
log_action(login, "LOGIN")
|
||||||
|
print(f"\nПривет, {full_name}")
|
||||||
|
print(HELP_TEXT)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = input(f"\n{login}> ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
log_action(login, "EXIT")
|
||||||
|
print("\nBye.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = line.split()
|
||||||
|
command, args = parts[0], parts[1:]
|
||||||
|
|
||||||
|
if command == "exit":
|
||||||
|
log_action(login, "EXIT")
|
||||||
|
print("Bye.")
|
||||||
|
break
|
||||||
|
elif command == "help":
|
||||||
|
print(HELP_TEXT)
|
||||||
|
elif command == "create":
|
||||||
|
cmd_create(args, login, perms)
|
||||||
|
elif command == "read":
|
||||||
|
cmd_read(args, login, perms)
|
||||||
|
elif command == "append":
|
||||||
|
cmd_append(args, login, perms)
|
||||||
|
elif command == "copy":
|
||||||
|
cmd_copy(args, login, perms)
|
||||||
|
elif command == "remove":
|
||||||
|
cmd_remove(args, login, perms)
|
||||||
|
else:
|
||||||
|
print(f"Unknown command: {command!r}. Type 'help' for available commands.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
106
lab2/bruteforce.py
Normal file
106
lab2/bruteforce.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import itertools
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from config import PASSWD_FILE
|
||||||
|
|
||||||
|
CHARSET = string.ascii_letters + string.digits + "!@#$%^&*()"
|
||||||
|
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 max_combinations(length: int) -> int:
|
||||||
|
if length == 1:
|
||||||
|
return len(FIRST_CHARS)
|
||||||
|
return len(FIRST_CHARS) * (len(CHARSET) ** (length - 1))
|
||||||
|
|
||||||
|
|
||||||
|
def brute_force_length(target_hash: str, length: int) -> tuple[str, int, float] | None:
|
||||||
|
count = 0
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
if length == 1:
|
||||||
|
for first in FIRST_CHARS:
|
||||||
|
count += 1
|
||||||
|
if hash_password(first) == target_hash:
|
||||||
|
return first, count, time.perf_counter() - start
|
||||||
|
return None
|
||||||
|
|
||||||
|
for first in FIRST_CHARS:
|
||||||
|
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:
|
||||||
|
return password, count, time.perf_counter() - start
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Brute force password cracker (SHA-256)")
|
||||||
|
parser.add_argument("login", help="Target username")
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-length",
|
||||||
|
type=int,
|
||||||
|
default=6,
|
||||||
|
help="Maximum password length to try (default: 6)",
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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(target_hash, length)
|
||||||
|
|
||||||
|
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}.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
11
lab2/config.py
Normal file
11
lab2/config.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = Path(os.environ.get("PRACTICE2_DIR", "/usr/local/practice2"))
|
||||||
|
|
||||||
|
ETC_DIR = BASE_DIR / "etc"
|
||||||
|
CONFDATA_DIR = BASE_DIR / "confdata"
|
||||||
|
BIN_DIR = BASE_DIR / "bin"
|
||||||
|
LOG_DIR = BASE_DIR / "log"
|
||||||
|
|
||||||
|
PASSWD_FILE = ETC_DIR / "passwd"
|
||||||
277
lab2/lab2.md
Normal file
277
lab2/lab2.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# Практическая работа №2
|
||||||
|
|
||||||
|
по дисциплине «Защита информации»
|
||||||
|
|
||||||
|
**Тема работы:** «Разработка и исследование системы аутентификации и авторизации»
|
||||||
|
**Преподаватель:** Силиненко А.В.
|
||||||
|
**Email:** [a_silinenko@mail.ru](mailto:a_silinenko@mail.ru)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Цели работы
|
||||||
|
|
||||||
|
* Разработать систему доступа пользователей к конфиденциальным данным;
|
||||||
|
* Исследовать стойкость паролей к атаке методом грубой силы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Задачи работы
|
||||||
|
|
||||||
|
### 2.1.
|
||||||
|
|
||||||
|
При необходимости установить на компьютер целевую ОС (Linux или MacOS), в которой производится разработка и использование системы.
|
||||||
|
|
||||||
|
### 2.2.
|
||||||
|
|
||||||
|
Разработать систему доступа пользователей к конфиденциальным данным, включающую:
|
||||||
|
|
||||||
|
* Выделенный каталог для хранения всех файлов системы;
|
||||||
|
* Утилиту для работы с данными аутентификации и авторизации (паролями и правами доступа);
|
||||||
|
* Утилиту доступа к конфиденциальным данным, обеспечивающую аутентификацию и авторизацию пользователя.
|
||||||
|
|
||||||
|
### 2.3.
|
||||||
|
|
||||||
|
Разработать программу взлома паролей методом грубой силы и исследовать стойкость паролей в зависимости от длины пароля и алгоритма хэширования.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Требования к работе
|
||||||
|
|
||||||
|
### 3.1. ОС
|
||||||
|
|
||||||
|
Работа выполняется в ОС **Linux** или **MacOS**.
|
||||||
|
|
||||||
|
Необходим доступ с правами суперпользователя.
|
||||||
|
Если доступа нет — установить гипервизор **VirtualBox** ([https://www.virtualbox.org/](https://www.virtualbox.org/)) и развернуть гостевую ОС.
|
||||||
|
|
||||||
|
Допускается установка ОС как второй системы без VirtualBox.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2. Структура каталогов
|
||||||
|
|
||||||
|
Необходимо создать дерево каталогов:
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/practice2/
|
||||||
|
├── etc # хранение данных аутентификации и авторизации
|
||||||
|
├── confdata # хранение конфиденциальных файлов
|
||||||
|
├── bin # разработанные утилиты
|
||||||
|
└── log # файлы регистрации
|
||||||
|
```
|
||||||
|
|
||||||
|
**Права доступа:** чтение, запись и выполнение только для пользователя `root`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3. Требования к утилите управления пользователями
|
||||||
|
|
||||||
|
#### Файл хранения данных
|
||||||
|
|
||||||
|
Файл:
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/practice2/etc/passwd
|
||||||
|
```
|
||||||
|
|
||||||
|
Структура записи:
|
||||||
|
|
||||||
|
```
|
||||||
|
<логин>:<хэш_пароля>:<идентификатор>:<права>:<ФИО>
|
||||||
|
```
|
||||||
|
|
||||||
|
Одна строка — один пользователь.
|
||||||
|
|
||||||
|
**Права на файл:** чтение и запись только для `root`.
|
||||||
|
|
||||||
|
#### Права доступа
|
||||||
|
|
||||||
|
* `r` — чтение
|
||||||
|
* `w` — запись
|
||||||
|
* `d` — удаление
|
||||||
|
|
||||||
|
#### Требования к функционалу
|
||||||
|
|
||||||
|
Утилита должна обеспечивать:
|
||||||
|
|
||||||
|
* Добавление нового пользователя:
|
||||||
|
|
||||||
|
* логин
|
||||||
|
* ФИО
|
||||||
|
* права доступа
|
||||||
|
* пароль + подтверждение
|
||||||
|
* Проверку корректности данных
|
||||||
|
* Хранение пароля в виде **хэш-значения**
|
||||||
|
* Использование алгоритма хэширования согласно индивидуальному заданию
|
||||||
|
* Редактирование существующего пользователя
|
||||||
|
* Изменение пароля
|
||||||
|
* Удаление пользователя
|
||||||
|
* Регистрацию всех действий с файлом `passwd`
|
||||||
|
|
||||||
|
#### Требования к паролю
|
||||||
|
|
||||||
|
* Кодировка: ASCII
|
||||||
|
* Разрешены:
|
||||||
|
|
||||||
|
* A–Z
|
||||||
|
* a–z
|
||||||
|
* 0–9
|
||||||
|
* `!@#$%^&*()`
|
||||||
|
* Первый символ не может быть цифрой или спецсимволом
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4. Требования к утилите доступа к конфиденциальным данным
|
||||||
|
|
||||||
|
#### Авторизация
|
||||||
|
|
||||||
|
При запуске:
|
||||||
|
|
||||||
|
1. Запрос логина
|
||||||
|
2. Запрос пароля
|
||||||
|
3. Проверка корректности
|
||||||
|
|
||||||
|
Если данные корректны:
|
||||||
|
|
||||||
|
* Вывод: `Привет, <ФИО>`
|
||||||
|
* Краткая справка
|
||||||
|
* Приглашение к вводу команд
|
||||||
|
|
||||||
|
Если данные некорректны:
|
||||||
|
|
||||||
|
* Повторный запрос
|
||||||
|
|
||||||
|
Остановка — по сигналу **SIGINT (Ctrl+C)**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Поддерживаемые команды
|
||||||
|
|
||||||
|
| Команда | Описание | Требуемые права |
|
||||||
|
| -------- | ------------------------ | --------------- |
|
||||||
|
| `read` | Вывод содержимого файла | r |
|
||||||
|
| `append` | Добавление данных в файл | w |
|
||||||
|
| `copy` | Копирование файла | r + w |
|
||||||
|
| `remove` | Удаление файла | d |
|
||||||
|
| `exit` | Выход | — |
|
||||||
|
| `help` | Справка | — |
|
||||||
|
|
||||||
|
#### Ограничения copy
|
||||||
|
|
||||||
|
* Разрешено копирование:
|
||||||
|
|
||||||
|
* в каталог `confdata`
|
||||||
|
* внутри `confdata`
|
||||||
|
* Запрещено:
|
||||||
|
|
||||||
|
* из `confdata` в другие каталоги
|
||||||
|
* перезапись существующих файлов внутри `confdata`
|
||||||
|
|
||||||
|
#### Дополнительные требования
|
||||||
|
|
||||||
|
* Утилита доступна для запуска всем пользователям ОС
|
||||||
|
* Все действия с конфиденциальными данными логируются
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5. Программа взлома паролей
|
||||||
|
|
||||||
|
Должна:
|
||||||
|
|
||||||
|
* Запускать утилиту доступа
|
||||||
|
* Перебирать пароли методом brute force
|
||||||
|
* Учитывать используемый алгоритм хэширования
|
||||||
|
* Фиксировать:
|
||||||
|
|
||||||
|
* время перебора
|
||||||
|
* количество итераций до взлома
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6. Общие требования
|
||||||
|
|
||||||
|
* Язык программирования — любой
|
||||||
|
* Код должен быть снабжен комментариями
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7. Требования к исследованию
|
||||||
|
|
||||||
|
Провести исследование для длин паролей:
|
||||||
|
|
||||||
|
```
|
||||||
|
3, 4, 5, 6, 7, 8 символов
|
||||||
|
```
|
||||||
|
|
||||||
|
Необходимо:
|
||||||
|
|
||||||
|
* Рассчитать максимальное количество итераций
|
||||||
|
* По экспериментам (3–4 символа) оценить время для 5–8
|
||||||
|
* Проверить теорию экспериментально
|
||||||
|
* Прервать эксперимент, если длительность > 8 часов
|
||||||
|
* Проводить тестирование без активных задач на ПК
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Требования к отчету
|
||||||
|
|
||||||
|
В отчете необходимо указать:
|
||||||
|
|
||||||
|
* Актуальность темы
|
||||||
|
|
||||||
|
* Цель и задачи
|
||||||
|
|
||||||
|
* Требования к системе
|
||||||
|
|
||||||
|
* Характеристики ПК (процессор, память)
|
||||||
|
|
||||||
|
* ОС и среду разработки
|
||||||
|
|
||||||
|
* Используемый язык
|
||||||
|
|
||||||
|
* Алгоритм хэширования
|
||||||
|
|
||||||
|
* Примеры сборки (если применимо)
|
||||||
|
|
||||||
|
* Примеры работы утилит
|
||||||
|
|
||||||
|
* Пример расчета количества итераций и времени взлома
|
||||||
|
|
||||||
|
* Таблицу или график с результатами:
|
||||||
|
|
||||||
|
* рассчитанное количество итераций и время
|
||||||
|
* полученные экспериментальные данные
|
||||||
|
|
||||||
|
* Выводы
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Справочная информация
|
||||||
|
|
||||||
|
### Поддержка алгоритмов хэширования
|
||||||
|
|
||||||
|
**Linux:**
|
||||||
|
|
||||||
|
* `md5sum`
|
||||||
|
* `sha1sum`
|
||||||
|
* `sha256sum`
|
||||||
|
* `sha512sum`
|
||||||
|
* `b2sum`
|
||||||
|
* библиотека `openssl`
|
||||||
|
|
||||||
|
**C++:**
|
||||||
|
|
||||||
|
* `std::hash`
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
|
||||||
|
* модуль `hashlib`
|
||||||
|
|
||||||
|
**Java:**
|
||||||
|
|
||||||
|
* `java.security.MessageDigest`
|
||||||
|
* Spring Security
|
||||||
|
|
||||||
|
**Go:**
|
||||||
|
|
||||||
|
* пакет `hash`
|
||||||
19
lab2/pyproject.toml
Normal file
19
lab2/pyproject.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[project]
|
||||||
|
name = "lab2"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Authentication, authorization and brute force research system"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
usermgr = "usermgr:main"
|
||||||
|
access = "access:main"
|
||||||
|
bruteforce = "bruteforce:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
include = ["*.py"]
|
||||||
38
lab2/setup.sh
Executable file
38
lab2/setup.sh
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BASE_DIR="${PRACTICE2_DIR:-/usr/local/practice2}"
|
||||||
|
|
||||||
|
echo "Setting up directory structure at: $BASE_DIR"
|
||||||
|
|
||||||
|
mkdir -p "$BASE_DIR/etc" "$BASE_DIR/confdata" "$BASE_DIR/bin" "$BASE_DIR/log"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/etc/passwd"
|
||||||
|
|
||||||
|
cp "$SCRIPT_DIR/config.py" "$BASE_DIR/bin/config.py"
|
||||||
|
cp "$SCRIPT_DIR/usermgr.py" "$BASE_DIR/bin/usermgr"
|
||||||
|
cp "$SCRIPT_DIR/access.py" "$BASE_DIR/bin/confaccess"
|
||||||
|
cp "$SCRIPT_DIR/bruteforce.py" "$BASE_DIR/bin/bruteforce"
|
||||||
|
|
||||||
|
chmod +x "$BASE_DIR/bin/usermgr" "$BASE_DIR/bin/confaccess" "$BASE_DIR/bin/bruteforce"
|
||||||
|
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
chmod 700 "$BASE_DIR" "$BASE_DIR/etc" "$BASE_DIR/confdata" "$BASE_DIR/bin" "$BASE_DIR/log"
|
||||||
|
chmod 600 "$BASE_DIR/etc/passwd"
|
||||||
|
echo "Permissions set (root-only)."
|
||||||
|
else
|
||||||
|
echo "Warning: not running as root; skipping permission hardening."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. Directory layout:"
|
||||||
|
ls -la "$BASE_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " $BASE_DIR/bin/usermgr add <login>"
|
||||||
|
echo " $BASE_DIR/bin/confaccess"
|
||||||
|
echo " $BASE_DIR/bin/bruteforce <login>"
|
||||||
|
echo ""
|
||||||
|
echo "To add bin to PATH temporarily:"
|
||||||
|
echo " export PATH=\"$BASE_DIR/bin:\$PATH\""
|
||||||
246
lab2/usermgr.py
Normal file
246
lab2/usermgr.py
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import hashlib
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from config import LOG_DIR, PASSWD_FILE
|
||||||
|
|
||||||
|
ALLOWED_CHARS = set(
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"
|
||||||
|
)
|
||||||
|
FIRST_CHAR_ALLOWED = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||||
|
VALID_PERM_CHARS = set("rwd")
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return hashlib.sha256(password.encode("ascii")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password(password: str) -> str | None:
|
||||||
|
if not password:
|
||||||
|
return "password cannot be empty"
|
||||||
|
if password[0] not in FIRST_CHAR_ALLOWED:
|
||||||
|
return "first character must be a letter (A-Z, a-z)"
|
||||||
|
invalid = [ch for ch in password if ch not in ALLOWED_CHARS]
|
||||||
|
if invalid:
|
||||||
|
return f"invalid characters: {''.join(set(invalid))!r}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_permissions(perms: str) -> str | None:
|
||||||
|
if not perms:
|
||||||
|
return "permissions cannot be empty"
|
||||||
|
for ch in perms:
|
||||||
|
if ch not in VALID_PERM_CHARS:
|
||||||
|
return f"invalid permission {ch!r}; allowed: r, w, d"
|
||||||
|
if len(set(perms)) != len(perms):
|
||||||
|
return "duplicate permissions"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def log_action(action: str) -> None:
|
||||||
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
timestamp = datetime.now().isoformat(sep=" ", timespec="seconds")
|
||||||
|
with open(LOG_DIR / "usermgr.log", "a") as f:
|
||||||
|
f.write(f"{timestamp} {action}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def read_users() -> list[dict]:
|
||||||
|
if not PASSWD_FILE.exists():
|
||||||
|
return []
|
||||||
|
users = []
|
||||||
|
with open(PASSWD_FILE) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(":", 4)
|
||||||
|
if len(parts) != 5:
|
||||||
|
continue
|
||||||
|
users.append(
|
||||||
|
{
|
||||||
|
"login": parts[0],
|
||||||
|
"password_hash": parts[1],
|
||||||
|
"id": parts[2],
|
||||||
|
"permissions": parts[3],
|
||||||
|
"full_name": parts[4],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
def write_users(users: list[dict]) -> None:
|
||||||
|
PASSWD_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(PASSWD_FILE, "w") as f:
|
||||||
|
for u in users:
|
||||||
|
f.write(
|
||||||
|
f"{u['login']}:{u['password_hash']}:{u['id']}:{u['permissions']}:{u['full_name']}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_user(users: list[dict], login: str) -> dict | None:
|
||||||
|
return next((u for u in users if u["login"] == login), None)
|
||||||
|
|
||||||
|
|
||||||
|
def next_uid(users: list[dict]) -> str:
|
||||||
|
if not users:
|
||||||
|
return "1"
|
||||||
|
return str(max(int(u["id"]) for u in users) + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_password() -> str:
|
||||||
|
while True:
|
||||||
|
password = getpass.getpass("Password: ")
|
||||||
|
err = validate_password(password)
|
||||||
|
if err:
|
||||||
|
print(f"Invalid password: {err}")
|
||||||
|
continue
|
||||||
|
confirm = getpass.getpass("Confirm password: ")
|
||||||
|
if password != confirm:
|
||||||
|
print("Passwords do not match.")
|
||||||
|
continue
|
||||||
|
return password
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_add(args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
login = args.login
|
||||||
|
|
||||||
|
if find_user(users, login):
|
||||||
|
print(f"User '{login}' already exists.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
full_name = input("Full name: ").strip()
|
||||||
|
if not full_name:
|
||||||
|
print("Full name cannot be empty.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
perms = input("Permissions (any combination of r, w, d): ").strip()
|
||||||
|
err = validate_permissions(perms)
|
||||||
|
if err:
|
||||||
|
print(f"Invalid permissions: {err}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
password = prompt_password()
|
||||||
|
|
||||||
|
uid = next_uid(users)
|
||||||
|
users.append(
|
||||||
|
{
|
||||||
|
"login": login,
|
||||||
|
"password_hash": hash_password(password),
|
||||||
|
"id": uid,
|
||||||
|
"permissions": perms,
|
||||||
|
"full_name": full_name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
write_users(users)
|
||||||
|
log_action(f"ADD login={login} id={uid} permissions={perms} full_name='{full_name}'")
|
||||||
|
print(f"User '{login}' added (id={uid}).")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_edit(args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
user = find_user(users, args.login)
|
||||||
|
if not user:
|
||||||
|
print(f"User '{args.login}' not found.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
changed = []
|
||||||
|
|
||||||
|
if args.full_name is not None:
|
||||||
|
if not args.full_name:
|
||||||
|
print("Full name cannot be empty.")
|
||||||
|
sys.exit(1)
|
||||||
|
user["full_name"] = args.full_name
|
||||||
|
changed.append("full_name")
|
||||||
|
|
||||||
|
if args.permissions is not None:
|
||||||
|
err = validate_permissions(args.permissions)
|
||||||
|
if err:
|
||||||
|
print(f"Invalid permissions: {err}")
|
||||||
|
sys.exit(1)
|
||||||
|
user["permissions"] = args.permissions
|
||||||
|
changed.append("permissions")
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
print("Nothing to change. Use --full-name and/or --permissions.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
write_users(users)
|
||||||
|
log_action(f"EDIT login={args.login} changed={','.join(changed)}")
|
||||||
|
print(f"User '{args.login}' updated: {', '.join(changed)}.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_passwd(args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
user = find_user(users, args.login)
|
||||||
|
if not user:
|
||||||
|
print(f"User '{args.login}' not found.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
password = prompt_password()
|
||||||
|
user["password_hash"] = hash_password(password)
|
||||||
|
write_users(users)
|
||||||
|
log_action(f"PASSWD login={args.login}")
|
||||||
|
print(f"Password for '{args.login}' updated.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_delete(args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
if not find_user(users, args.login):
|
||||||
|
print(f"User '{args.login}' not found.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
users = [u for u in users if u["login"] != args.login]
|
||||||
|
write_users(users)
|
||||||
|
log_action(f"DELETE login={args.login}")
|
||||||
|
print(f"User '{args.login}' deleted.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list(_args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
if not users:
|
||||||
|
print("No users.")
|
||||||
|
return
|
||||||
|
print(f"{'Login':<16} {'ID':<4} {'Perms':<6} Full Name")
|
||||||
|
print("-" * 52)
|
||||||
|
for u in users:
|
||||||
|
print(f"{u['login']:<16} {u['id']:<4} {u['permissions']:<6} {u['full_name']}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="User management utility for practice2")
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
p_add = sub.add_parser("add", help="Add a new user")
|
||||||
|
p_add.add_argument("login", help="Username (login)")
|
||||||
|
p_add.set_defaults(func=cmd_add)
|
||||||
|
|
||||||
|
p_edit = sub.add_parser("edit", help="Edit an existing user")
|
||||||
|
p_edit.add_argument("login", help="Username to edit")
|
||||||
|
p_edit.add_argument("--full-name", dest="full_name", help="New full name")
|
||||||
|
p_edit.add_argument("--permissions", help="New permissions (e.g. rwd)")
|
||||||
|
p_edit.set_defaults(func=cmd_edit)
|
||||||
|
|
||||||
|
p_passwd = sub.add_parser("passwd", help="Change user password")
|
||||||
|
p_passwd.add_argument("login", help="Username")
|
||||||
|
p_passwd.set_defaults(func=cmd_passwd)
|
||||||
|
|
||||||
|
p_del = sub.add_parser("delete", help="Delete a user")
|
||||||
|
p_del.add_argument("login", help="Username to delete")
|
||||||
|
p_del.set_defaults(func=cmd_delete)
|
||||||
|
|
||||||
|
p_list = sub.add_parser("list", help="List all users")
|
||||||
|
p_list.set_defaults(func=cmd_list)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
8
lab2/uv.lock
generated
Normal file
8
lab2/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lab2"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
Reference in New Issue
Block a user