Compare commits
8 Commits
doklad
...
c73f7dc042
| Author | SHA1 | Date | |
|---|---|---|---|
| c73f7dc042 | |||
| d3a40d4ef7 | |||
| 7c83fe5e1a | |||
| 642e3cf0c7 | |||
| c373e8f5d9 | |||
| c62f6284d2 | |||
| 8aeb9a4244 | |||
| 37eff0ed9b |
@@ -6,7 +6,7 @@
|
||||
|
||||
```
|
||||
$PRACTICE2_DIR/ # по умолчанию /usr/local/practice2
|
||||
├── etc/passwd # логин:sha256:id:права:ФИО
|
||||
├── etc/passwd # логин:md5:id:права:ФИО
|
||||
├── confdata/ # конфиденциальные файлы
|
||||
├── bin/ # утилиты (usermgr, confaccess, bruteforce)
|
||||
└── log/ # usermgr.log, access.log
|
||||
@@ -71,6 +71,8 @@ confaccess
|
||||
PRACTICE2_DIR=/tmp/practice2 confaccess
|
||||
```
|
||||
|
||||
Режим проверки учётных данных (для bruteforce): `confaccess --check <login>` — читает пароли построчно из 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: **MD5** (по индивидуальному заданию).
|
||||
Фиксируется время перебора и количество итераций до нахождения пароля.
|
||||
Перебор останавливается автоматически при достижении лимита 8 часов.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import getpass
|
||||
import hashlib
|
||||
import shutil
|
||||
@@ -23,7 +24,7 @@ Commands:
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return hashlib.sha256(password.encode("ascii")).hexdigest()
|
||||
return hashlib.md5(password.encode("ascii"), usedforsecurity=False).hexdigest()
|
||||
|
||||
|
||||
def log_action(login: str, action: str) -> None:
|
||||
@@ -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()
|
||||
|
||||
@@ -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"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")
|
||||
|
||||
result = brute_force_length(target_hash, length)
|
||||
result = brute_force_length(args.login, length, proc)
|
||||
|
||||
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: {elapsed:.4f}s")
|
||||
print(f" Speed: {count / elapsed:,.0f} hashes/s")
|
||||
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)")
|
||||
|
||||
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__":
|
||||
|
||||
@@ -275,3 +275,10 @@
|
||||
**Go:**
|
||||
|
||||
* пакет `hash`
|
||||
|
||||
|
||||
---
|
||||
|
||||
Примечание:
|
||||
|
||||
Brute Force должен запускать утилиту из-под себя и через неё пытаться подобрать пароль, он не должен лезть в файл напрямую.
|
||||
@@ -1,19 +0,0 @@
|
||||
[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"]
|
||||
@@ -18,7 +18,7 @@ VALID_PERM_CHARS = set("rwd")
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return hashlib.sha256(password.encode("ascii")).hexdigest()
|
||||
return hashlib.md5(password.encode("ascii"), usedforsecurity=False).hexdigest()
|
||||
|
||||
|
||||
def validate_password(password: str) -> str | None:
|
||||
|
||||
8
lab2/uv.lock
generated
@@ -1,8 +0,0 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "lab2"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
2
lab3/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.pyc
|
||||
*.pdf
|
||||
94
lab3/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Lab 3 — DAC + MAC
|
||||
|
||||
Система доступа к конфиденциальным данным с дискреционным (DAC) и мандатным (MAC) управлением доступом. Основана на Lab 2.
|
||||
|
||||
## Структура каталогов
|
||||
|
||||
```
|
||||
$PRACTICE3_DIR/ # по умолчанию /usr/local/practice3
|
||||
├── etc/
|
||||
│ ├── passwd # логин:sha256:id:права:ФИО
|
||||
│ ├── access_mode # BOTH | DAC_ONLY | MAC_ONLY
|
||||
│ ├── acl # матрица доступа (DAC)
|
||||
│ ├── subject_labels # метки субъектов (MAC)
|
||||
│ └── object_labels # метки объектов (MAC)
|
||||
├── confdata/ # конфиденциальные файлы
|
||||
├── bin/ # usermgr, confaccess, bruteforce
|
||||
└── log/ # usermgr.log, access.log
|
||||
```
|
||||
|
||||
## Режимы проверки доступа
|
||||
|
||||
| Режим | DAC | MAC |
|
||||
|-------|-----|-----|
|
||||
| BOTH (по умолчанию) | да | да |
|
||||
| DAC_ONLY | да | нет |
|
||||
| MAC_ONLY | нет | да |
|
||||
|
||||
Режим задаётся через `usermgr set-mode` (только root).
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
chmod +x setup.sh
|
||||
|
||||
# для пути по умолчанию нужен root:
|
||||
sudo ./setup.sh
|
||||
|
||||
# для тестирования без root:
|
||||
PRACTICE3_DIR=/tmp/practice3 ./setup.sh
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
### usermgr — управление пользователями
|
||||
|
||||
```bash
|
||||
usermgr add alice
|
||||
usermgr list
|
||||
usermgr edit alice --full-name "Иванов Иван" --permissions rw
|
||||
usermgr edit alice --label 1 # метка субъекта (root only)
|
||||
usermgr set-label report.txt 2 # метка объекта (root only)
|
||||
usermgr set-mode DAC_ONLY # режим проверки (root only)
|
||||
usermgr show-mode # текущий режим (root only)
|
||||
usermgr passwd alice
|
||||
usermgr delete alice
|
||||
```
|
||||
|
||||
### confaccess — доступ к данным
|
||||
|
||||
```bash
|
||||
confaccess
|
||||
PRACTICE3_DIR=/tmp/practice3 confaccess
|
||||
```
|
||||
|
||||
Команды после аутентификации:
|
||||
|
||||
```
|
||||
create <file> создать файл (владелец = текущий пользователь)
|
||||
read <file> прочитать файл
|
||||
append <file> <text> дописать в файл
|
||||
copy <src> <dst> скопировать в confdata
|
||||
remove <file> удалить файл
|
||||
grant <user> <path> <perms> выдать права (только владелец)
|
||||
help / exit
|
||||
```
|
||||
|
||||
### DAC (дискреционный доступ)
|
||||
|
||||
- Каждый объект имеет владельца (создатель)
|
||||
- Владелец может выдавать права через `grant <user> <path> <perms>`
|
||||
- Права: r (чтение), w (запись), d (удаление)
|
||||
|
||||
### MAC (мандатный доступ, Белл–Лападула)
|
||||
|
||||
- Метки: 0 — несекретно, 1 — ДСП, 2 — секретно
|
||||
- Нет чтения сверху: субъект читает только объекты с уровнем ≤ своего
|
||||
- Нет записи вниз: субъект пишет только в объекты с уровнем ≥ своего
|
||||
|
||||
### bruteforce
|
||||
|
||||
```bash
|
||||
bruteforce alice
|
||||
bruteforce alice --max-length 4
|
||||
```
|
||||
138
lab3/bruteforce.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
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 BIN_DIR
|
||||
|
||||
CHARSET = string.ascii_letters + string.digits + "!@#$%^&*()"
|
||||
FIRST_CHARS = string.ascii_letters
|
||||
|
||||
MAX_HOURS = 8
|
||||
|
||||
|
||||
def get_access_cmd() -> list[str]:
|
||||
"""Resolve path to confaccess utility."""
|
||||
for name in ("confaccess", "access"):
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return [path]
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
confaccess_script = script_dir / "confaccess.py"
|
||||
if confaccess_script.exists():
|
||||
return [sys.executable, str(confaccess_script)]
|
||||
for name in ("confaccess", "access"):
|
||||
bin_cmd = BIN_DIR / name
|
||||
if bin_cmd.exists():
|
||||
return [str(bin_cmd)]
|
||||
return ["confaccess"]
|
||||
|
||||
|
||||
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(
|
||||
login: str, length: int, proc: subprocess.Popen[str]
|
||||
) -> tuple[str, int, float] | None:
|
||||
"""Try all passwords of given length via batch process. proc = confaccess --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:
|
||||
if time.perf_counter() - start > MAX_HOURS * 3600:
|
||||
return None
|
||||
if check(first):
|
||||
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
|
||||
password = first + "".join(rest)
|
||||
if check(password):
|
||||
return password, count, time.perf_counter() - start
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Brute force password cracker (via confaccess utility)"
|
||||
)
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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")
|
||||
|
||||
result = brute_force_length(args.login, length, proc)
|
||||
|
||||
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)")
|
||||
|
||||
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__":
|
||||
main()
|
||||
518
lab3/confaccess.py
Normal file
@@ -0,0 +1,518 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Access to confidential data with DAC and MAC."""
|
||||
|
||||
import argparse
|
||||
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 (
|
||||
ACL_FILE,
|
||||
ACCESS_MODE_FILE,
|
||||
CONFDATA_DIR,
|
||||
LOG_DIR,
|
||||
OBJECT_LABELS_FILE,
|
||||
PASSWD_FILE,
|
||||
SUBJECT_LABELS_FILE,
|
||||
)
|
||||
|
||||
HELP_TEXT = """\
|
||||
Commands:
|
||||
create <file> create new empty file
|
||||
read <file> print file contents
|
||||
append <file> <text> append text line to file
|
||||
copy <src> <dst> copy file into confdata
|
||||
remove <file> delete file from confdata
|
||||
grant <user> <path> <perms> grant access (owner only)
|
||||
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 read_access_mode() -> str:
|
||||
if not ACCESS_MODE_FILE.exists():
|
||||
return "BOTH"
|
||||
raw = ACCESS_MODE_FILE.read_text().strip().upper()
|
||||
if raw in ("BOTH", "DAC_ONLY", "MAC_ONLY"):
|
||||
return raw
|
||||
return "BOTH"
|
||||
|
||||
|
||||
def normalize_path(path: str) -> str:
|
||||
"""Normalize path relative to confdata (no ./, no trailing /)."""
|
||||
p = path.strip().lstrip("./")
|
||||
parts = [x for x in p.split("/") if x]
|
||||
return "/".join(parts) if parts else ""
|
||||
|
||||
|
||||
def read_acl() -> dict[str, dict]:
|
||||
"""Read ACL: path -> {owner, perms: {user: perms_str}}."""
|
||||
acl: dict[str, dict] = {}
|
||||
if not ACL_FILE.exists():
|
||||
return acl
|
||||
for line in ACL_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":", 2)
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
path, owner, assignments = parts[0], parts[1], parts[2]
|
||||
path = normalize_path(path)
|
||||
perms: dict[str, str] = {}
|
||||
for ass in assignments.split(","):
|
||||
if ":" in ass:
|
||||
u, p = ass.split(":", 1)
|
||||
perms[u.strip()] = p.strip()
|
||||
acl[path] = {"owner": owner, "perms": perms}
|
||||
return acl
|
||||
|
||||
|
||||
def write_acl(acl: dict[str, dict]) -> None:
|
||||
ACL_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = []
|
||||
for path in sorted(acl.keys()):
|
||||
entry = acl[path]
|
||||
owner = entry["owner"]
|
||||
perms = entry["perms"]
|
||||
ass = ",".join(f"{u}:{p}" for u, p in sorted(perms.items()))
|
||||
lines.append(f"{path}:{owner}:{ass}")
|
||||
ACL_FILE.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def read_subject_labels() -> dict[str, int]:
|
||||
labels: dict[str, int] = {}
|
||||
if not SUBJECT_LABELS_FILE.exists():
|
||||
return labels
|
||||
for line in SUBJECT_LABELS_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":", 1)
|
||||
if len(parts) == 2 and parts[1] in ("0", "1", "2"):
|
||||
labels[parts[0]] = int(parts[1])
|
||||
return labels
|
||||
|
||||
|
||||
def read_object_labels() -> dict[str, int]:
|
||||
labels: dict[str, int] = {}
|
||||
if not OBJECT_LABELS_FILE.exists():
|
||||
return labels
|
||||
for line in OBJECT_LABELS_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":", 1)
|
||||
if len(parts) == 2 and parts[1] in ("0", "1", "2"):
|
||||
labels[parts[0]] = int(parts[1])
|
||||
return labels
|
||||
|
||||
|
||||
def write_object_labels(labels: dict[str, int]) -> None:
|
||||
OBJECT_LABELS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = [f"{path}:{level}" for path, level in sorted(labels.items())]
|
||||
OBJECT_LABELS_FILE.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def dac_allows(login: str, action: str, rel_path: str) -> bool:
|
||||
"""Check DAC: does login have required permission on object?"""
|
||||
rel_path = normalize_path(rel_path)
|
||||
acl = read_acl()
|
||||
if action in ("create", "create_dst"):
|
||||
# New object: not in ACL yet, any authenticated user can create
|
||||
if rel_path not in acl:
|
||||
return True
|
||||
entry = acl.get(rel_path)
|
||||
if not entry:
|
||||
return False
|
||||
perms = entry["perms"].get(login, "")
|
||||
if action == "read":
|
||||
return "r" in perms
|
||||
if action in ("write", "append", "create_dst"):
|
||||
return "w" in perms
|
||||
if action == "remove":
|
||||
return "d" in perms
|
||||
if action == "grant":
|
||||
return entry["owner"] == login
|
||||
return False
|
||||
|
||||
|
||||
def dac_owner(rel_path: str) -> str | None:
|
||||
"""Return owner of path or None."""
|
||||
rel_path = normalize_path(rel_path)
|
||||
acl = read_acl()
|
||||
entry = acl.get(rel_path)
|
||||
return entry["owner"] if entry else None
|
||||
|
||||
|
||||
def mac_allows(login: str, action: str, rel_path: str, obj_level: int | None = None) -> bool:
|
||||
"""Check MAC (Bell-LaPadula)."""
|
||||
labels = read_subject_labels()
|
||||
subj_level = labels.get(login, 0)
|
||||
obj_labels = read_object_labels()
|
||||
norm_path = normalize_path(rel_path)
|
||||
if obj_level is None:
|
||||
obj_level = obj_labels.get(norm_path)
|
||||
if action == "read":
|
||||
# No read up: subject_level >= object_level
|
||||
obj_lev = obj_level if obj_level is not None else 0
|
||||
return subj_level >= obj_lev
|
||||
if action in ("write", "append", "remove"):
|
||||
obj_lev = obj_level if obj_level is not None else 0
|
||||
return subj_level <= obj_lev
|
||||
if action == "create_dst":
|
||||
# New object gets subject's label; subject can write to same level
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def check_access(login: str, action: str, rel_path: str, mode: str) -> bool:
|
||||
if mode in ("BOTH", "DAC_ONLY"):
|
||||
if not dac_allows(login, action, rel_path):
|
||||
return False
|
||||
if mode in ("BOTH", "MAC_ONLY"):
|
||||
if not mac_allows(login, action, rel_path):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
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 rel_path_from_confdata(path: Path) -> str:
|
||||
try:
|
||||
rel = path.relative_to(CONFDATA_DIR.resolve())
|
||||
return "/".join(rel.parts)
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
|
||||
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, mode: str) -> None:
|
||||
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
|
||||
rel = rel_path_from_confdata(path)
|
||||
if not check_access(login, "read", rel, mode):
|
||||
print("Permission denied")
|
||||
return
|
||||
print(path.read_text(), end="")
|
||||
log_action(login, f"READ {path}")
|
||||
|
||||
|
||||
def cmd_create(args: list[str], login: str, mode: str) -> None:
|
||||
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
|
||||
rel = rel_path_from_confdata(path)
|
||||
# DAC: new object, add to ACL with owner=login
|
||||
# MAC: new object gets subject's label
|
||||
if mode in ("BOTH", "DAC_ONLY"):
|
||||
acl = read_acl()
|
||||
acl[rel] = {"owner": login, "perms": {login: "rwd"}}
|
||||
write_acl(acl)
|
||||
if mode in ("BOTH", "MAC_ONLY"):
|
||||
labels = read_subject_labels()
|
||||
subj_level = labels.get(login, 0)
|
||||
obj_labels = read_object_labels()
|
||||
obj_labels[rel] = subj_level
|
||||
write_object_labels(obj_labels)
|
||||
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, mode: str) -> None:
|
||||
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
|
||||
rel = rel_path_from_confdata(path)
|
||||
if not check_access(login, "append", rel, mode):
|
||||
print("Permission denied")
|
||||
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, mode: str) -> None:
|
||||
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
|
||||
|
||||
dst_rel = rel_path_from_confdata(dst)
|
||||
src_rel = rel_path_from_confdata(src) if is_in_confdata(src) else None
|
||||
|
||||
if src_rel is not None:
|
||||
if not check_access(login, "read", src_rel, mode):
|
||||
print("Permission denied (read source)")
|
||||
return
|
||||
if not check_access(login, "create_dst", dst_rel, mode):
|
||||
print("Permission denied (create destination)")
|
||||
return
|
||||
|
||||
shutil.copy2(src, dst)
|
||||
if mode in ("BOTH", "DAC_ONLY"):
|
||||
acl = read_acl()
|
||||
acl[dst_rel] = {"owner": login, "perms": {login: "rwd"}}
|
||||
write_acl(acl)
|
||||
if mode in ("BOTH", "MAC_ONLY"):
|
||||
labels = read_subject_labels()
|
||||
subj_level = labels.get(login, 0)
|
||||
obj_labels = read_object_labels()
|
||||
obj_labels[dst_rel] = subj_level
|
||||
write_object_labels(obj_labels)
|
||||
log_action(login, f"COPY {src} -> {dst}")
|
||||
print("Done.")
|
||||
|
||||
|
||||
def cmd_remove(args: list[str], login: str, mode: str) -> None:
|
||||
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
|
||||
rel = rel_path_from_confdata(path)
|
||||
if not check_access(login, "remove", rel, mode):
|
||||
print("Permission denied")
|
||||
return
|
||||
path.unlink()
|
||||
if mode in ("BOTH", "DAC_ONLY"):
|
||||
acl = read_acl()
|
||||
acl.pop(rel, None)
|
||||
write_acl(acl)
|
||||
if mode in ("BOTH", "MAC_ONLY"):
|
||||
obj_labels = read_object_labels()
|
||||
obj_labels.pop(rel, None)
|
||||
write_object_labels(obj_labels)
|
||||
log_action(login, f"REMOVE {path}")
|
||||
print("Done.")
|
||||
|
||||
|
||||
def cmd_grant(args: list[str], login: str, mode: str) -> None:
|
||||
if mode not in ("BOTH", "DAC_ONLY"):
|
||||
print("grant is only available in DAC or BOTH mode")
|
||||
return
|
||||
if len(args) != 3:
|
||||
print("Usage: grant <user> <path> <perms>")
|
||||
return
|
||||
target_user, path_arg, perms = args[0], args[1], args[2]
|
||||
for ch in perms:
|
||||
if ch not in "rwd":
|
||||
print(f"Invalid permission {ch!r}; allowed: r, w, d")
|
||||
return
|
||||
path = confdata_path(path_arg)
|
||||
if not is_in_confdata(path):
|
||||
print("Access denied: path must be inside confdata")
|
||||
return
|
||||
if not path.exists():
|
||||
print(f"File not found: {path_arg}")
|
||||
return
|
||||
rel = rel_path_from_confdata(path)
|
||||
if not dac_allows(login, "grant", rel):
|
||||
print("Permission denied (only owner can grant)")
|
||||
return
|
||||
acl = read_acl()
|
||||
entry = acl.get(rel)
|
||||
if not entry:
|
||||
print("ACL entry not found")
|
||||
return
|
||||
entry["perms"][target_user] = perms
|
||||
write_acl(acl)
|
||||
log_action(login, f"GRANT {target_user} {rel} {perms}")
|
||||
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:
|
||||
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()
|
||||
full_name = user["full_name"]
|
||||
mode = read_access_mode()
|
||||
|
||||
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, cmd_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(cmd_args, login, mode)
|
||||
elif command == "read":
|
||||
cmd_read(cmd_args, login, mode)
|
||||
elif command == "append":
|
||||
cmd_append(cmd_args, login, mode)
|
||||
elif command == "copy":
|
||||
cmd_copy(cmd_args, login, mode)
|
||||
elif command == "remove":
|
||||
cmd_remove(cmd_args, login, mode)
|
||||
elif command == "grant":
|
||||
cmd_grant(cmd_args, login, mode)
|
||||
else:
|
||||
print(f"Unknown command: {command!r}. Type 'help' for available commands.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
15
lab3/config.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(os.environ.get("PRACTICE3_DIR", "/usr/local/practice3"))
|
||||
|
||||
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"
|
||||
ACCESS_MODE_FILE = ETC_DIR / "access_mode"
|
||||
ACL_FILE = ETC_DIR / "acl"
|
||||
SUBJECT_LABELS_FILE = ETC_DIR / "subject_labels"
|
||||
OBJECT_LABELS_FILE = ETC_DIR / "object_labels"
|
||||
108
lab3/lab3.md
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
# Практическая работа №3
|
||||
**Дисциплина:** Защита информации
|
||||
**Тема:** Реализация моделей дискреционного и мандатного управления доступом
|
||||
|
||||
**Преподаватель:** Силиненко А.В.
|
||||
**Email:** a_silinenko@mail.ru
|
||||
|
||||
---
|
||||
|
||||
## 1. Цели работы
|
||||
|
||||
- Изучить особенности моделей дискреционного (DAC) и мандатного (MAC) управления доступом.
|
||||
- На базе разработанной в практической работе 2 системы аутентификации и авторизации реализовать DAC и MAC.
|
||||
|
||||
---
|
||||
|
||||
## 2. Задачи работы
|
||||
|
||||
### 2.1
|
||||
Изучить особенности дискреционного (произвольного) управления доступом, а также модель Харрисона–Руззо–Ульмана, основанную на DAC.
|
||||
|
||||
### 2.2
|
||||
На базе разработанной в практической работе 2 системы аутентификации и авторизации разработать систему, реализующую DAC, включая:
|
||||
|
||||
- владение объектами;
|
||||
- произвольное назначение прав доступа к объектам;
|
||||
- контроль на основе матрицы или списка доступа.
|
||||
|
||||
### 2.3
|
||||
Изучить особенности мандатного (принудительного) управления доступом, а также модель Белла–Лападулы, основанную на MAC.
|
||||
|
||||
### 2.4
|
||||
На базе разработанной в практической работе 2 системы аутентификации и авторизации разработать систему, реализующую MAC, включая:
|
||||
|
||||
- метки безопасности для субъектов и объектов;
|
||||
- свойства безопасности модели Белла–Лападулы.
|
||||
|
||||
---
|
||||
|
||||
## 3. Требования к работе
|
||||
|
||||
### 3.1
|
||||
Работа выполняется в ОС **Linux** или **MacOS**.
|
||||
|
||||
### 3.2
|
||||
Для выполнения работы необходимо создать отдельное дерево каталогов, например:
|
||||
|
||||
```
|
||||
/usr/local/practice3/etc – файлы настроек
|
||||
/usr/local/practice3/confdata – файлы с конфиденциальной информацией
|
||||
/usr/local/practice3/bin – исполняемые файлы
|
||||
/usr/local/practice3/log – файлы регистрации
|
||||
```
|
||||
|
||||
Права доступа к созданным каталогам: **чтение, запись и выполнение только для пользователя root**.
|
||||
|
||||
При необходимости возможно создание дополнительных каталогов.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Требования к системе DAC
|
||||
|
||||
- у каждого объекта должен быть владелец, который может произвольно назначать и передавать права доступа к этому объекту;
|
||||
- контроль доступа к объектам должен осуществляться на основе **матрицы или списка доступа**, где хранится информация о правах доступа субъектов к объектам.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Требования к системе MAC
|
||||
|
||||
- каждому субъекту и объекту должна присваиваться **метка безопасности суперпользователем**;
|
||||
- предусмотреть **не менее трех уровней меток безопасности**, например:
|
||||
- несекретно
|
||||
- для служебного пользования (ДСП)
|
||||
- секретно
|
||||
- субъекты не могут менять метки безопасности;
|
||||
- контроль доступа должен осуществляться на основе свойств безопасности **модели Белла–Лападулы**:
|
||||
- «нет чтения сверху»
|
||||
- «нет записи вниз»
|
||||
- дискреционное свойство
|
||||
|
||||
---
|
||||
|
||||
## 4. Требования к отчету
|
||||
|
||||
В разделе отчета должны быть приведены:
|
||||
|
||||
- актуальность темы работы в контексте дисциплины;
|
||||
- цель и задачи работы;
|
||||
- требования к разрабатываемым системам;
|
||||
- краткие сведения о DAC и MAC;
|
||||
- примеры компиляции и сборки приложений (если используется компилируемый язык);
|
||||
- примеры запуска и работы утилит доступа к конфиденциальной информации;
|
||||
- примеры содержимого служебных файлов:
|
||||
- матрицы (или списка) доступа для DAC
|
||||
- меток конфиденциальности для MAC;
|
||||
- выводы по проделанной работе.
|
||||
|
||||
---
|
||||
|
||||
Примечание
|
||||
|
||||
Та система котрая нами уже разработана в lab2
|
||||
1. Необходимо выпустить ее реинкарнацию с учетом требований к тому что это должен быть дискреционный подход к управлению доступа
|
||||
Свойства системы которые необходимо будет реализовать будет в задании
|
||||
2. Необходимо реализовать мандатный подход к управлению доступом
|
||||
|
||||
Структура остается прежней нам просто нужно добавить компоненты для первого и второго подходов
|
||||
43
lab3/setup.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BASE_DIR="${PRACTICE3_DIR:-/usr/local/practice3}"
|
||||
|
||||
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"
|
||||
echo "BOTH" > "$BASE_DIR/etc/access_mode"
|
||||
touch "$BASE_DIR/etc/acl"
|
||||
touch "$BASE_DIR/etc/subject_labels"
|
||||
touch "$BASE_DIR/etc/object_labels"
|
||||
|
||||
cp "$SCRIPT_DIR/config.py" "$BASE_DIR/bin/config.py"
|
||||
cp "$SCRIPT_DIR/usermgr.py" "$BASE_DIR/bin/usermgr"
|
||||
cp "$SCRIPT_DIR/confaccess.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" "$BASE_DIR/etc/access_mode" \
|
||||
"$BASE_DIR/etc/acl" "$BASE_DIR/etc/subject_labels" "$BASE_DIR/etc/object_labels"
|
||||
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\""
|
||||
378
lab3/usermgr.py
Normal file
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import getpass
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from config import (
|
||||
ACCESS_MODE_FILE,
|
||||
ETC_DIR,
|
||||
LOG_DIR,
|
||||
OBJECT_LABELS_FILE,
|
||||
PASSWD_FILE,
|
||||
SUBJECT_LABELS_FILE,
|
||||
)
|
||||
|
||||
ALLOWED_CHARS = set(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"
|
||||
)
|
||||
FIRST_CHAR_ALLOWED = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||
VALID_PERM_CHARS = set("rwd")
|
||||
VALID_LABELS = frozenset({"0", "1", "2"})
|
||||
|
||||
|
||||
def require_root() -> None:
|
||||
"""Exit if not running as root."""
|
||||
if os.geteuid() != 0:
|
||||
print("Must run as root", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
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 read_access_mode() -> str:
|
||||
"""Read access mode; default BOTH if file missing."""
|
||||
if not ACCESS_MODE_FILE.exists():
|
||||
return "BOTH"
|
||||
raw = ACCESS_MODE_FILE.read_text().strip().upper()
|
||||
if raw in ("BOTH", "DAC_ONLY", "MAC_ONLY"):
|
||||
return raw
|
||||
return "BOTH"
|
||||
|
||||
|
||||
def write_access_mode(mode: str) -> None:
|
||||
ETC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ACCESS_MODE_FILE.write_text(mode + "\n")
|
||||
|
||||
|
||||
def read_subject_labels() -> dict[str, int]:
|
||||
"""Read subject labels; login -> level (0, 1, 2)."""
|
||||
labels: dict[str, int] = {}
|
||||
if not SUBJECT_LABELS_FILE.exists():
|
||||
return labels
|
||||
for line in SUBJECT_LABELS_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":", 1)
|
||||
if len(parts) == 2 and parts[1] in VALID_LABELS:
|
||||
labels[parts[0]] = int(parts[1])
|
||||
return labels
|
||||
|
||||
|
||||
def write_subject_labels(labels: dict[str, int]) -> None:
|
||||
ETC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
lines = [f"{login}:{level}" for login, level in sorted(labels.items())]
|
||||
SUBJECT_LABELS_FILE.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def read_object_labels() -> dict[str, int]:
|
||||
"""Read object labels; path (relative to confdata) -> level."""
|
||||
labels: dict[str, int] = {}
|
||||
if not OBJECT_LABELS_FILE.exists():
|
||||
return labels
|
||||
for line in OBJECT_LABELS_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(":", 1)
|
||||
if len(parts) == 2 and parts[1] in VALID_LABELS:
|
||||
labels[parts[0]] = int(parts[1])
|
||||
return labels
|
||||
|
||||
|
||||
def write_object_labels(labels: dict[str, int]) -> None:
|
||||
ETC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
lines = [f"{path}:{level}" for path, level in sorted(labels.items())]
|
||||
OBJECT_LABELS_FILE.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
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:
|
||||
if args.label is not None:
|
||||
require_root()
|
||||
|
||||
users = read_users()
|
||||
user = find_user(users, args.login)
|
||||
if not user:
|
||||
print(f"User '{args.login}' not found.")
|
||||
sys.exit(1)
|
||||
|
||||
changed: list[str] = []
|
||||
|
||||
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 args.label is not None:
|
||||
if args.label not in VALID_LABELS:
|
||||
print(f"Invalid label {args.label!r}; allowed: 0, 1, 2")
|
||||
sys.exit(1)
|
||||
labels = read_subject_labels()
|
||||
labels[args.login] = int(args.label)
|
||||
write_subject_labels(labels)
|
||||
changed.append("label")
|
||||
|
||||
if not changed:
|
||||
print("Nothing to change. Use --full-name, --permissions, and/or --label.")
|
||||
sys.exit(0)
|
||||
|
||||
if "full_name" in changed or "permissions" in changed:
|
||||
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 cmd_set_mode(args: argparse.Namespace) -> None:
|
||||
require_root()
|
||||
mode = args.mode.upper()
|
||||
if mode not in ("BOTH", "DAC_ONLY", "MAC_ONLY"):
|
||||
print(f"Invalid mode {args.mode!r}; allowed: BOTH, DAC_ONLY, MAC_ONLY")
|
||||
sys.exit(1)
|
||||
write_access_mode(mode)
|
||||
log_action(f"SET_MODE mode={mode}")
|
||||
print(f"Access mode set to {mode}.")
|
||||
|
||||
|
||||
def cmd_show_mode(_args: argparse.Namespace) -> None:
|
||||
require_root()
|
||||
mode = read_access_mode()
|
||||
print(f"Current access mode: {mode}")
|
||||
|
||||
|
||||
def cmd_set_label(args: argparse.Namespace) -> None:
|
||||
require_root()
|
||||
if args.label not in VALID_LABELS:
|
||||
print(f"Invalid label {args.label!r}; allowed: 0, 1, 2")
|
||||
sys.exit(1)
|
||||
path = args.path
|
||||
if path.startswith("./"):
|
||||
path = path[2:]
|
||||
path = "/".join(p for p in path.split("/") if p)
|
||||
labels = read_object_labels()
|
||||
labels[path] = int(args.label)
|
||||
write_object_labels(labels)
|
||||
log_action(f"SET_LABEL path={path} label={args.label}")
|
||||
print(f"Object label for '{path}' set to {args.label}.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="User management utility for practice3")
|
||||
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.add_argument("--label", help="Security label (0, 1, 2) - root only")
|
||||
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)
|
||||
|
||||
p_set_mode = sub.add_parser("set-mode", help="Set access mode (BOTH/DAC_ONLY/MAC_ONLY) - root only")
|
||||
p_set_mode.add_argument("mode", help="BOTH, DAC_ONLY, or MAC_ONLY")
|
||||
p_set_mode.set_defaults(func=cmd_set_mode)
|
||||
|
||||
p_show_mode = sub.add_parser("show-mode", help="Show current access mode - root only")
|
||||
p_show_mode.set_defaults(func=cmd_show_mode)
|
||||
|
||||
p_set_label = sub.add_parser("set-label", help="Set object security label - root only")
|
||||
p_set_label.add_argument("path", help="Path relative to confdata")
|
||||
p_set_label.add_argument("label", help="0, 1, or 2")
|
||||
p_set_label.set_defaults(func=cmd_set_label)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
536
lab4/README.md
Normal file
@@ -0,0 +1,536 @@
|
||||
Сетевые адаптеры:
|
||||
|
||||
Adapter 1: NAT
|
||||
|
||||
для выхода в интернет (DNS, HTTP)
|
||||
|
||||
Adapter 2: Internal Network
|
||||
|
||||
имя: intnet
|
||||
для связи с VM2
|
||||
|
||||
|
||||
sudo vim /etc/netplan/01-netcfg.yaml
|
||||
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
enp0s3: # NAT
|
||||
dhcp4: true
|
||||
enp0s8: # internal
|
||||
addresses: [192.168.100.1/24]
|
||||
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
enp0s3: # NAT
|
||||
dhcp4: true
|
||||
enp0s8: # internal
|
||||
addresses: [192.168.100.2/24]
|
||||
|
||||
|
||||
sudo chmod 600 /etc/netplan/01-netcfg.yaml
|
||||
sudo netplan apply
|
||||
|
||||
|
||||
|
||||
|
||||
sudo iptables -F
|
||||
sudo iptables -X
|
||||
sudo iptables -t nat -F
|
||||
|
||||
|
||||
|
||||
Ниже — учебная инструкция **только для настройки и проверки iptables**, без шагов про установку ОС, VirtualBox, netplan и утилит. Будем исходить из твоего текущего стенда:
|
||||
|
||||
* `firewall-host` — защищаемый хост, IP `192.168.100.1`
|
||||
* `external-client` — внешний клиент, IP `192.168.100.2`
|
||||
* у обеих машин есть NAT-интерфейс `enp0s3`
|
||||
* внутренний интерфейс — `enp0s8`
|
||||
* SSH к обеим машинам у тебя уже работает через проброс портов VirtualBox на `127.0.0.1:40001` и `127.0.0.1:40002`
|
||||
|
||||
Политика лабы: разрешить loopback, DNS, ping наружу, ping к защищаемому хосту только с одного IP, HTTP/HTTPS, а всё остальное запретить. Это прямо соответствует тексту задания.
|
||||
|
||||
---
|
||||
|
||||
# 2. Сначала разрешаем SSH, чтобы не отрезать себе доступ
|
||||
|
||||
Так как ты подключаешься к `firewall-host` по SSH, нужно **до включения блокировки** разрешить входящие SSH-подключения.
|
||||
|
||||
В твоём случае подключение приходит на саму Ubuntu-машину уже **после NAT VirtualBox**, то есть для Linux это обычный входящий TCP на порт `22`.
|
||||
|
||||
Выполни на `firewall-host`:
|
||||
|
||||
```bash
|
||||
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-A INPUT` — добавить правило в конец цепочки `INPUT`, которая обрабатывает пакеты, входящие на этот хост.
|
||||
* `-p tcp` — правило относится только к протоколу TCP.
|
||||
* `--dport 22` — порт назначения 22, то есть SSH-сервер.
|
||||
* `-j ACCEPT` — разрешить такой трафик.
|
||||
|
||||
Теперь разрешим ответы сервера по уже установленному SSH-соединению:
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-A OUTPUT` — добавить правило в цепочку `OUTPUT`, то есть для пакетов, исходящих с этого хоста.
|
||||
* `-p tcp` — правило касается TCP.
|
||||
* `--sport 22` — исходный порт 22; это ответы SSH-сервера клиенту.
|
||||
* `-j ACCEPT` — разрешить.
|
||||
|
||||
Такой вариант рабочий, но в Linux обычно лучше использовать правило состояний соединений. Поэтому следующим шагом мы добавим его тоже.
|
||||
|
||||
---
|
||||
|
||||
# 3. Разрешаем уже установленные и связанные соединения
|
||||
|
||||
Это одно из самых важных правил. Оно позволяет не расписывать вручную каждый ответный пакет.
|
||||
|
||||
```bash
|
||||
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-A INPUT` — правило для входящих пакетов.
|
||||
* `-m conntrack` — подключить модуль отслеживания состояний соединений.
|
||||
* `--ctstate ESTABLISHED,RELATED` — матчить пакеты:
|
||||
|
||||
* `ESTABLISHED` — относящиеся к уже установленному соединению;
|
||||
* `RELATED` — связанные с уже существующим соединением.
|
||||
* `-j ACCEPT` — разрешить.
|
||||
|
||||
И аналогично для исходящих:
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* здесь то же самое, но для исходящих пакетов.
|
||||
|
||||
Это правило особенно важно для:
|
||||
|
||||
* SSH-сессии, в которой ты уже сидишь;
|
||||
* ответов от DNS-сервера;
|
||||
* ответов от HTTP/HTTPS-серверов;
|
||||
* ответов на исходящий ping (`echo-reply`).
|
||||
|
||||
**Порядок имеет значение.** Раз это правило стоит **раньше**, чем узкие правила вида «принять UDP/TCP с `--sport 53` или TCP с `--sport 80/443`» или «`echo-reply`», то **все ответные пакеты** по уже разрешённым исходящим запросам срабатывают здесь. Отдельные строки `INPUT` для `sport` DNS/HTTP(S) и для входящего `echo-reply` становятся **лишними**: до них очередь не дойдёт, а счётчики у таких правил останутся нулевыми.
|
||||
|
||||
На `INPUT` в учебной конфигурации **осмысленно оставить** явные разрешения только для **нового** входящего трафика, которое не описывается как `ESTABLISHED`/`RELATED`: SSH (если нужен), loopback и входящий `echo-request` только с разрешённого адреса (см. раздел 8).
|
||||
|
||||
---
|
||||
|
||||
# 4. Разрешаем loopback
|
||||
|
||||
Loopback — это локальное взаимодействие внутри самой ОС через интерфейс `lo`. По условию лабы его нужно разрешить.
|
||||
|
||||
```bash
|
||||
sudo iptables -A INPUT -i lo -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-i lo` — входящий интерфейс `lo`, то есть loopback.
|
||||
* `-j ACCEPT` — разрешить.
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -o lo -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-o lo` — исходящий интерфейс `lo`.
|
||||
* `-j ACCEPT` — разрешить.
|
||||
|
||||
---
|
||||
|
||||
# 5. Разрешаем DNS
|
||||
|
||||
По условию нужно разрешить взаимодействие с DNS-сервером. Обычно DNS-запросы идут по UDP на порт `53`. Иногда может использоваться и TCP 53, поэтому для учебной работы лучше разрешить оба варианта.
|
||||
|
||||
## UDP DNS
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-A OUTPUT` — правило для исходящих запросов.
|
||||
* `-p udp` — DNS чаще всего использует UDP.
|
||||
* `--dport 53` — порт назначения 53, стандартный порт DNS.
|
||||
* `-j ACCEPT` — разрешить.
|
||||
|
||||
Входящие ответы DNS (UDP с `sport 53`) после этого правила пропускаются правилом `ESTABLISHED,RELATED` на `INPUT`; отдельная строка `INPUT --sport 53` не нужна и при типичном порядке правил всё равно не получила бы трафика.
|
||||
|
||||
## TCP DNS
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* TCP используется реже, но может понадобиться для больших DNS-ответов или специальных случаев.
|
||||
* входящие TCP-ответы от DNS идут через `ESTABLISHED,RELATED` на `INPUT`.
|
||||
|
||||
---
|
||||
|
||||
# 6. Разрешаем HTTP и HTTPS
|
||||
|
||||
По заданию нужно разрешить доступ к любым внешним серверам по HTTP/HTTPS.
|
||||
|
||||
## HTTP
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-p tcp` — HTTP работает по TCP.
|
||||
* `--dport 80` — порт назначения 80, стандартный HTTP.
|
||||
* правило разрешает открывать веб-страницы по HTTP.
|
||||
|
||||
Входящие ответы HTTP (и далее HTTPS) принимаются правилом `ESTABLISHED,RELATED` на `INPUT`; отдельное `INPUT --sport 80` не требуется.
|
||||
|
||||
## HTTPS
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* порт 443 — стандартный HTTPS.
|
||||
|
||||
Входящие ответы HTTPS обрабатываются на `INPUT` через `ESTABLISHED,RELATED`.
|
||||
|
||||
---
|
||||
|
||||
# 7. Разрешаем ping наружу
|
||||
|
||||
По заданию нужно разрешить использование `ping` для проверки достижимости любых компьютеров во внешней сети. Для `ping` используется протокол ICMP. Конкретно запрос — это `echo-request`, ответ — `echo-reply`.
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-p icmp` — правило для протокола ICMP.
|
||||
* `--icmp-type echo-request` — ICMP-пакеты типа “эхо-запрос”, то есть сам ping-запрос.
|
||||
* `-j ACCEPT` — разрешить отправку.
|
||||
|
||||
Ответы `echo-reply` на этот исходящий ping ядро относит к той же «сессии»; на `INPUT` они проходят через `ESTABLISHED,RELATED`. Отдельное правило `INPUT -p icmp --icmp-type echo-reply` не обязательно и при раннем conntrack дублирует его бесполезно.
|
||||
|
||||
---
|
||||
|
||||
# 8. Разрешаем ping к защищаемому хосту только с одного адреса
|
||||
|
||||
По заданию защищаемый хост должен отвечать на ping только от одного конкретного внешнего адреса. В твоём стенде таким адресом будет `192.168.100.2`, то есть `external-client`.
|
||||
|
||||
```bash
|
||||
sudo iptables -A INPUT -p icmp --icmp-type echo-request -s 192.168.100.2 -d 192.168.100.1 -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-p icmp` — ICMP.
|
||||
* `--icmp-type echo-request` — входящий ping-запрос.
|
||||
* `-s 192.168.100.2` — источник должен быть именно `192.168.100.2`.
|
||||
* `-d 192.168.100.1` — адрес назначения — защищаемый хост.
|
||||
* `-j ACCEPT` — разрешить.
|
||||
|
||||
```bash
|
||||
sudo iptables -A OUTPUT -p icmp --icmp-type echo-reply -s 192.168.100.1 -d 192.168.100.2 -j ACCEPT
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* это правило разрешает отправку ответа ping именно этому клиенту.
|
||||
* `-s 192.168.100.1` — источник ответа, сам firewall-host.
|
||||
* `-d 192.168.100.2` — получатель ответа, только разрешённый клиент.
|
||||
|
||||
---
|
||||
|
||||
# 9. Только теперь включаем политику DROP по умолчанию
|
||||
|
||||
Когда все разрешающие правила уже стоят, можно включить запрет всего остального.
|
||||
|
||||
```bash
|
||||
sudo iptables -P INPUT DROP
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `-P` — установить политику по умолчанию для цепочки.
|
||||
* `INPUT` — цепочка входящих пакетов.
|
||||
* `DROP` — все пакеты, которые не подошли ни под одно разрешающее правило, будут отбрасываться.
|
||||
|
||||
```bash
|
||||
sudo iptables -P OUTPUT DROP
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* то же самое для исходящих пакетов.
|
||||
|
||||
```bash
|
||||
sudo iptables -P FORWARD DROP
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* цепочка `FORWARD` нужна для транзитных пакетов через хост.
|
||||
* в этой лабораторной маршрутизатор делать не требуется, поэтому безопасно оставить `DROP`.
|
||||
|
||||
---
|
||||
|
||||
# 10. Проверяем, что SSH не отвалился
|
||||
|
||||
Сразу после установки политик выполни:
|
||||
|
||||
```bash
|
||||
sudo iptables -L -n -v --line-numbers
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* убедись, что правила SSH, conntrack, loopback, исходящие DNS/HTTP(S)/ICMP и узкое правило входящего ping с `192.168.100.2` стоят в списке.
|
||||
* на `INPUT` основной рост счётчиков у «ответного» трафика обычно виден у строки `ESTABLISHED,RELATED`, а не у отдельных `--sport` (если ты их вообще не добавляешь).
|
||||
|
||||
Открой **вторую SSH-сессию** к `firewall-host`:
|
||||
|
||||
```bash
|
||||
ssh -p 40001 arity@127.0.0.1
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* это контрольная проверка.
|
||||
* пока первая сессия ещё жива, ты проверяешь, что новое подключение тоже проходит.
|
||||
* если вторая сессия открылась, значит SSH точно не заблокирован.
|
||||
|
||||
---
|
||||
|
||||
# 11. Проверка правил по заданию
|
||||
|
||||
## Проверка DNS
|
||||
|
||||
На `firewall-host`:
|
||||
|
||||
```bash
|
||||
nslookup example.com
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `nslookup` отправляет DNS-запрос серверу имён.
|
||||
* если команда возвращает IP-адрес сайта, значит DNS разрешён.
|
||||
|
||||
## Проверка HTTP
|
||||
|
||||
```bash
|
||||
curl http://example.com
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `curl` делает HTTP-запрос к веб-серверу.
|
||||
* если приходит HTML-ответ, правило HTTP работает.
|
||||
|
||||
## Проверка HTTPS
|
||||
|
||||
```bash
|
||||
curl https://example.com
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* то же самое, но по HTTPS на порт 443.
|
||||
|
||||
## Проверка ping наружу
|
||||
|
||||
```bash
|
||||
ping -c 4 8.8.8.8
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `ping` — утилита проверки достижимости узла.
|
||||
* `-c 4` — отправить только 4 запроса, а не бесконечно.
|
||||
* `8.8.8.8` — внешний IP-адрес.
|
||||
* если ответы приходят, исходящий ping разрешён.
|
||||
|
||||
## Проверка ping к защищаемому хосту с разрешённого адреса
|
||||
|
||||
На `external-client`:
|
||||
|
||||
```bash
|
||||
ping -c 4 192.168.100.1
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* клиент `192.168.100.2` должен успешно пинговать `firewall-host`, потому что именно этот источник разрешён.
|
||||
|
||||
## Проверка блокировки лишнего трафика
|
||||
|
||||
Надёжный вариант — **TCP на порт, который не разрешён политикой**, на второй ВМ в `intnet` (у тебя это `external-client`).
|
||||
|
||||
На `external-client` подними слушатель на порт, например `8080`:
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8080
|
||||
```
|
||||
|
||||
На `firewall-host` попробуй подключиться:
|
||||
|
||||
```bash
|
||||
timeout 5 nc -vz 192.168.100.2 8080
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* исходящий TCP на `192.168.100.2:8080` не попадает под разрешённые `dport 53`/`80`/`443`, поэтому при политике `OUTPUT DROP` соединение не устанавливается (обычно таймаут).
|
||||
* проверка к `example.com:22` для отчёта часто бессмысленна: порт 22 у публичных имён часто закрыт независимо от твоего МЭ.
|
||||
|
||||
---
|
||||
|
||||
# 12. Контроль трафика через tcpdump
|
||||
|
||||
По заданию нужно показать прохождение пакетов через `tcpdump`.
|
||||
|
||||
Общий просмотр трафика:
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i any -n
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `tcpdump` — сниффер пакетов.
|
||||
* `-i any` — слушать все интерфейсы сразу.
|
||||
* `-n` — не преобразовывать адреса в имена.
|
||||
|
||||
Просмотр только ICMP:
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i any -n icmp
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* фильтр `icmp` покажет только ping-трафик.
|
||||
|
||||
Просмотр DNS:
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i any -n port 53
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `port 53` — показать DNS-пакеты.
|
||||
|
||||
Просмотр HTTP/HTTPS:
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i any -n 'tcp port 80 or tcp port 443'
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* кавычки нужны, чтобы shell корректно передал выражение целиком.
|
||||
* `or` — логическое “или”.
|
||||
* выражение показывает трафик к HTTP и HTTPS.
|
||||
|
||||
Просмотр SSH:
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i any -n tcp port 22
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* поможет убедиться, что SSH-пакеты действительно проходят через фильтр.
|
||||
|
||||
---
|
||||
|
||||
# 13. Сохранение правил
|
||||
|
||||
Когда убедишься, что всё работает, сохрани конфигурацию:
|
||||
|
||||
```bash
|
||||
sudo netfilter-persistent save
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* `netfilter-persistent` сохраняет текущие правила iptables, чтобы они восстановились после перезагрузки.
|
||||
|
||||
Можно дополнительно сохранить в файл:
|
||||
|
||||
```bash
|
||||
sudo iptables-save > ~/iptables-lab4.rules
|
||||
```
|
||||
|
||||
Пояснение:
|
||||
|
||||
* это текстовый дамп всех правил.
|
||||
* удобно приложить к отчёту или использовать для восстановления.
|
||||
|
||||
---
|
||||
|
||||
# 14. Готовый набор команд в правильном порядке
|
||||
|
||||
Ниже — тот же порядок, но компактным блоком, чтобы ты мог выполнять по шагам:
|
||||
|
||||
```bash
|
||||
sudo iptables-save > ~/iptables-before-lab.rules
|
||||
|
||||
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
|
||||
sudo iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT
|
||||
|
||||
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
sudo iptables -A INPUT -i lo -j ACCEPT
|
||||
sudo iptables -A OUTPUT -o lo -j ACCEPT
|
||||
|
||||
sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
||||
sudo iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
|
||||
|
||||
sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
|
||||
sudo iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
|
||||
|
||||
sudo iptables -A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||
|
||||
sudo iptables -A INPUT -p icmp --icmp-type echo-request -s 192.168.100.2 -d 192.168.100.1 -j ACCEPT
|
||||
sudo iptables -A OUTPUT -p icmp --icmp-type echo-reply -s 192.168.100.1 -d 192.168.100.2 -j ACCEPT
|
||||
|
||||
sudo iptables -P INPUT DROP
|
||||
sudo iptables -P OUTPUT DROP
|
||||
sudo iptables -P FORWARD DROP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 16. Что написать в учебном смысле про SSH как “дополнение”
|
||||
|
||||
Можно формулировать так:
|
||||
|
||||
> В базовом задании SSH не входит в перечень разрешённого трафика, однако для сохранения удалённого доступа к стенду было добавлено дополнительное правило, разрешающее входящие TCP-соединения на порт 22 защищаемого хоста. Правило было установлено до включения политик DROP по умолчанию, чтобы не потерять административный доступ к системе.
|
||||
|
||||
Это хорошо звучит и по сути верно.
|
||||
69
lab4/lab4.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Защита информации, 2026
|
||||
|
||||
## Практическая работа №4
|
||||
**по дисциплине «Защита информации»**
|
||||
|
||||
**Тема работы:** «Межсетевое экранирование»
|
||||
**Преподаватель:** Силиненко А.В.
|
||||
**Email:** a_silinenko@mail.ru
|
||||
|
||||
## 1. Цель работы
|
||||
Получение базовых знаний по настройке межсетевого экрана (МЭ) в ОС Linux/MacOS.
|
||||
|
||||
## 2. Задачи практической работы
|
||||
### 2.1.
|
||||
Изучение МЭ iptables или любого другого, обеспечивающего выполнение задания.
|
||||
|
||||
### 2.2.
|
||||
Реализация заданной политики доступа.
|
||||
|
||||
## 3. Ход практической работы
|
||||
### 3.1.
|
||||
Установить или убедиться в наличии iptables.
|
||||
|
||||
### 3.2.
|
||||
Изучить МЭ iptables, в том числе:
|
||||
- установка МЭ;
|
||||
- принципы и порядок обработки пакетов, основные цепочки (таблицы) обработки;
|
||||
- действия по умолчанию;
|
||||
- возможные параметры правил фильтрации.
|
||||
|
||||
### 3.3.
|
||||
Изучить команды вывода таблиц фильтрации, добавления, редактирования и удаления правил фильтрации.
|
||||
|
||||
### 3.4.
|
||||
Реализовать следующую политику доступа:
|
||||
- разрешить локальное взаимодействие через интерфейс loopback;
|
||||
- разрешить взаимодействие с DNS-сервером;
|
||||
- разрешить использование утилиты ping для проверки достижимости компьютеров с любыми IP-адресами во внешней сети;
|
||||
- разрешить использование утилиты ping для проверки достижимости защищаемого хоста только с конкретного адреса внешней сети;
|
||||
- разрешить доступ по протоколам HTTP/HTTPS к любым внешним серверам;
|
||||
- блокировать все пакеты, не удовлетворяющие указанным выше условиям.
|
||||
|
||||
Правила политики доступа должны содержать (там, где это уместно):
|
||||
- IP-адрес (сеть) источника;
|
||||
- IP-адрес (сеть) приемника;
|
||||
- транспортный протокол;
|
||||
- для протоколов TCP и UDP: порт приемника;
|
||||
- для протокола ICMP: тип и код сообщения.
|
||||
|
||||
### 3.5.
|
||||
Произвести проверку корректности реализации заданной политики, используя программы ping, nslookup (или аналоги), web-браузер.
|
||||
|
||||
Также убедиться, что весь трафик, кроме разрешенного, блокируется.
|
||||
|
||||
### 3.6.
|
||||
Проконтролировать прохождение пакетов утилитой tcpdump.
|
||||
|
||||
## 4. Требования к отчету
|
||||
### 4.1.
|
||||
В разделе отчета, посвященному данной работе, должны быть приведены:
|
||||
- актуальность темы работы в контексте курса;
|
||||
- цели и задачи работы;
|
||||
- схема стенда, в т.ч. защищаемый компьютер с МЭ, внешняя сеть, DNS-сервер(а), IP-адреса хостов;
|
||||
- принципы обработки пакетов в цепочках (таблицах) iptables;
|
||||
- примеры команд добавления правил фильтрации;
|
||||
- набор правил фильтрации, реализующий заданную политику доступа;
|
||||
- проверку реализованной политики – демонстрацию примеров пропуска разрешенного трафика и блокировки запрещенного;
|
||||
- пример контроля трафика (вывод tcpdump) при проверках реализованной политики;
|
||||
- выводы по проделанной работе.
|
||||
27
lab4/stand.mmd
Normal file
@@ -0,0 +1,27 @@
|
||||
graph TB
|
||||
subgraph host["Хост-машина"]
|
||||
subgraph fw["firewall-host (защищаемый хост, iptables)"]
|
||||
fw_lo["lo: 127.0.0.1/8"]
|
||||
fw_nat["enp0s3 (NAT)<br/>10.0.2.15/24"]
|
||||
fw_int["enp0s8 (Internal)<br/>192.168.100.1/24"]
|
||||
end
|
||||
subgraph ec["external-client (внешний клиент)"]
|
||||
ec_nat["enp0s3 (NAT)<br/>10.0.2.15/24"]
|
||||
ec_int["enp0s8 (Internal)<br/>192.168.100.2/24"]
|
||||
end
|
||||
end
|
||||
|
||||
fw_int <--->|"Internal Network (intnet)<br/>192.168.100.0/24"| ec_int
|
||||
|
||||
inet["Интернет<br/>(DNS: 10.0.2.3, HTTP/HTTPS,<br/>ICMP: 8.8.8.8 и др.)"]
|
||||
fw_nat -->|"NAT"| inet
|
||||
|
||||
style fw fill:#e8f4e8,stroke:#2d7d2d,stroke-width:2px
|
||||
style ec fill:#e8e8f4,stroke:#2d2d7d,stroke-width:2px
|
||||
style host fill:#f9f9f9,stroke:#999,stroke-width:1px
|
||||
style inet fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
style fw_lo fill:#fff,stroke:#666
|
||||
style fw_nat fill:#fff,stroke:#666
|
||||
style fw_int fill:#fff,stroke:#666
|
||||
style ec_nat fill:#fff,stroke:#666
|
||||
style ec_int fill:#fff,stroke:#666
|
||||
2
lab5/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
key.txt
|
||||
*.pyc
|
||||
76
lab5/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Практическая работа №5 — Конфиденциальный обмен сообщениями
|
||||
|
||||
Клиент-серверное приложение для обмена сообщениями по TCP с поддержкой
|
||||
шифрования 3DES-CBC (с затравкой; ключ сессии из SHA-256 от ключа файла и соли),
|
||||
контроля целостности сообщения **MD5** (по варианту, как в работе №2) и опциональной
|
||||
аутентификации отправителя **HMAC-SHA256**.
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
## Ключ шифрования
|
||||
|
||||
Ключ хранится в текстовом файле (не менее 32 символов). Права на файл — только
|
||||
чтение владельцем (`600`). Программа проверяет права при запуске и отказывается
|
||||
работать, если файл доступен группе или другим пользователям.
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32 > key.txt
|
||||
chmod 600 key.txt
|
||||
```
|
||||
|
||||
## Запуск
|
||||
|
||||
### Сервер
|
||||
|
||||
```bash
|
||||
uv run main.py server --port 9000 # без шифрования
|
||||
uv run main.py server --port 9000 --encrypt # 3DES-CBC
|
||||
uv run main.py server --port 9000 --encrypt --integrity # + MD5 digest
|
||||
uv run main.py server --port 9000 --encrypt --integrity --test-integrity # отправка с повреждённым хэшем
|
||||
uv run main.py server --port 9000 --encrypt --hmac # 3DES + HMAC-SHA256
|
||||
uv run main.py server --port 9000 --hmac # только HMAC (без шифрования)
|
||||
uv run main.py server --port 9000 --key /path/to/key.txt --encrypt # другой файл ключа
|
||||
```
|
||||
|
||||
### Клиент
|
||||
|
||||
```bash
|
||||
uv run main.py client 127.0.0.1 9000
|
||||
uv run main.py client 127.0.0.1 9000 --encrypt
|
||||
uv run main.py client 127.0.0.1 9000 --encrypt --integrity
|
||||
uv run main.py client 127.0.0.1 9000 --encrypt --integrity --test-integrity
|
||||
uv run main.py client 127.0.0.1 9000 --encrypt --hmac
|
||||
uv run main.py client 127.0.0.1 9000 --encrypt --hmac --test-integrity
|
||||
```
|
||||
|
||||
## Режимы работы
|
||||
|
||||
| Флаг | Описание |
|
||||
|------|----------|
|
||||
| *(без флагов)* | Открытый текст, без проверки целостности |
|
||||
| `--encrypt` | Шифрование 3DES-CBC с затравкой (соль + IV генерируются случайно для каждого сообщения) |
|
||||
| `--integrity` | Контроль целостности — MD5-хэш открытого текста (по индивидуальному заданию) |
|
||||
| `--hmac` | HMAC-SHA256 — контроль целостности + аутентификация отправителя (требует ключ, несовместим с `--integrity`) |
|
||||
| `--test-integrity` | Режим тестирования: отправляется заведомо некорректный хэш/HMAC (требует `--integrity` или `--hmac`) |
|
||||
|
||||
## Анализ трафика с помощью tcpdump
|
||||
|
||||
Для наблюдения за передаваемыми данными удобно использовать `tcpdump`.
|
||||
При работе на одной машине (loopback):
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i lo -X -n tcp port 9000
|
||||
```
|
||||
|
||||
При работе по сети (замените `eth0` на имя сетевого интерфейса):
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i eth0 -X -n tcp port 9000
|
||||
```
|
||||
|
||||
- **Без шифрования** — в дампе видны сообщения открытым текстом (JSON с полем `"data": "текст сообщения"`).
|
||||
- **С шифрованием** — в дампе видны только шифротекст, соль и IV в hex; читаемый текст отсутствует.
|
||||
68
lab5/lab5.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Практическая работа №5
|
||||
|
||||
по дисциплине «Защита информации»
|
||||
**Тема работы:** «Разработка клиент-серверного приложения для конфиденциального обмена сообщениями»
|
||||
**Преподаватель:** Силиненко А.В.
|
||||
**Email:** a_silinenko@mail.ru
|
||||
|
||||
## 1. Цель и задачи работы
|
||||
|
||||
### 1.1. Цель работы
|
||||
|
||||
Разработка клиент-серверного приложения для конфиденциального обмена сообщениями с использованием шифрования и контроля целостности.
|
||||
|
||||
### 1.2. Задачи работы
|
||||
|
||||
- получение базовых знаний по использованию криптографических функций библиотеки `crypto` в ОС Linux или другой библиотеки, обеспечивающей реализацию требований задания;
|
||||
- разработка клиент-серверного приложения для обмена шифрованными сообщениями с контролем целостности;
|
||||
- проверка работы приложения в различных режимах использования с помощью программы анализа трафика.
|
||||
|
||||
## 2. Требования к приложению
|
||||
|
||||
### 2.1. Общие требования
|
||||
|
||||
Разработать приложение типа «клиент-сервер», позволяющее обмениваться короткими сообщениями между клиентской и серверной частями приложения с обеспечением шифрования и контроля целостности.
|
||||
|
||||
### 2.2. Общие требования
|
||||
|
||||
- протокол взаимодействия: TCP;
|
||||
- длина сообщения: не более 80 символов;
|
||||
- полнодуплексный обмен сообщениями: возможность одновременного приема и передачи сообщений;
|
||||
- IP-адрес и порт серверной части передается клиентской части приложения в качестве параметров запуска в командной строке;
|
||||
- приложение должно быть способно функционировать как при запуске клиентской и серверной частей приложения на одном компьютере, так и на разных сетевых узлах;
|
||||
- язык написания программы: любой.
|
||||
|
||||
### 2.3. Требования к функции шифрования
|
||||
|
||||
- приложение должно поддерживать функцию шифрования сообщений на основе симметричного шифрования с использованием библиотеки `crypto`, входящей в API `openssl`, или другой аналогичной библиотеки;
|
||||
- алгоритм симметричного шифрования: согласно индивидуальному заданию;
|
||||
В моём варианте это 3DES
|
||||
- ключ шифрования длиной не менее 32 символов задается в файле;
|
||||
- права доступа к файлу: чтение только владельцем;
|
||||
- использование опции шифрования задается ключом командной строки при запуске программы.
|
||||
|
||||
### 2.4. Требования к функции контроля целостности
|
||||
|
||||
- приложение должно поддерживать функцию контроля целостности на основе подсчета хэш-значения передаваемого сообщения;
|
||||
- алгоритм вычисления хэш-значения - в соответствии с индивидуальным заданием (см. практическую работу №2);
|
||||
- использование опции контроля целостности задается ключом командной строки при запуске программы;
|
||||
- предусмотреть режим тестирования, в котором при включенной опции контроля целостности отправляются сообщения с некорректным хэш-значением.
|
||||
|
||||
### 2.5. Необязательные требования
|
||||
|
||||
- реализовать шифрование с «затравкой» («солью»), обеспечивающей разные шифрованные сообщения при одном и том же открытом тексте;
|
||||
- реализовать для разработанного приложения опцию выработки и использования сессионного ключа на основе алгоритма Диффи-Хеллмана вместо заранее заданного ключа.
|
||||
|
||||
## 3. Требования к отчету
|
||||
|
||||
### 3.1. В разделе отчета, посвященном данной работе, должны быть приведены
|
||||
|
||||
- актуальность темы работы в контексте курса;
|
||||
- цели и задачи работы;
|
||||
- краткое описание особенностей используемого алгоритма симметричного шифрования, а также шифрования «с затравкой» и алгоритма Диффи-Хеллмана, если реализованы соответствующие требования;
|
||||
- команды компиляции и сборки исполняемых модулей приложения;
|
||||
- описание процедуры проверки работы приложения с примерами, включая выводы программы анализа трафика, подтверждающие реализацию требований передачи сообщений без шифрования, с шифрованием, с шифрованием и контролем целостности;
|
||||
- описание проверки опции контроля целостности, в которой отправляются сообщения с некорректным хэш-значением;
|
||||
- описание проверки шифрования «с затравкой» и алгоритма Диффи-Хеллмана, если реализованы соответствующие требования;
|
||||
- текст разработанного приложения (клиентской и серверной частей) с комментариями - в приложении к отчету;
|
||||
- выводы по проделанной работе.
|
||||
367
lab5/main.py
Normal file
@@ -0,0 +1,367 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from cryptography.hazmat.decrepit.ciphers.algorithms import TripleDES
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, modes
|
||||
from cryptography.hazmat.primitives import padding as sym_padding
|
||||
|
||||
MAX_MSG_LEN = 80
|
||||
DEFAULT_KEY_FILE = "key.txt"
|
||||
|
||||
|
||||
def check_key_permissions(path: str) -> None:
|
||||
st = os.stat(path)
|
||||
mode = st.st_mode
|
||||
bad = stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH
|
||||
if mode & bad:
|
||||
current = oct(mode & 0o777)
|
||||
print(f"[!] Key file permissions too open ({current}): {path}")
|
||||
print(f" Run: chmod 600 {path}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def load_key(path: str) -> str:
|
||||
if not os.path.isfile(path):
|
||||
print(f"[!] Key file not found: {path}")
|
||||
sys.exit(1)
|
||||
check_key_permissions(path)
|
||||
with open(path) as f:
|
||||
key = f.read().strip()
|
||||
if len(key) < 32:
|
||||
print(f"[!] Key must be at least 32 characters (got {len(key)})")
|
||||
sys.exit(1)
|
||||
return key
|
||||
|
||||
|
||||
def derive_3des_key(file_key: str, salt: bytes) -> bytes:
|
||||
return hashlib.sha256(file_key.encode() + salt).digest()[:24]
|
||||
|
||||
|
||||
def encrypt_message(plaintext: str, file_key: str) -> tuple[bytes, bytes, bytes]:
|
||||
salt = os.urandom(16)
|
||||
key = derive_3des_key(file_key, salt)
|
||||
iv = os.urandom(8)
|
||||
|
||||
padder = sym_padding.PKCS7(64).padder()
|
||||
padded = padder.update(plaintext.encode()) + padder.finalize()
|
||||
|
||||
encryptor = Cipher(TripleDES(key), modes.CBC(iv)).encryptor()
|
||||
ct = encryptor.update(padded) + encryptor.finalize()
|
||||
return salt, iv, ct
|
||||
|
||||
|
||||
def decrypt_message(salt: bytes, iv: bytes, ct: bytes, file_key: str) -> str:
|
||||
key = derive_3des_key(file_key, salt)
|
||||
decryptor = Cipher(TripleDES(key), modes.CBC(iv)).decryptor()
|
||||
padded = decryptor.update(ct) + decryptor.finalize()
|
||||
|
||||
unpadder = sym_padding.PKCS7(64).unpadder()
|
||||
plaintext = unpadder.update(padded) + unpadder.finalize()
|
||||
return plaintext.decode()
|
||||
|
||||
|
||||
def compute_hash(text: str) -> str:
|
||||
return hashlib.md5(text.encode(), usedforsecurity=False).hexdigest()
|
||||
|
||||
|
||||
def compute_hmac(file_key: str, text: str, salt: bytes) -> str:
|
||||
return hmac.new(file_key.encode(), text.encode() + salt, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def recv_exactly(sock: socket.socket, n: int) -> bytes | None:
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
return None
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
|
||||
def send_packet(sock: socket.socket, data: bytes) -> None:
|
||||
sock.sendall(struct.pack("!I", len(data)) + data)
|
||||
|
||||
|
||||
def recv_packet(sock: socket.socket) -> dict | None:
|
||||
header = recv_exactly(sock, 4)
|
||||
if header is None:
|
||||
return None
|
||||
length = struct.unpack("!I", header)[0]
|
||||
payload = recv_exactly(sock, length)
|
||||
if payload is None:
|
||||
return None
|
||||
return json.loads(payload.decode())
|
||||
|
||||
|
||||
def do_send(
|
||||
sock: socket.socket,
|
||||
text: str,
|
||||
file_key: str | None,
|
||||
use_encrypt: bool,
|
||||
use_integrity: bool,
|
||||
use_hmac: bool,
|
||||
test_integrity: bool,
|
||||
) -> None:
|
||||
msg: dict = {}
|
||||
print(f" [TX] plaintext: {text}")
|
||||
|
||||
enc_salt: bytes | None = None
|
||||
if use_encrypt:
|
||||
assert file_key is not None
|
||||
salt, iv, ct = encrypt_message(text, file_key)
|
||||
enc_salt = salt
|
||||
derived_hex = derive_3des_key(file_key, salt).hex()
|
||||
msg["encrypted"] = True
|
||||
msg["salt"] = salt.hex()
|
||||
msg["iv"] = iv.hex()
|
||||
msg["data"] = ct.hex()
|
||||
print(f" [TX] salt: {salt.hex()}")
|
||||
print(f" [TX] 3DES key: {derived_hex}")
|
||||
print(f" [TX] IV: {iv.hex()}")
|
||||
print(f" [TX] ciphertext: {ct.hex()}")
|
||||
else:
|
||||
msg["encrypted"] = False
|
||||
msg["data"] = text
|
||||
|
||||
if use_integrity:
|
||||
h = compute_hash(text)
|
||||
if test_integrity:
|
||||
h = hashlib.md5(b"CORRUPTED_" + os.urandom(4), usedforsecurity=False).hexdigest()
|
||||
print(f" [TX] MD5: {h} (CORRUPTED!)")
|
||||
else:
|
||||
print(f" [TX] MD5: {h}")
|
||||
msg["hash"] = h
|
||||
|
||||
if use_hmac:
|
||||
assert file_key is not None
|
||||
hmac_salt = enc_salt if enc_salt is not None else os.urandom(16)
|
||||
h = compute_hmac(file_key, text, hmac_salt)
|
||||
msg["hmac_salt"] = hmac_salt.hex()
|
||||
if test_integrity:
|
||||
h = hmac.new(b"WRONG_KEY", os.urandom(8), hashlib.sha256).hexdigest()
|
||||
print(f" [TX] HMAC salt: {hmac_salt.hex()}")
|
||||
print(f" [TX] HMAC-256: {h} (CORRUPTED!)")
|
||||
else:
|
||||
print(f" [TX] HMAC salt: {hmac_salt.hex()}")
|
||||
print(f" [TX] HMAC-256: {h}")
|
||||
msg["hmac"] = h
|
||||
|
||||
send_packet(sock, json.dumps(msg).encode())
|
||||
print()
|
||||
|
||||
|
||||
def do_recv(
|
||||
msg: dict,
|
||||
file_key: str | None,
|
||||
) -> str | None:
|
||||
encrypted = msg.get("encrypted", False)
|
||||
|
||||
if encrypted:
|
||||
salt = bytes.fromhex(msg["salt"])
|
||||
iv = bytes.fromhex(msg["iv"])
|
||||
ct = bytes.fromhex(msg["data"])
|
||||
print(f" [RX] ciphertext: {ct.hex()}")
|
||||
print(f" [RX] salt: {salt.hex()}")
|
||||
print(f" [RX] IV: {iv.hex()}")
|
||||
if file_key is None:
|
||||
print(" [RX] ERROR: message is encrypted but no key loaded")
|
||||
return None
|
||||
derived_hex = derive_3des_key(file_key, salt).hex()
|
||||
print(f" [RX] 3DES key: {derived_hex}")
|
||||
try:
|
||||
text = decrypt_message(salt, iv, ct, file_key)
|
||||
except Exception as e:
|
||||
print(f" [RX] decryption failed: {e}")
|
||||
return None
|
||||
print(f" [RX] decrypted: {text}")
|
||||
else:
|
||||
text = msg["data"]
|
||||
print(f" [RX] plaintext: {text}")
|
||||
|
||||
if "hash" in msg:
|
||||
received_hash = msg["hash"]
|
||||
computed_hash = compute_hash(text)
|
||||
ok = received_hash == computed_hash
|
||||
print(f" [RX] recv hash: {received_hash}")
|
||||
print(f" [RX] calc hash: {computed_hash}")
|
||||
if ok:
|
||||
print(" [RX] integrity: OK")
|
||||
else:
|
||||
print(" [RX] integrity: FAIL - message may have been tampered with!")
|
||||
|
||||
if "hmac" in msg:
|
||||
hmac_salt = bytes.fromhex(msg["hmac_salt"])
|
||||
received_hmac = msg["hmac"]
|
||||
if file_key is None:
|
||||
print(" [RX] ERROR: HMAC present but no key loaded")
|
||||
else:
|
||||
computed_h = compute_hmac(file_key, text, hmac_salt)
|
||||
ok = hmac.compare_digest(received_hmac, computed_h)
|
||||
print(f" [RX] HMAC salt: {hmac_salt.hex()}")
|
||||
print(f" [RX] recv HMAC: {received_hmac}")
|
||||
print(f" [RX] calc HMAC: {computed_h}")
|
||||
if ok:
|
||||
print(" [RX] HMAC auth: OK - sender verified")
|
||||
else:
|
||||
print(" [RX] HMAC auth: FAIL - sender NOT verified!")
|
||||
|
||||
print()
|
||||
return text
|
||||
|
||||
|
||||
def receiver_thread(
|
||||
sock: socket.socket,
|
||||
file_key: str | None,
|
||||
stop_event: threading.Event,
|
||||
) -> None:
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
msg = recv_packet(sock)
|
||||
if msg is None:
|
||||
print("\n[*] Connection closed by remote side.")
|
||||
stop_event.set()
|
||||
break
|
||||
do_recv(msg, file_key)
|
||||
except OSError:
|
||||
if not stop_event.is_set():
|
||||
print("\n[*] Connection lost.")
|
||||
stop_event.set()
|
||||
|
||||
|
||||
def sender_thread(
|
||||
sock: socket.socket,
|
||||
file_key: str | None,
|
||||
use_encrypt: bool,
|
||||
use_integrity: bool,
|
||||
use_hmac: bool,
|
||||
test_integrity: bool,
|
||||
stop_event: threading.Event,
|
||||
) -> None:
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
text = input()
|
||||
except EOFError:
|
||||
stop_event.set()
|
||||
break
|
||||
if stop_event.is_set():
|
||||
break
|
||||
if len(text) > MAX_MSG_LEN:
|
||||
print(f" [!] Message too long ({len(text)} > {MAX_MSG_LEN}), truncated.")
|
||||
text = text[:MAX_MSG_LEN]
|
||||
do_send(sock, text, file_key, use_encrypt, use_integrity, use_hmac, test_integrity)
|
||||
except OSError:
|
||||
if not stop_event.is_set():
|
||||
stop_event.set()
|
||||
|
||||
|
||||
def run_session(
|
||||
sock: socket.socket,
|
||||
addr: tuple,
|
||||
file_key: str | None,
|
||||
use_encrypt: bool,
|
||||
use_integrity: bool,
|
||||
use_hmac: bool,
|
||||
test_integrity: bool,
|
||||
) -> None:
|
||||
flags = []
|
||||
if use_encrypt:
|
||||
flags.append("3DES")
|
||||
if use_integrity:
|
||||
flags.append("MD5")
|
||||
if use_hmac:
|
||||
flags.append("HMAC-SHA256")
|
||||
if test_integrity:
|
||||
flags.append("test-integrity")
|
||||
mode_str = ", ".join(flags) if flags else "plaintext"
|
||||
print(f"[*] Session with {addr[0]}:{addr[1]} [{mode_str}]")
|
||||
print("[*] Type messages and press Enter to send. Ctrl+C / Ctrl+D to quit.\n")
|
||||
|
||||
stop = threading.Event()
|
||||
rx = threading.Thread(target=receiver_thread, args=(sock, file_key, stop), daemon=True)
|
||||
tx = threading.Thread(target=sender_thread, args=(sock, file_key, use_encrypt, use_integrity, use_hmac, test_integrity, stop), daemon=True)
|
||||
rx.start()
|
||||
tx.start()
|
||||
|
||||
try:
|
||||
while not stop.is_set():
|
||||
stop.wait(0.5)
|
||||
except KeyboardInterrupt:
|
||||
print("\n[*] Interrupted.")
|
||||
stop.set()
|
||||
|
||||
sock.close()
|
||||
|
||||
|
||||
def run_server(args: argparse.Namespace) -> None:
|
||||
file_key = load_key(args.key) if (args.encrypt or args.hmac) else None
|
||||
|
||||
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
srv.bind(("0.0.0.0", args.port))
|
||||
srv.listen(1)
|
||||
print(f"[*] Listening on 0.0.0.0:{args.port} ...")
|
||||
|
||||
conn, addr = srv.accept()
|
||||
print(f"[*] Client connected: {addr[0]}:{addr[1]}")
|
||||
srv.close()
|
||||
run_session(conn, addr, file_key, args.encrypt, args.integrity, args.hmac, args.test_integrity)
|
||||
|
||||
|
||||
def run_client(args: argparse.Namespace) -> None:
|
||||
file_key = load_key(args.key) if (args.encrypt or args.hmac) else None
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect((args.host, args.port))
|
||||
print(f"[*] Connected to {args.host}:{args.port}")
|
||||
run_session(sock, (args.host, args.port), file_key, args.encrypt, args.integrity, args.hmac, args.test_integrity)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Encrypted messaging (3DES-CBC; integrity MD5 per assignment; optional HMAC-SHA256)"
|
||||
)
|
||||
sub = parser.add_subparsers(dest="role", required=True)
|
||||
|
||||
for name, sp in [("server", sub.add_parser("server", help="Start server")),
|
||||
("client", sub.add_parser("client", help="Start client"))]:
|
||||
if name == "server":
|
||||
sp.add_argument("--port", type=int, required=True)
|
||||
else:
|
||||
sp.add_argument("host")
|
||||
sp.add_argument("port", type=int)
|
||||
sp.add_argument("--encrypt", action="store_true", help="Enable 3DES-CBC encryption")
|
||||
sp.add_argument("--integrity", action="store_true", help="MD5 integrity hash (variant / lab 2)")
|
||||
sp.add_argument("--hmac", action="store_true", help="HMAC-SHA256 integrity + authentication")
|
||||
sp.add_argument("--test-integrity", action="store_true", help="Send corrupted hash/HMAC")
|
||||
sp.add_argument("--key", default=DEFAULT_KEY_FILE, help="Path to key file (default: key.txt)")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = build_parser().parse_args()
|
||||
if args.integrity and args.hmac:
|
||||
print("[!] --integrity and --hmac are mutually exclusive")
|
||||
sys.exit(1)
|
||||
if args.test_integrity and not (args.integrity or args.hmac):
|
||||
print("[!] --test-integrity requires --integrity or --hmac")
|
||||
sys.exit(1)
|
||||
if args.role == "server":
|
||||
run_server(args)
|
||||
else:
|
||||
run_client(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
9
lab5/pyproject.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[project]
|
||||
name = "lab5"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"cryptography>=46.0.6",
|
||||
]
|
||||
109
lab5/uv.lock
generated
Normal file
@@ -0,0 +1,109 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lab5"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "cryptography", specifier = ">=46.0.6" }]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 18 KiB |
BIN
report/img/lab3-acl.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
report/img/lab3-confaccess.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
report/img/lab3-labels.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
report/img/lab3-setup.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
report/img/lab3-usermgr-mode.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
report/img/lab4-stand.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
report/img/lab5-enc-int.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
report/img/lab5-enc-tcpdump.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
report/img/lab5-enc.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
report/img/lab5-hmac-fail.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
report/img/lab5-hmac.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
report/img/lab5-plain-tcpdump.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
report/img/lab5-plain.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
report/img/lab5-salt.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
report/img/lab5-test-int.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
@@ -176,6 +176,9 @@
|
||||
\begin{enumerate}
|
||||
\item Практическая работа №1. Анализ уязвимостей программного обеспечения. Данная практическая работа посвящена анализу уязвимостей программного обеспечения с использованием Банка данных угроз безопасности информации ФСТЭК России~\cite{fstec-bdu}. В ходе выполнения работы предусмотрено изучение структуры разделов «Угрозы» и «Уязвимости», а также поиск уязвимостей по заданным критериям. Особое внимание уделяется выявлению уязвимостей, соответствующих используемым версиям операционных систем личных устройств, и рассмотрению возможных мер по их устранению.
|
||||
\item Практическая работа №2. Разработка и исследование системы аутентификации и авторизации. Данная практическая работа посвящена разработке системы доступа пользователей к конфиденциальным данным и исследованию стойкости паролей к атаке методом грубой силы. В ходе выполнения работы реализованы утилита управления пользователями, утилита доступа к конфиденциальным данным и программа перебора паролей, а также проведено экспериментальное исследование зависимости времени взлома от длины пароля.
|
||||
\item Практическая работа №3. Реализация моделей дискреционного и мандатного управления доступом. Данная практическая работа посвящена расширению системы из работы №2 путём реализации моделей DAC (дискреционный доступ) и MAC (мандатный доступ) на основе модели Белла–Лападулы.
|
||||
\item Практическая работа №4. Межсетевое экранирование средствами \texttt{iptables}. Данная практическая работа посвящена настройке межсетевого экрана в Linux, формированию политики фильтрации сетевого трафика и экспериментальной проверке пропуска разрешённых соединений и блокировки запрещённых пакетов с использованием \texttt{tcpdump}.
|
||||
\item Практическая работа №5. Разработка клиент-серверного приложения для конфиденциального обмена сообщениями. Данная практическая работа посвящена разработке TCP-приложения с поддержкой шифрования 3DES-CBC с затравкой (выработка ключа сессии на основе SHA-256), контроля целостности сообщения на основе MD5 в соответствии с индивидуальным заданием, дополнительного режима HMAC-SHA256, а также проверке корректности шифрования с помощью анализа трафика.
|
||||
\end{enumerate}
|
||||
|
||||
\newpage
|
||||
@@ -360,7 +363,7 @@ A: C (Availability Impact: Complete) — полное нарушение дос
|
||||
\begin{enumerate}
|
||||
\item Разработать систему доступа пользователей к конфиденциальным данным, включающую утилиту управления пользователями и утилиту доступа к конфиденциальным данным.
|
||||
\item Разработать программу взлома паролей методом грубой силы.
|
||||
\item Исследовать стойкость паролей в зависимости от их длины при использовании алгоритма хэширования SHA-256.
|
||||
\item Исследовать стойкость паролей в зависимости от их длины при использовании алгоритма хэширования MD5 (индивидуальный вариант задания).
|
||||
\end{enumerate}
|
||||
|
||||
\subsection{Требования к системе}
|
||||
@@ -396,13 +399,13 @@ A: C (Availability Impact: Complete) — полное нарушение дос
|
||||
\item Операционная система: Ubuntu 25.10
|
||||
\end{itemize}
|
||||
|
||||
В качестве среды разработки использовался редактор Cursor. Язык программирования — Python 3.14. Алгоритм хэширования — SHA-256, реализованный с использованием модуля \texttt{hashlib} стандартной библиотеки Python.
|
||||
В качестве среды разработки использовался редактор Cursor. Язык программирования — Python 3.14. Для хэширования паролей используется алгоритм MD5 (индивидуальный вариант задания), реализованный с использованием модуля \texttt{hashlib} стандартной библиотеки Python.
|
||||
|
||||
\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} — вывод списка всех пользователей. Пароль хранится в виде MD5-хэша (128 бит, 32 шестнадцатеричных символа). Все операции с файлом паролей регистрируются в журнале \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 +430,11 @@ A: C (Availability Impact: Complete) — полное нарушение дос
|
||||
|
||||
Копирование разрешено только в каталог \texttt{confdata} или внутри него. Копирование из \texttt{confdata} в другие каталоги и перезапись существующих файлов запрещены.
|
||||
|
||||
Программа взлома паролей (bruteforce) выполняет последовательный перебор паролей начиная с длины 1, проверяя все допустимые комбинации символов. Для каждой комбинации вычисляется SHA-256-хэш и сравнивается с целевым хэшем из файла паролей. При нахождении совпадения фиксируются найденный пароль, количество итераций и затраченное время. Перебор прекращается при обнаружении пароля, достижении заданной максимальной длины или истечении восьмичасового лимита.
|
||||
Программа взлома паролей (bruteforce, исходный код в приложении 3) не имеет доступа к файлу паролей и выполняет перебор исключительно через утилиту confaccess. При запуске bruteforce создаёт один процесс \texttt{confaccess --check <логин>} и передаёт ему пароли построчно; утилита проверяет каждый пароль (хэширование MD5 и сравнение с данными из \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 +499,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,42 +514,615 @@ 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. Величина $v$ получена по трём запускам программы bruteforce на том же ПК для паролей длиной 2, 3 и~4 символа (хэширование MD5): средняя скорость составила $v \approx 2{,}4 \times 10^4$ проверок в секунду. Столбцы \textit{Эксп. итераций} и \textit{Эксп. время} отражают число попыток и длительность перебора на длине $n$ до нахождения пароля в соответствующем запуске.
|
||||
|
||||
На рисунке~\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 расчётное максимальное время при измеренном $v$ составляет около $6 \times 10^4$~с (порядка суток), поэтому полный перебор на практике не выполнялся; для больших длин $t_{\max}$ ещё выше. Результаты приведены в таблице~\ref{tab:bruteforce}.
|
||||
|
||||
\begin{table}[h!]
|
||||
\centering
|
||||
\caption{Результаты исследования стойкости паролей (SHA-256, $v = 1{,}50 \times 10^6$ Х/с)}
|
||||
\caption{Результаты исследования стойкости паролей (MD5, $v \approx 2{,}4 \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{,}16$ & $134$ & $0{,}006$ \\
|
||||
3 & $269\,568$ & $11$ & $9\,638$ & $0{,}40$ \\
|
||||
4 & $19\,408\,896$ & $818$ & $693\,926$ & $30{,}5$ \\
|
||||
5 & $1\,397\,440\,512$ & $58\,900$ & \multicolumn{2}{c}{не проводился} \\
|
||||
6 & $1{,}01 \times 10^{11}$ & $4{,}24 \times 10^6$ & \multicolumn{2}{c}{не проводился} \\
|
||||
7 & $7{,}24 \times 10^{12}$ & ${\approx}3{,}05 \times 10^{8}$ & \multicolumn{2}{c}{не проводился} \\
|
||||
8 & $5{,}22 \times 10^{14}$ & ${\approx}2{,}2 \times 10^{10}$ & \multicolumn{2}{c}{не проводился} \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\end{table}
|
||||
|
||||
\subsection{Выводы}
|
||||
|
||||
В ходе практической работы была разработана система доступа пользователей к конфиденциальным данным, включающая утилиту управления пользователями, утилиту доступа и программу взлома паролей методом грубой силы. Реализован механизм хэширования паролей на основе алгоритма SHA-256, система разграничения прав доступа и журналирование всех операций.
|
||||
В ходе практической работы была разработана система доступа пользователей к конфиденциальным данным, включающая утилиту управления пользователями, утилиту доступа и программу взлома паролей методом грубой силы. Реализован механизм хэширования паролей на основе алгоритма MD5 (индивидуальный вариант задания), система разграничения прав доступа и журналирование всех операций.
|
||||
|
||||
Теоретический анализ показал, что количество итераций, необходимых для полного перебора паролей, экспоненциально возрастает с увеличением их длины. Экспериментальное исследование позволило оценить реальную скорость перебора и подтвердить теоретические оценки. Полученные результаты демонстрируют, что использование паролей длиной 6 символов и более существенно затрудняет атаку методом грубой силы.
|
||||
|
||||
\newpage
|
||||
\section{Реализация моделей дискреционного и мандатного управления доступом}
|
||||
|
||||
\subsection{Актуальность темы}
|
||||
|
||||
Модели управления доступом являются фундаментом систем защиты информации. Дискреционное управление (DAC) позволяет владельцам объектов гибко назначать права, однако не защищает от утечки информации через легитимных пользователей. Мандатное управление (MAC) на основе меток конфиденциальности обеспечивает принудительный контроль потока информации и применяется в системах с повышенными требованиями к защите. Изучение и практическая реализация обеих моделей в контексте дисциплины <<Защита информации>> позволяет закрепить понимание механизмов разграничения доступа и их ограничений.
|
||||
|
||||
\subsection{Цели и задачи работы}
|
||||
|
||||
Практическая работа №3 по дисциплине <<Защита информации>> посвящена реализации моделей дискреционного (DAC) и мандатного (MAC) управления доступом на базе системы, разработанной в практической работе №2.
|
||||
|
||||
Цель работы: изучить особенности моделей DAC и MAC и реализовать их в рамках существующей системы аутентификации и авторизации.
|
||||
|
||||
Задачи работы:
|
||||
\begin{enumerate}
|
||||
\item Изучить особенности дискреционного управления доступом и модель Харрисона–Руззо–Ульмана.
|
||||
\item Разработать систему DAC с владением объектами, произвольным назначением прав и контролем на основе матрицы (списка) доступа.
|
||||
\item Изучить особенности мандатного управления доступом и модель Белла–Лападулы.
|
||||
\item Разработать систему MAC с метками безопасности для субъектов и объектов и свойствами модели Белла–Лападулы.
|
||||
\end{enumerate}
|
||||
|
||||
\subsection{Краткие сведения о DAC и MAC}
|
||||
|
||||
\textbf{Дискреционное управление доступом (DAC)} основано на произвольном назначении прав владельцем объекта. Каждый объект имеет владельца, который может назначать и передавать права доступа другим субъектам. Контроль осуществляется на основе матрицы доступа или списков доступа (ACL), где для каждой пары (субъект, объект) указаны разрешённые операции. Модель Харрисона–Руззо–Ульмана формализует операции над матрицей доступа (создание/удаление субъектов и объектов, выдача и отзыв прав).
|
||||
|
||||
\textbf{Мандатное управление доступом (MAC)} основано на метках конфиденциальности, назначаемых суперпользователем. Субъекты и объекты получают метки (уровни), и доступ определяется правилами, не зависящими от воли пользователей. Модель Белла–Лападулы реализует два основных свойства: <<нет чтения сверху>> (субъект может читать только объекты с уровнем не выше своего) и <<нет записи вниз>> (субъект может записывать только в объекты с уровнем не ниже своего), что предотвращает утечку информации с высокого уровня на низкий.
|
||||
|
||||
\subsection{Требования к разрабатываемым системам}
|
||||
|
||||
Структура каталогов аналогична работе №2, но с базовым путём \texttt{/usr/local/practice3} (переменная \texttt{PRACTICE3\_DIR}):
|
||||
|
||||
\begin{itemize}
|
||||
\item \texttt{etc/} — файлы настроек: \texttt{passwd}, \texttt{access\_mode}, \texttt{acl}, \texttt{subject\_labels}, \texttt{object\_labels};
|
||||
\item \texttt{confdata/} — конфиденциальные файлы;
|
||||
\item \texttt{bin/} — утилиты;
|
||||
\item \texttt{log/} — журналы.
|
||||
\end{itemize}
|
||||
|
||||
\textbf{DAC:} у каждого объекта — владелец; владелец может назначать права через команду \texttt{grant}; контроль по списку доступа (ACL) в формате \\ \texttt{путь:владелец:пользователь:права,...}.
|
||||
|
||||
\textbf{MAC:} метки безопасности (0 — несекретно, 1 — ДСП, 2 — секретно) назначаются суперпользователем через \texttt{usermgr}; субъекты не могут менять метки; проверка по свойствам Белла–Лападулы.
|
||||
|
||||
Режим проверки (BOTH — DAC и MAC, DAC\_ONLY, MAC\_ONLY) задаётся администратором через \texttt{usermgr set-mode} и доступен только root.
|
||||
|
||||
\subsection{Описание реализации}
|
||||
|
||||
Система построена на базе кода из практической работы №2. Утилита \texttt{usermgr} расширена админскими подкомандами (приложение 5): \texttt{set-mode BOTH|DAC\_ONLY|MAC\_ONLY} — установка режима проверки; \texttt{show-mode} — вывод текущего режима; \texttt{edit <логин> --label <0|1|2>} — установка метки субъекта; \texttt{set-label <путь> <0|1|2>} — установка метки объекта. Все админские команды требуют прав root.
|
||||
|
||||
Утилита \texttt{confaccess} (приложение 6) полностью переработана. При \texttt{create} объект получает владельца (текущий пользователь) и запись в ACL; при операциях \texttt{read}, \texttt{append}, \texttt{remove} проверяется наличие соответствующего права в ACL. Команда \texttt{grant <пользователь> <путь> <права>} доступна только владельцу объекта. Для MAC при каждой операции проверяются свойства Белла–Лападулы: чтение разрешено при уровне субъекта $\ge$ уровня объекта; запись (append, create, remove) — при уровне субъекта $\le$ уровня объекта. Конфигурация путей вынесена в \texttt{config.py} (приложение 7). Скрипт \texttt{setup.sh} (приложение 8) создаёт структуру для practice3 и инициализирует файлы \texttt{access\_mode}, \texttt{acl}, \texttt{subject\_labels}, \texttt{object\_labels}.
|
||||
|
||||
\subsection{Развёртывание и примеры работы}
|
||||
|
||||
Установка выполняется аналогично работе №2:
|
||||
|
||||
\begin{verbatim}
|
||||
chmod +x setup.sh
|
||||
PRACTICE3_DIR=/tmp/practice3 ./setup.sh
|
||||
\end{verbatim}
|
||||
|
||||
Результат выполнения скрипта представлен на рисунке~\ref{fig:lab3-setup}.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
% Заглушка: добавить скриншот img/lab3-setup.png
|
||||
\includegraphics[width=0.6\linewidth]{img/lab3-setup.png}
|
||||
\caption{Результат выполнения скрипта setup.sh (practice3)}
|
||||
\label{fig:lab3-setup}
|
||||
\end{figure}
|
||||
|
||||
Примеры работы утилит: переключение режима \texttt{usermgr set-mode}, установка меток, работа \texttt{confaccess} с командами \texttt{create}, \texttt{grant}, \texttt{read} — на рисунках~\ref{fig:lab3-usermgr} и~\ref{fig:lab3-confaccess}.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
% Заглушка: добавить скриншот img/lab3-usermgr-mode.png
|
||||
\includegraphics[width=0.6\linewidth]{img/lab3-usermgr-mode.png}
|
||||
\caption{Работа usermgr: set-mode, show-mode, set-label}
|
||||
\label{fig:lab3-usermgr}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
% Заглушка: добавить скриншот img/lab3-confaccess.png
|
||||
\includegraphics[width=0.6\linewidth]{img/lab3-confaccess.png}
|
||||
\caption{Работа confaccess: create, grant, read}
|
||||
\label{fig:lab3-confaccess}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Примеры содержимого служебных файлов}
|
||||
|
||||
\textbf{Список доступа (ACL) для DAC} — файл \texttt{etc/acl}. Каждая строка: \texttt{путь:владелец:пользователь1:права1,пользователь2:права2,...}. Пример:
|
||||
|
||||
\begin{verbatim}
|
||||
report.txt:alice:alice:rwd,bob:r
|
||||
secret.txt:bob:bob:rwd
|
||||
\end{verbatim}
|
||||
|
||||
Владелец \texttt{report.txt} — alice (полные права rwd); bob имеет только чтение (r).
|
||||
|
||||
\textbf{Метки конфиденциальности для MAC} — файлы \texttt{etc/subject\_labels} (субъекты) и \texttt{etc/object\_labels} (объекты). Формат: \texttt{идентификатор:уровень}, где уровень — 0 (несекретно), 1 (ДСП), 2 (секретно). Пример:
|
||||
|
||||
\begin{verbatim}
|
||||
# subject_labels
|
||||
alice:1
|
||||
bob:0
|
||||
|
||||
# object_labels
|
||||
report.txt:0
|
||||
secret.txt:2
|
||||
\end{verbatim}
|
||||
|
||||
Примеры содержимого файлов ACL и меток представлены на рисунках~\ref{fig:lab3-acl} и~\ref{fig:lab3-labels}.
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
% Заглушка: добавить скриншот img/lab3-acl.png
|
||||
\includegraphics[width=0.7\linewidth]{img/lab3-acl.png}
|
||||
\caption{Содержимое файла etc/acl (список доступа DAC)}
|
||||
\label{fig:lab3-acl}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[h!]
|
||||
\centering
|
||||
% Заглушка: добавить скриншот img/lab3-labels.png
|
||||
\includegraphics[width=0.7\linewidth]{img/lab3-labels.png}
|
||||
\caption{Содержимое файлов subject\_labels и object\_labels (метки MAC)}
|
||||
\label{fig:lab3-labels}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Выводы}
|
||||
|
||||
В ходе практической работы №3 на базе системы из работы №2 реализованы модели дискреционного и мандатного управления доступом. DAC реализован через список доступа (ACL) с владельцами объектов и командой \texttt{grant} для произвольного назначения прав. MAC реализован на основе модели Белла–Лападулы с тремя уровнями меток и проверкой свойств <<нет чтения сверху>> и <<нет записи вниз>>. Режимы проверки (BOTH, DAC\_ONLY, MAC\_ONLY) позволяют гибко настраивать систему; переключение доступно только администратору (root). Полученный опыт демонстрирует принципиальные различия между дискреционным и мандатным подходами к управлению доступом и их практическую реализацию в информационных системах.
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Межсетевое экранирование средствами iptables}
|
||||
|
||||
\subsection{Актуальность темы}
|
||||
|
||||
Межсетевое экранирование является одним из базовых механизмов сетевой защиты информационных систем. Локальный хостовый межсетевой экран позволяет минимизировать поверхность атаки, явно разрешая только необходимый трафик и блокируя все остальные соединения по принципу <<запрещено всё, что не разрешено>>. Практическая настройка \texttt{iptables} в рамках курса <<Защита информации>> позволяет закрепить представление о пакетной фильтрации, ролях цепочек \texttt{INPUT}, \texttt{OUTPUT}, \texttt{FORWARD} и о проверке работоспособности правил на реальном стенде.
|
||||
|
||||
\subsection{Цели и задачи работы}
|
||||
|
||||
Цель работы: получение базовых знаний по настройке межсетевого экрана в ОС Linux и реализация заданной политики доступа.
|
||||
|
||||
Задачи работы:
|
||||
\begin{enumerate}
|
||||
\item Изучить межсетевой экран \texttt{iptables}: принципы обработки пакетов, основные цепочки, действия по умолчанию, параметры правил фильтрации.
|
||||
\item Развернуть лабораторный стенд на базе виртуальных машин VirtualBox.
|
||||
\item Реализовать заданную политику доступа: разрешить loopback, DNS, исходящий ping, входящий ping только от конкретного адреса, HTTP/HTTPS; заблокировать всё остальное.
|
||||
\item Проверить корректность реализации политики.
|
||||
\item Проконтролировать прохождение пакетов утилитой \texttt{tcpdump}.
|
||||
\end{enumerate}
|
||||
|
||||
\subsection{Схема стенда}
|
||||
|
||||
Стенд реализован на базе двух виртуальных машин VirtualBox с ОС Ubuntu Server 22.04. Каждая машина имеет два сетевых адаптера:
|
||||
\begin{itemize}
|
||||
\item \textbf{Adapter~1 (NAT)} --- доступ в Интернет (DNS, HTTP/HTTPS, ICMP);
|
||||
\item \textbf{Adapter~2 (Internal Network <<intnet>>)} --- изолированная сеть \texttt{192.168.100.0/24} для связи между машинами.
|
||||
\end{itemize}
|
||||
|
||||
Схема стенда представлена на рисунке~\ref{fig:lab4-topology}, адресация узлов --- в таблице~\ref{tab:lab4-addr}.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.85\linewidth]{img/lab4-stand.png}
|
||||
\caption{Схема лабораторного стенда}
|
||||
\label{fig:lab4-topology}
|
||||
\end{figure}
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\caption{Адресация узлов стенда}
|
||||
\label{tab:lab4-addr}
|
||||
\begin{tabularx}{\textwidth}{l l l X}
|
||||
\toprule
|
||||
Узел & Интерфейс & IP-адрес & Назначение \\
|
||||
\midrule
|
||||
\multirow{2}{*}{firewall-host} & enp0s3 (NAT) & 10.0.2.15/24 & Доступ в Интернет \\
|
||||
& enp0s8 (intnet) & 192.168.100.1/24 & Внутренняя сеть \\
|
||||
\midrule
|
||||
\multirow{2}{*}{external-client} & enp0s3 (NAT) & 10.0.2.15/24 & Доступ в Интернет \\
|
||||
& enp0s8 (intnet) & 192.168.100.2/24 & Внутренняя сеть \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\end{table}
|
||||
|
||||
DNS-запросы обрабатываются службой \texttt{systemd-resolved} (\texttt{127.0.0.53}), перенаправляющей их DNS-серверу NAT-шлюза VirtualBox (\texttt{10.0.2.3}). Управление виртуальными машинами осуществлялось по SSH через проброс портов VirtualBox.
|
||||
|
||||
\subsection{Принципы обработки пакетов в \texttt{iptables}}
|
||||
|
||||
Утилита \texttt{iptables} организует правила фильтрации в виде таблиц и цепочек. В работе использовалась таблица \texttt{filter} (по умолчанию), содержащая три встроенные цепочки:
|
||||
\begin{itemize}
|
||||
\item \texttt{INPUT} --- входящие пакеты, адресованные локальным процессам;
|
||||
\item \texttt{OUTPUT} --- исходящие пакеты локальных процессов;
|
||||
\item \texttt{FORWARD} --- транзитные пакеты (маршрутизация).
|
||||
\end{itemize}
|
||||
|
||||
Пакет последовательно сравнивается с правилами цепочки. При совпадении выполняется действие: \texttt{ACCEPT} (пропустить), \texttt{DROP} (молча отбросить) или \texttt{REJECT} (отбросить с уведомлением). Если ни одно правило не сработало, применяется политика по умолчанию.
|
||||
|
||||
Модуль \texttt{conntrack} позволяет отслеживать состояния соединений. Правило с параметром \texttt{-{}-ctstate ESTABLISHED,RELATED} пропускает ответные пакеты уже установленных соединений без необходимости дублировать на цепочке \texttt{INPUT} отдельные правила по исходным портам для DNS, HTTP/HTTPS и для \texttt{echo-reply} на исходящий \texttt{ping}: при расположении этого правила \textbf{выше} узких правил ответный трафик срабатывает в нём, а счётчики у гипотетических строк \texttt{INPUT} с~\texttt{-{}-sport~53}/\texttt{80}/\texttt{443} оставались~бы нулевыми. На~\texttt{INPUT} явно заданы только новые входящие сценарии: SSH (для стенда), loopback и входящий \texttt{echo-request} с разрешённого адреса \texttt{192.168.100.2}.
|
||||
|
||||
\subsection{Реализация политики фильтрации}
|
||||
|
||||
Перед настройкой все существующие правила были сброшены:
|
||||
\begin{verbatim}
|
||||
sudo iptables -F && sudo iptables -X
|
||||
sudo iptables -t nat -F && sudo iptables -t mangle -F
|
||||
\end{verbatim}
|
||||
|
||||
Примеры команд добавления правил:
|
||||
|
||||
Разрешение loopback-трафика (\texttt{-i~lo} --- входящий интерфейс loopback):
|
||||
\begin{verbatim}
|
||||
sudo iptables -A INPUT -i lo -j ACCEPT
|
||||
\end{verbatim}
|
||||
|
||||
Разрешение DNS-запросов (\texttt{-p~udp}, порт~53):
|
||||
\begin{verbatim}
|
||||
sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
||||
\end{verbatim}
|
||||
|
||||
Разрешение входящего ping только от конкретного адреса (\texttt{-{}-icmp-type echo-request} --- тип~8, параметры \texttt{-s} и \texttt{-d} --- адреса источника и назначения):
|
||||
\begin{verbatim}
|
||||
sudo iptables -A INPUT -p icmp --icmp-type echo-request \
|
||||
-s 192.168.100.2 -d 192.168.100.1 -j ACCEPT
|
||||
\end{verbatim}
|
||||
|
||||
Полный набор правил, реализующий заданную политику доступа:
|
||||
|
||||
{\footnotesize
|
||||
\begin{verbatim}
|
||||
# SSH (для сохранения удалённого доступа к стенду)
|
||||
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
|
||||
sudo iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT
|
||||
# Уже установленные и связанные соединения
|
||||
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
# Loopback
|
||||
sudo iptables -A INPUT -i lo -j ACCEPT
|
||||
sudo iptables -A OUTPUT -o lo -j ACCEPT
|
||||
# DNS (UDP и TCP, порт 53; ответы на INPUT через ESTABLISHED,RELATED)
|
||||
sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
||||
sudo iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
|
||||
# HTTP и HTTPS (ответы на INPUT через ESTABLISHED,RELATED)
|
||||
sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
|
||||
sudo iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
|
||||
# Исходящий ping (echo-reply на INPUT через ESTABLISHED,RELATED)
|
||||
sudo iptables -A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||
# Входящий ping только от 192.168.100.2
|
||||
sudo iptables -A INPUT -p icmp --icmp-type echo-request \
|
||||
-s 192.168.100.2 -d 192.168.100.1 -j ACCEPT
|
||||
sudo iptables -A OUTPUT -p icmp --icmp-type echo-reply \
|
||||
-s 192.168.100.1 -d 192.168.100.2 -j ACCEPT
|
||||
# Политики по умолчанию
|
||||
sudo iptables -P INPUT DROP
|
||||
sudo iptables -P OUTPUT DROP
|
||||
sudo iptables -P FORWARD DROP
|
||||
\end{verbatim}
|
||||
}
|
||||
|
||||
Правило для SSH не входит в задание, но необходимо для сохранения удалённого доступа; оно добавлено до включения политик \texttt{DROP}. Отдельные правила \texttt{INPUT} с~\texttt{-{}-sport} для ответов DNS/HTTP(S) и с~\texttt{echo-reply} для исходящего \texttt{ping} не используются: этот трафик обрабатывается правилом \texttt{ESTABLISHED,RELATED}.
|
||||
|
||||
Итоговое состояние таблицы \texttt{filter} (\texttt{iptables -L -n -v -{}-line-numbers}):
|
||||
|
||||
{\scriptsize
|
||||
\begin{verbatim}
|
||||
Chain INPUT (policy DROP 0 packets, 0 bytes)
|
||||
num pkts bytes target prot in source destination
|
||||
1 162 10124 ACCEPT tcp * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
|
||||
2 312 142K ACCEPT all * 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
|
||||
3 12 780 ACCEPT all lo 0.0.0.0/0 0.0.0.0/0
|
||||
4 8 672 ACCEPT icmp * 192.168.100.2 192.168.100.1 icmptype 8
|
||||
|
||||
Chain FORWARD (policy DROP 0 packets, 0 bytes)
|
||||
|
||||
Chain OUTPUT (policy DROP 0 packets, 0 bytes)
|
||||
num pkts bytes target prot out source destination
|
||||
1 119 21028 ACCEPT tcp * 0.0.0.0/0 0.0.0.0/0 tcp spt:22
|
||||
2 289 33680 ACCEPT all * 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
|
||||
3 12 780 ACCEPT all * lo 0.0.0.0/0 0.0.0.0/0
|
||||
4 28 1982 ACCEPT udp * 0.0.0.0/0 0.0.0.0/0 udp dpt:53
|
||||
5 0 0 ACCEPT tcp * 0.0.0.0/0 0.0.0.0/0 tcp dpt:53
|
||||
6 4 240 ACCEPT tcp * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
|
||||
7 10 600 ACCEPT tcp * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443
|
||||
8 4 336 ACCEPT icmp * 0.0.0.0/0 0.0.0.0/0 icmptype 8
|
||||
9 8 672 ACCEPT icmp * 192.168.100.1 192.168.100.2 icmptype 0
|
||||
\end{verbatim}
|
||||
}
|
||||
|
||||
\subsection{Проверка реализованной политики}
|
||||
|
||||
\textbf{DNS.} Команда \texttt{nslookup} на firewall-host успешно разрешает имена:
|
||||
|
||||
{\footnotesize
|
||||
\begin{verbatim}
|
||||
$ nslookup example.com
|
||||
Server: 127.0.0.53
|
||||
Address: 127.0.0.53#53
|
||||
|
||||
Non-authoritative answer:
|
||||
Name: example.com
|
||||
Address: 104.18.26.120
|
||||
Name: example.com
|
||||
Address: 104.18.27.120
|
||||
\end{verbatim}
|
||||
}
|
||||
|
||||
\textbf{HTTP/HTTPS.} Утилита \texttt{curl} успешно получает ответ от внешнего сервера как по HTTP, так и по HTTPS:
|
||||
|
||||
{\footnotesize
|
||||
\begin{verbatim}
|
||||
$ curl http://example.com
|
||||
<!doctype html><html lang="en"><head><title>Example Domain
|
||||
</title>...</html>
|
||||
|
||||
$ curl https://example.com
|
||||
<!doctype html><html lang="en"><head><title>Example Domain
|
||||
</title>...</html>
|
||||
\end{verbatim}
|
||||
}
|
||||
|
||||
\textbf{Исходящий ping.} Защищаемый хост пингует внешние адреса:
|
||||
|
||||
{\footnotesize
|
||||
\begin{verbatim}
|
||||
$ ping -c 4 8.8.8.8
|
||||
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
|
||||
64 bytes from 8.8.8.8: icmp_seq=1 ttl=255 time=12.0 ms
|
||||
64 bytes from 8.8.8.8: icmp_seq=2 ttl=255 time=11.9 ms
|
||||
64 bytes from 8.8.8.8: icmp_seq=3 ttl=255 time=11.3 ms
|
||||
64 bytes from 8.8.8.8: icmp_seq=4 ttl=255 time=11.4 ms
|
||||
|
||||
--- 8.8.8.8 ping statistics ---
|
||||
4 packets transmitted, 4 received, 0% packet loss
|
||||
\end{verbatim}
|
||||
}
|
||||
|
||||
\textbf{Входящий ping от разрешённого адреса.} С external-client (\texttt{192.168.100.2}) ping проходит:
|
||||
|
||||
{\footnotesize
|
||||
\begin{verbatim}
|
||||
arity@external-client:~$ ping -c 4 192.168.100.1
|
||||
64 bytes from 192.168.100.1: icmp_seq=1 ttl=64 time=0.433 ms
|
||||
64 bytes from 192.168.100.1: icmp_seq=2 ttl=64 time=0.369 ms
|
||||
64 bytes from 192.168.100.1: icmp_seq=3 ttl=64 time=0.440 ms
|
||||
64 bytes from 192.168.100.1: icmp_seq=4 ttl=64 time=0.398 ms
|
||||
|
||||
--- 192.168.100.1 ping statistics ---
|
||||
4 packets transmitted, 4 received, 0% packet loss
|
||||
\end{verbatim}
|
||||
}
|
||||
|
||||
\textbf{Блокировка запрещённого трафика.} На узле \texttt{external-client} запущен \texttt{python3 -m http.server 8080}; исходящий TCP на порт~8080 в политику не входит (разрешены только 53, 80, 443), поэтому с \texttt{firewall-host} соединение не устанавливается:
|
||||
|
||||
{\footnotesize
|
||||
\begin{verbatim}
|
||||
$ timeout 5 nc -vz 192.168.100.2 8080
|
||||
nc: connect to 192.168.100.2 port 8080 (tcp) failed: Connection timed out
|
||||
\end{verbatim}
|
||||
}
|
||||
|
||||
\subsection{Контроль трафика средствами \texttt{tcpdump}}
|
||||
|
||||
Для подтверждения прохождения пакетов использовалась утилита \texttt{tcpdump}. Ниже приведён вывод при выполнении \texttt{ping} от external-client к firewall-host:
|
||||
|
||||
{\footnotesize
|
||||
\begin{verbatim}
|
||||
$ sudo tcpdump -i any -n icmp
|
||||
18:26:20.960046 enp0s8 In IP
|
||||
192.168.100.2 > 192.168.100.1:
|
||||
ICMP echo request, id 2, seq 1, length 64
|
||||
18:26:20.960119 enp0s8 Out IP
|
||||
192.168.100.1 > 192.168.100.2:
|
||||
ICMP echo reply, id 2, seq 1, length 64
|
||||
18:26:21.990542 enp0s8 In IP
|
||||
192.168.100.2 > 192.168.100.1:
|
||||
ICMP echo request, id 2, seq 2, length 64
|
||||
18:26:21.990587 enp0s8 Out IP
|
||||
192.168.100.1 > 192.168.100.2:
|
||||
ICMP echo reply, id 2, seq 2, length 64
|
||||
...
|
||||
8 packets captured
|
||||
\end{verbatim}
|
||||
}
|
||||
|
||||
На интерфейсе \texttt{enp0s8} видны входящие ICMP echo-request от \texttt{192.168.100.2} и исходящие echo-reply, что подтверждает работу правила входящего ping от разрешённого адреса.
|
||||
|
||||
Аналогичным образом были зафиксированы DNS-пакеты (\texttt{sudo tcpdump -i any -n port 53}): запрос от \texttt{127.0.0.1} к \texttt{127.0.0.53} через loopback, перенаправление к \texttt{10.0.2.3} через \texttt{enp0s3} и получение ответа. При выполнении \texttt{curl http://example.com} вывод \texttt{tcpdump} с фильтром \texttt{tcp port 80} зафиксировал трёхстороннее TCP-рукопожатие (SYN, SYN-ACK, ACK), HTTP-запрос \texttt{GET / HTTP/1.1} и ответ \texttt{HTTP/1.1 200 OK}, подтверждая корректную работу правила для порта~80.
|
||||
|
||||
\subsection{Выводы}
|
||||
|
||||
В ходе практической работы №4 на стенде из двух виртуальных машин VirtualBox настроена политика межсетевого экранирования средствами \texttt{iptables}. Реализована схема с политикой \texttt{DROP} по умолчанию и разрешающими правилами для loopback, DNS, исходящего ping, входящего ping от адреса \texttt{192.168.100.2} и HTTP/HTTPS; ответный трафик по этим сценариям на цепочке \texttt{INPUT} обрабатывается правилом \texttt{ESTABLISHED,RELATED}. Экспериментальная проверка подтвердила пропуск разрешённого трафика и блокировку соединений, не описанных в политике. Анализ трафика утилитой \texttt{tcpdump} продемонстрировал соответствие наблюдаемой сетевой активности заданным правилам фильтрации.
|
||||
|
||||
|
||||
\newpage
|
||||
\section{Разработка клиент-серверного приложения для конфиденциального обмена сообщениями}
|
||||
|
||||
\subsection{Актуальность темы}
|
||||
|
||||
Обеспечение конфиденциальности и целостности данных при передаче по сети является одной из ключевых задач информационной безопасности. Даже при использовании защищённых протоколов транспортного уровня понимание механизмов симметричного шифрования и контроля целостности на уровне приложения позволяет проектировать системы с дополнительным эшелоном защиты. Практическая реализация клиент-серверного приложения с шифрованием и хэш-контролем в рамках курса <<Защита информации>> закрепляет навыки работы с криптографическими примитивами и демонстрирует их влияние на передаваемый трафик.
|
||||
|
||||
\subsection{Цели и задачи работы}
|
||||
|
||||
Цель работы: разработка клиент-серверного приложения для обмена короткими сообщениями по протоколу TCP с обеспечением шифрования и контроля целостности.
|
||||
|
||||
Задачи работы:
|
||||
\begin{enumerate}
|
||||
\item Получить базовые знания по использованию криптографических функций библиотеки \texttt{cryptography} в Python.
|
||||
\item Разработать клиент-серверное приложение для обмена шифрованными сообщениями с контролем целостности.
|
||||
\item Проверить работу приложения в различных режимах, в том числе с помощью программы анализа трафика \texttt{tcpdump}.
|
||||
\end{enumerate}
|
||||
|
||||
\subsection{Описание алгоритма шифрования}
|
||||
|
||||
\textbf{3DES (Triple DES)} — симметричный блочный шифр, основанный на трёхкратном применении алгоритма DES в режиме EDE (Encrypt–Decrypt–Encrypt) с тремя независимыми подключами. Размер блока — 64 бита (8 байт), длина ключа — 192 бита (24 байта, из которых 168 бит являются эффективными ключевыми битами, а оставшиеся 24 бита — биты чётности). Несмотря на то что 3DES считается устаревшим и уступает AES по производительности, он по-прежнему обеспечивает достаточный уровень стойкости для учебных задач.
|
||||
|
||||
В разработанном приложении используется режим сцепления блоков \textbf{CBC} (Cipher Block Chaining): каждый блок открытого текста перед шифрованием складывается по модулю 2 с предыдущим блоком шифротекста (для первого блока — с вектором инициализации IV). Это обеспечивает зависимость каждого блока шифротекста от всех предшествующих блоков, затрудняя анализ повторяющихся фрагментов. Дополнение до границы блока выполняется по схеме PKCS7.
|
||||
|
||||
\textbf{Шифрование с затравкой (солью).} Для каждого сообщения генерируется случайная 16-байтовая затравка (salt). Ключ 3DES длиной 24 байта вычисляется как первые 24 байта хэша SHA-256 от конкатенации мастер-ключа из файла и затравки: $K_{\mathrm{3DES}} = \mathrm{SHA\text{-}256}(K_{\mathrm{file}} \mathbin\| \mathrm{salt})[{:}24]$. Кроме того, для каждого сообщения генерируется случайный 8-байтовый IV. Затравка и IV передаются вместе с шифротекстом в открытом виде, что позволяет получателю воспроизвести ключ. Благодаря уникальности затравки и IV одинаковые открытые тексты порождают различные шифротексты, что исключает атаки на основе повторения.
|
||||
|
||||
\textbf{Контроль целостности} реализован с помощью хэш-функции MD5 для открытого текста сообщения (пункт~2.4 задания: алгоритм хэширования в соответствии с индивидуальным заданием, как в практической работе №2). При включённой опции \texttt{-{}-integrity} отправитель вычисляет MD5-хэш открытого текста и передаёт его вместе с данными. Получатель вычисляет MD5 самостоятельно и сравнивает с полученным значением; при расхождении выводится предупреждение о нарушении целостности. Выработка 24-байтового ключа 3DES из файлового ключа и соли, напротив, выполняется через SHA-256 и не входит в требование п.~2.4 о хэше сообщения.
|
||||
|
||||
\textbf{HMAC-SHA256.} Дополнительно реализован режим HMAC (Hash-based Message Authentication Code) на основе SHA-256: $\mathrm{HMAC} = \mathrm{HMAC\text{-}SHA256}(K_{\mathrm{file}},\; \mathrm{plaintext} \mathbin\| \mathrm{salt})$. В отличие от дайджеста в режиме \texttt{-{}-integrity}, HMAC использует секретный ключ и обеспечивает аутентификацию отправителя. Режимы \texttt{-{}-integrity} (MD5 дайджест сообщения) и \texttt{-{}-hmac} (HMAC-SHA256) взаимоисключающие.
|
||||
|
||||
\subsection{Описание реализации}
|
||||
|
||||
Приложение реализовано на языке Python~3.14 в виде единого файла \texttt{main.py} (исходный код приведён в приложении~9). Для криптографических операций используется библиотека \texttt{cryptography}. Управление зависимостями осуществляется менеджером \texttt{uv}.
|
||||
|
||||
\textbf{Протокол обмена.} Сообщения передаются по TCP в формате: 4~байта длины (big-endian) + JSON-объект. Структура JSON-объекта:
|
||||
|
||||
\begin{verbatim}
|
||||
{
|
||||
"encrypted": true/false,
|
||||
"salt": "hex (16 байт)",
|
||||
"iv": "hex (8 байт)",
|
||||
"data": "hex шифротекст / открытый текст",
|
||||
"hash": "MD5 hex (если включён --integrity)"
|
||||
}
|
||||
\end{verbatim}
|
||||
|
||||
\textbf{Полнодуплексный обмен} обеспечивается двумя потоками: один читает ввод пользователя и отправляет сообщения, второй принимает входящие сообщения и выводит их на экран.
|
||||
|
||||
\textbf{Режимы запуска} задаются ключами командной строки: \texttt{-{}-encrypt} включает шифрование 3DES-CBC, \texttt{-{}-integrity} включает контроль целостности на основе MD5, \texttt{-{}-hmac} включает HMAC-SHA256 (несовместим с \texttt{-{}-integrity}), \texttt{-{}-test-integrity} активирует режим отправки сообщений с заведомо некорректным хэшем или HMAC (требует \texttt{-{}-integrity} или \texttt{-{}-hmac}).
|
||||
|
||||
\textbf{Защита ключа.} Мастер-ключ считывается из файла (по умолчанию \texttt{key.txt}). При запуске программа проверяет права доступа к файлу ключа и завершает работу с ошибкой, если файл доступен для чтения группе или другим пользователям (права более открытые, чем \texttt{600}).
|
||||
|
||||
\subsection{Команды сборки и запуска}
|
||||
|
||||
Установка зависимостей:
|
||||
\begin{verbatim}
|
||||
uv sync
|
||||
\end{verbatim}
|
||||
|
||||
Создание файла ключа:
|
||||
\begin{verbatim}
|
||||
openssl rand -hex 32 > key.txt
|
||||
chmod 600 key.txt
|
||||
\end{verbatim}
|
||||
|
||||
Запуск сервера и клиента (примеры):
|
||||
\begin{verbatim}
|
||||
uv run main.py server --port 9000
|
||||
uv run main.py client 127.0.0.1 9000
|
||||
|
||||
uv run main.py server --port 9000 --encrypt
|
||||
uv run main.py client 127.0.0.1 9000 --encrypt
|
||||
|
||||
uv run main.py server --port 9000 --encrypt --integrity
|
||||
uv run main.py client 127.0.0.1 9000 --encrypt --integrity
|
||||
\end{verbatim}
|
||||
|
||||
\subsection{Проверка работы приложения}
|
||||
|
||||
\subsubsection{Передача сообщений без шифрования}
|
||||
|
||||
При запуске без флагов сообщения передаются открытым текстом. На рисунке~\ref{fig:lab5-plain} показан вывод серверной и клиентской частей приложения.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.8\linewidth]{img/lab5-plain.png}
|
||||
\caption{Обмен сообщениями без шифрования}
|
||||
\label{fig:lab5-plain}
|
||||
\end{figure}
|
||||
|
||||
Анализ трафика утилитой \texttt{tcpdump} (рис.~\ref{fig:lab5-plain-tcpdump}) подтверждает, что текст сообщения передаётся в открытом виде и может быть прочитан непосредственно из дампа пакетов.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.85\linewidth]{img/lab5-plain-tcpdump.png}
|
||||
\caption{Дамп трафика (\texttt{tcpdump}) при передаче без шифрования — текст виден}
|
||||
\label{fig:lab5-plain-tcpdump}
|
||||
\end{figure}
|
||||
|
||||
\subsubsection{Передача сообщений с шифрованием}
|
||||
|
||||
При включённой опции \texttt{-{}-encrypt} сообщения шифруются алгоритмом 3DES-CBC с затравкой. На рисунке~\ref{fig:lab5-enc} показан вывод приложения: видны параметры шифрования (соль, производный ключ, IV, шифротекст) и результат расшифровки.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.85\linewidth]{img/lab5-enc.png}
|
||||
\caption{Обмен сообщениями с шифрованием 3DES-CBC}
|
||||
\label{fig:lab5-enc}
|
||||
\end{figure}
|
||||
|
||||
Дамп трафика (рис.~\ref{fig:lab5-enc-tcpdump}) показывает, что в пакетах присутствует только шифротекст в виде hex-строк; читаемый текст сообщения отсутствует.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.85\linewidth]{img/lab5-enc-tcpdump.png}
|
||||
\caption{Дамп трафика при передаче с шифрованием — текст не виден}
|
||||
\label{fig:lab5-enc-tcpdump}
|
||||
\end{figure}
|
||||
|
||||
\subsubsection{Шифрование с контролем целостности}
|
||||
|
||||
При одновременном использовании флагов \texttt{-{}-encrypt} и \texttt{-{}-integrity} к зашифрованному сообщению добавляется MD5-хэш открытого текста. На стороне получателя после расшифровки вычисляется собственный MD5 и сравнивается с полученным (рис.~\ref{fig:lab5-enc-int}).
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.85\linewidth]{img/lab5-enc-int.png}
|
||||
\caption{Обмен сообщениями с шифрованием и контролем целостности}
|
||||
\label{fig:lab5-enc-int}
|
||||
\end{figure}
|
||||
|
||||
\subsubsection{Тестирование контроля целостности с некорректным хэшем}
|
||||
|
||||
В режиме \texttt{-{}-test-integrity} отправитель намеренно передаёт некорректный хэш. Получатель обнаруживает расхождение и выводит предупреждение (рис.~\ref{fig:lab5-test-int}).
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.85\linewidth]{img/lab5-test-int.png}
|
||||
\caption{Обнаружение некорректного хэш-значения}
|
||||
\label{fig:lab5-test-int}
|
||||
\end{figure}
|
||||
|
||||
\subsubsection{Демонстрация шифрования с затравкой}
|
||||
|
||||
При отправке одного и того же сообщения дважды шифротексты различаются благодаря случайной затравке и IV (рис.~\ref{fig:lab5-salt}).
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.85\linewidth]{img/lab5-salt.png}
|
||||
\caption{Разные шифротексты для одинаковых сообщений (эффект затравки)}
|
||||
\label{fig:lab5-salt}
|
||||
\end{figure}
|
||||
|
||||
\subsubsection{Контроль целостности и аутентификация с HMAC-SHA256}
|
||||
|
||||
При использовании флага \texttt{-{}-hmac} вместо \texttt{-{}-integrity} к каждому сообщению прикладывается код HMAC-SHA256, вычисленный с использованием секретного ключа и случайной затравки. На рисунке~\ref{fig:lab5-hmac} показан обмен сообщениями в режиме \texttt{-{}-encrypt -{}-hmac}: видны HMAC-затравка, вычисленный HMAC на стороне отправителя и результат проверки на стороне получателя. При отправке одного и того же сообщения повторно значение HMAC отличается благодаря новой затравке.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.85\linewidth]{img/lab5-hmac.png}
|
||||
\caption{Обмен сообщениями с HMAC-SHA256: аутентификация и контроль целостности}
|
||||
\label{fig:lab5-hmac}
|
||||
\end{figure}
|
||||
|
||||
На рисунке~\ref{fig:lab5-hmac-fail} показана проверка режима \texttt{-{}-test-integrity} совместно с \texttt{-{}-hmac}: отправитель передаёт намеренно некорректный HMAC, получатель обнаруживает несоответствие и сообщает о неудачной аутентификации.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.85\linewidth]{img/lab5-hmac-fail.png}
|
||||
\caption{Обнаружение некорректного HMAC — отправитель не аутентифицирован}
|
||||
\label{fig:lab5-hmac-fail}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Выводы}
|
||||
|
||||
В ходе практической работы №5 разработано клиент-серверное приложение для конфиденциального обмена сообщениями по протоколу TCP. Реализовано шифрование 3DES-CBC с затравкой (ключ сессии из SHA-256), обеспечивающей различные шифротексты при одинаковых открытых текстах, контроль целостности сообщения на основе MD5 (как в индивидуальном задании и практической работе №2) и дополнительный режим HMAC-SHA256 для аутентификации отправителя. Экспериментальная проверка с помощью \texttt{tcpdump} подтвердила, что при включённом шифровании содержимое сообщений не передаётся в открытом виде, а при включённом контроле целостности получатель успешно обнаруживает намеренно повреждённые сообщения.
|
||||
|
||||
Теоретический анализ показал, что количество итераций, необходимых для полного перебора паролей, экспоненциально возрастает с увеличением их длины. Экспериментальное исследование позволило оценить реальную скорость перебора и подтвердить теоретические оценки. Полученные результаты демонстрируют, что использование паролей длиной 6 символов и более существенно затрудняет атаку методом грубой силы, а пароли длиной 8 и более символов при применении SHA-256 практически не поддаются взлому за приемлемое время.
|
||||
|
||||
\newpage
|
||||
\section*{Заключение}
|
||||
@@ -554,10 +1130,63 @@ N(n) = 52 \cdot 72^{n-1}
|
||||
|
||||
В ходе выполнения практической работы №1 был проведён анализ уязвимостей программного обеспечения с использованием Банка данных угроз ФСТЭК России. Были изучены структура разделов <<Угрозы>> и <<Уязвимости>>, определены версии операционных систем личного компьютера и смартфона, а также выполнен поиск уязвимостей с уровнем опасности <<Критический>> и <<Высокий>>. Проведён анализ наиболее актуальных уязвимостей, рассмотрены их характеристики и векторы CVSS, а также рекомендации по устранению. В процессе выполнения работы были получены практические навыки поиска, анализа и оценки уязвимостей информационных систем.
|
||||
|
||||
В ходе выполнения практической работы №2 была разработана система доступа пользователей к конфиденциальным данным. Реализованы утилита управления пользователями с хэшированием паролей по алгоритму SHA-256, утилита доступа с разграничением прав и журналированием операций, а также программа взлома паролей методом грубой силы. Проведено исследование стойкости паролей в зависимости от их длины, результаты которого подтвердили экспоненциальную зависимость числа итераций от длины пароля и продемонстрировали практическую устойчивость достаточно длинных паролей к атаке полного перебора.
|
||||
В ходе выполнения практической работы №2 была разработана система доступа пользователей к конфиденциальным данным. Реализованы утилита управления пользователями с хэшированием паролей по алгоритму MD5 (индивидуальный вариант задания), утилита доступа с разграничением прав и журналированием операций, а также программа взлома паролей методом грубой силы. Проведено исследование стойкости паролей в зависимости от их длины, результаты которого подтвердили экспоненциальную зависимость числа итераций от длины пароля и продемонстрировали практическую устойчивость достаточно длинных паролей к атаке полного перебора.
|
||||
|
||||
В ходе выполнения практической работы №3 на базе системы из работы №2 реализованы модели дискреционного (DAC) и мандатного (MAC) управления доступом. DAC реализован через список доступа с владельцами объектов и командой выдачи прав; MAC — на основе модели Белла–Лападулы с тремя уровнями меток конфиденциальности. Реализация демонстрирует принципиальные различия между подходами и их практическое применение.
|
||||
|
||||
В ходе выполнения практической работы №4 на стенде из двух виртуальных машин VirtualBox настроена политика межсетевого экранирования средствами \texttt{iptables}. Реализованы правила фильтрации, разрешающие loopback-трафик, DNS, ICMP и HTTP/HTTPS, а все прочие соединения блокируются. Корректность работы правил подтверждена тестовыми соединениями и анализом пакетов утилитой \texttt{tcpdump}.
|
||||
|
||||
В ходе выполнения практической работы №5 разработано клиент-серверное приложение для конфиденциального обмена сообщениями по протоколу TCP. Реализовано шифрование 3DES-CBC с затравкой (производный ключ через SHA-256), контроль целостности сообщения MD5 и режим HMAC-SHA256 для аутентификации отправителя. Анализ трафика утилитой \texttt{tcpdump} подтвердил, что при включённом шифровании содержимое сообщений не доступно в открытом виде, а режим тестирования продемонстрировал корректное обнаружение нарушений целостности.
|
||||
|
||||
\newpage
|
||||
\printbibliography[heading=bibintoc]
|
||||
|
||||
\newpage
|
||||
\section*{Приложение 1. Исходный код файла usermgr.py из практической работы №2}
|
||||
\addcontentsline{toc}{section}{Приложение 1. Исходный код файла usermgr.py из практической работы №2}
|
||||
\label{app:usermgr}
|
||||
\lstinputlisting{../lab2/usermgr.py}
|
||||
|
||||
\newpage
|
||||
\section*{Приложение 2. Исходный код файла access.py из практической работы №2}
|
||||
\addcontentsline{toc}{section}{Приложение 2. Исходный код файла access.py из практической работы №2}
|
||||
\lstinputlisting{../lab2/access.py}
|
||||
|
||||
\newpage
|
||||
\section*{Приложение 3. Исходный код файла bruteforce.py из практической работы №2}
|
||||
\addcontentsline{toc}{section}{Приложение 3. Исходный код файла bruteforce.py из практической работы №2}
|
||||
\label{app:bruteforce}
|
||||
\lstinputlisting{../lab2/bruteforce.py}
|
||||
|
||||
\newpage
|
||||
\section*{Приложение 4. Исходный код файла setup.sh из практической работы №2}
|
||||
\addcontentsline{toc}{section}{Приложение 4. Исходный код файла setup.sh из практической работы №2}
|
||||
\lstinputlisting{../lab2/setup.sh}
|
||||
|
||||
\newpage
|
||||
\section*{Приложение 5. Исходный код файла usermgr.py из практической работы №3}
|
||||
\addcontentsline{toc}{section}{Приложение 5. Исходный код файла usermgr.py из практической работы №3}
|
||||
\lstinputlisting{../lab3/usermgr.py}
|
||||
|
||||
\newpage
|
||||
\section*{Приложение 6. Исходный код файла confaccess.py из практической работы №3}
|
||||
\addcontentsline{toc}{section}{Приложение 6. Исходный код файла confaccess.py из практической работы №3}
|
||||
\lstinputlisting{../lab3/confaccess.py}
|
||||
|
||||
\newpage
|
||||
\section*{Приложение 7. Исходный код файла config.py из практической работы №3}
|
||||
\addcontentsline{toc}{section}{Приложение 7. Исходный код файла config.py из практической работы №3}
|
||||
\lstinputlisting{../lab3/config.py}
|
||||
|
||||
\newpage
|
||||
\section*{Приложение 8. Исходный код файла setup.sh из практической работы №3}
|
||||
\addcontentsline{toc}{section}{Приложение 8. Исходный код файла setup.sh из практической работы №3}
|
||||
\lstinputlisting{../lab3/setup.sh}
|
||||
|
||||
\newpage
|
||||
\section*{Приложение 9. Исходный код файла main.py из практической работы №5}
|
||||
\addcontentsline{toc}{section}{Приложение 9. Исходный код файла main.py из практической работы №5}
|
||||
\label{app:lab5-main}
|
||||
\lstinputlisting{../lab5/main.py}
|
||||
|
||||
\end{document}
|
||||