Compare commits
12 Commits
lab1
...
c73f7dc042
| Author | SHA1 | Date | |
|---|---|---|---|
| c73f7dc042 | |||
| d3a40d4ef7 | |||
| 7c83fe5e1a | |||
| 642e3cf0c7 | |||
| c373e8f5d9 | |||
| c62f6284d2 | |||
| 8aeb9a4244 | |||
| 37eff0ed9b | |||
| b6b5e16d63 | |||
| bc1299e966 | |||
| 60a4471a8c | |||
| 07e02401eb |
345
lab1/report.tex
@@ -1,345 +0,0 @@
|
|||||||
\documentclass[a4paper, final]{article}
|
|
||||||
%\usepackage{literat} % Нормальные шрифты
|
|
||||||
\usepackage[14pt]{extsizes} % для того чтобы задать нестандартный 14-ый размер шрифта
|
|
||||||
\usepackage{tabularx}
|
|
||||||
\usepackage{booktabs}
|
|
||||||
\usepackage[T2A]{fontenc}
|
|
||||||
\usepackage[utf8]{inputenc}
|
|
||||||
\usepackage[russian]{babel}
|
|
||||||
\usepackage{amsmath}
|
|
||||||
\usepackage[left=25mm, top=20mm, right=20mm, bottom=20mm, footskip=10mm]{geometry}
|
|
||||||
\usepackage{ragged2e} %для растягивания по ширине
|
|
||||||
\usepackage{setspace} %для межстрочного интервала
|
|
||||||
\usepackage{moreverb} %для работы с листингами
|
|
||||||
\usepackage{indentfirst} % для абзацного отступа
|
|
||||||
\usepackage{moreverb} %для печати в листинге исходного кода программ
|
|
||||||
\usepackage{pdfpages} %для вставки других pdf файлов
|
|
||||||
\usepackage{tikz}
|
|
||||||
\usepackage{graphicx}
|
|
||||||
\usepackage{afterpage}
|
|
||||||
\usepackage{longtable}
|
|
||||||
\usepackage{float}
|
|
||||||
|
|
||||||
% Рекомендуется для biblatex (кавычки/локализация цитат и т.п.)
|
|
||||||
\usepackage{csquotes}
|
|
||||||
|
|
||||||
% ГОСТ-стили для biblatex
|
|
||||||
\usepackage[
|
|
||||||
backend=biber,
|
|
||||||
bibstyle=gost-numeric, % ссылки вида: [1]
|
|
||||||
citestyle=gost-numeric,
|
|
||||||
sorting=none % порядок в списке = по первому цитированию
|
|
||||||
]{biblatex}
|
|
||||||
|
|
||||||
% Все источники хранятся в отдельном файле
|
|
||||||
\addbibresource{refs.bib}
|
|
||||||
|
|
||||||
\renewcommand*{\bibfont}{\small}
|
|
||||||
|
|
||||||
% \usepackage[paper=A4,DIV=12]{typearea}
|
|
||||||
\usepackage{pdflscape}
|
|
||||||
% \usepackage{lscape}
|
|
||||||
|
|
||||||
\usepackage{array}
|
|
||||||
\usepackage{multirow}
|
|
||||||
|
|
||||||
\renewcommand\verbatimtabsize{4\relax}
|
|
||||||
\renewcommand\listingoffset{0.2em} %отступ от номеров строк в листинге
|
|
||||||
\renewcommand{\arraystretch}{1.4} % изменяю высоту строки в таблице
|
|
||||||
\usepackage[font=small, singlelinecheck=false, justification=centering, format=plain, labelsep=period]{caption} %для настройки заголовка таблицы
|
|
||||||
\usepackage{listings} %листинги
|
|
||||||
\usepackage{xcolor} % цвета
|
|
||||||
% \usepackage{hyperref}% для гиперссылок
|
|
||||||
\usepackage{enumitem} %для перечислений
|
|
||||||
|
|
||||||
\newcommand{\specialcell}[2][l]{\begin{tabular}[#1]{@{}l@{}}#2\end{tabular}}
|
|
||||||
|
|
||||||
|
|
||||||
\setlist[enumerate,itemize]{leftmargin=1.2cm} %отступ в перечислениях
|
|
||||||
|
|
||||||
% \hypersetup{colorlinks,
|
|
||||||
% allcolors=[RGB]{010 090 200}} %красивые гиперссылки (не красные)
|
|
||||||
|
|
||||||
% подгружаемые языки — подробнее в документации listings (это всё для листингов)
|
|
||||||
\lstloadlanguages{ SQL}
|
|
||||||
% включаем кириллицу и добавляем кое−какие опции
|
|
||||||
\lstset{tabsize=2,
|
|
||||||
breaklines,
|
|
||||||
basicstyle=\footnotesize,
|
|
||||||
columns=fullflexible,
|
|
||||||
flexiblecolumns,
|
|
||||||
numbers=left,
|
|
||||||
numberstyle={\footnotesize},
|
|
||||||
keywordstyle=\color{blue},
|
|
||||||
inputencoding=cp1251,
|
|
||||||
extendedchars=true
|
|
||||||
}
|
|
||||||
\lstdefinelanguage{MyC}{
|
|
||||||
language=SQL,
|
|
||||||
% ndkeywordstyle=\color{darkgray}\bfseries,
|
|
||||||
% identifierstyle=\color{black},
|
|
||||||
% morecomment=[n]{/**}{*/},
|
|
||||||
% commentstyle=\color{blue}\ttfamily,
|
|
||||||
% stringstyle=\color{red}\ttfamily,
|
|
||||||
% morestring=[b]",
|
|
||||||
% showstringspaces=false,
|
|
||||||
% morecomment=[l][\color{gray}]{//},
|
|
||||||
keepspaces=true,
|
|
||||||
escapechar=\%,
|
|
||||||
texcl=true
|
|
||||||
}
|
|
||||||
|
|
||||||
\textheight=24cm % высота текста
|
|
||||||
\textwidth=16cm % ширина текста
|
|
||||||
\oddsidemargin=0pt % отступ от левого края
|
|
||||||
\topmargin=-1.5cm % отступ от верхнего края
|
|
||||||
\parindent=24pt % абзацный отступ
|
|
||||||
\parskip=5pt % интервал между абзацами
|
|
||||||
\tolerance=2000 % терпимость к "жидким" строкам
|
|
||||||
\flushbottom % выравнивание высоты страниц
|
|
||||||
|
|
||||||
|
|
||||||
% Настройка листингов
|
|
||||||
\lstset{
|
|
||||||
language=python,
|
|
||||||
extendedchars=\true,
|
|
||||||
inputencoding=utf8,
|
|
||||||
keepspaces=true,
|
|
||||||
% captionpos=b, % подписи листингов снизу
|
|
||||||
}
|
|
||||||
|
|
||||||
% Настройка содержания
|
|
||||||
\usepackage{tocloft}
|
|
||||||
\usepackage[hidelinks]{hyperref}
|
|
||||||
|
|
||||||
% section в содержании НЕ жирным
|
|
||||||
\renewcommand{\cftsecfont}{\normalfont}
|
|
||||||
\renewcommand{\cftsecpagefont}{\normalfont}
|
|
||||||
|
|
||||||
% убрать отступ у subsection
|
|
||||||
\setlength{\cftsubsecindent}{0pt}
|
|
||||||
|
|
||||||
% subsubsection курсивом
|
|
||||||
\usepackage{titlesec}
|
|
||||||
|
|
||||||
\titleformat{\subsubsection}
|
|
||||||
{\normalfont\large\itshape} % стиль: обычный + курсив
|
|
||||||
{\thesubsubsection} % номер (убери если не нужен)
|
|
||||||
{1em}
|
|
||||||
{}
|
|
||||||
|
|
||||||
|
|
||||||
\begin{document}
|
|
||||||
% ТИТУЛЬНЫЙ ЛИСТ
|
|
||||||
\begin{center}
|
|
||||||
\hfill \break
|
|
||||||
\hfill \break
|
|
||||||
\normalsize{МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ\\
|
|
||||||
федеральное государственное автономное образовательное учреждение высшего образования «Санкт-Петербургский политехнический университет Петра Великого»\\[10pt]}
|
|
||||||
\normalsize{Институт компьютерных наук и кибербезопасности}\\[10pt]
|
|
||||||
\normalsize{Высшая школа технологий искусственного интеллекта}\\[10pt]
|
|
||||||
\normalsize{Направление: 02.03.01 <<Математика и компьютерные науки>>}\\
|
|
||||||
|
|
||||||
\hfill \break
|
|
||||||
\hfill \break
|
|
||||||
\hfill \break
|
|
||||||
\large{Отчёт по практическим работам по дисциплине}\\
|
|
||||||
\large{<<Защита информации>>}\\
|
|
||||||
|
|
||||||
\hfill \break
|
|
||||||
\hfill \break
|
|
||||||
\hfill \break
|
|
||||||
\end{center}
|
|
||||||
|
|
||||||
\small{
|
|
||||||
\begin{tabular}{lrrl}
|
|
||||||
\!\!\!Студент, & \hspace{2cm} & & \\
|
|
||||||
\!\!\!группы 5130201/20101 & \hspace{2cm} & \underline{\hspace{3cm}} & Тищенко А. А. \\\\
|
|
||||||
\!\!\!Руководитель & \hspace{2cm} & & \\
|
|
||||||
\!\!\! & \hspace{2cm} & \underline{\hspace{3cm}} & Силиненко А. В. \\\\
|
|
||||||
&&\hspace{4cm}
|
|
||||||
\end{tabular}
|
|
||||||
\begin{flushright}
|
|
||||||
<<\underline{\hspace{1cm}}>>\underline{\hspace{2.5cm}} 2026г.
|
|
||||||
\end{flushright}
|
|
||||||
}
|
|
||||||
|
|
||||||
\hfill \break
|
|
||||||
\hfill \break
|
|
||||||
\begin{center} \small{Санкт-Петербург, 2026} \end{center}
|
|
||||||
\thispagestyle{empty} % выключаем отображение номера для этой страницы
|
|
||||||
|
|
||||||
\newpage
|
|
||||||
\tableofcontents
|
|
||||||
\thispagestyle{empty} % выключаем отображение номера для этой страницы
|
|
||||||
|
|
||||||
\newpage
|
|
||||||
\section*{Введение}
|
|
||||||
\addcontentsline{toc}{section}{Введение}
|
|
||||||
|
|
||||||
В рамках курса <<Защита информации>> было выполнено несколько практических работ, по результатам которых был составлен данный отчёт, содержащий информацию по всем практическим работам. Отчетная информация по каждой работе – это отдельный раздел в общем отчете.
|
|
||||||
|
|
||||||
В отчёте представлены результаты следующих практических работ:
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Практическая работа №1. Анализ уязвимостей программного обеспечения. Данная практическая работа посвящена анализу уязвимостей программного обеспечения с использованием Банка данных угроз безопасности информации ФСТЭК России~\cite{fstec-bdu}. В ходе выполнения работы предусмотрено изучение структуры разделов «Угрозы» и «Уязвимости», а также поиск уязвимостей по заданным критериям. Особое внимание уделяется выявлению уязвимостей, соответствующих используемым версиям операционных систем личных устройств, и рассмотрению возможных мер по их устранению.
|
|
||||||
\end{enumerate}
|
|
||||||
|
|
||||||
\newpage
|
|
||||||
\section{Практическая работа №1. Анализ уязвимостей программного обеспечения}
|
|
||||||
\subsection{Знакомство с разделом <<Угрозы>> Банка угроз}
|
|
||||||
Раздел <<Угрозы>> Банка данных угроз безопасности информации ФСТЭК России предназначен для систематизированного представления сведений об актуальных угрозах безопасности информации. Данный раздел содержит структурированную информацию, позволяющую оценить характер угроз, возможные последствия их реализации и способы противодействия.
|
|
||||||
|
|
||||||
Раздел включает в себя следующие основные аспекты:
|
|
||||||
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Классификация угроз: угрозы распределяются по различным категориям в зависимости от источника возникновения, способа реализации и объекта воздействия. Это позволяет упорядочить информацию и упростить её анализ.
|
|
||||||
|
|
||||||
\item Описание угроз: для каждой угрозы приводится развернутое описание, включающее возможные сценарии реализации, цели нарушителя и потенциальные последствия для информационной системы.
|
|
||||||
|
|
||||||
\item Объекты воздействия: указываются типы информационных систем, ресурсов или процессов, на которые может быть направлена угроза.
|
|
||||||
|
|
||||||
\item Уровень опасности: каждой угрозе присваивается определённый уровень опасности, отражающий степень потенциального ущерба при её реализации.
|
|
||||||
|
|
||||||
\item Рекомендации по противодействию: приводятся общие меры и подходы, направленные на предупреждение реализации угрозы или снижение возможных негативных последствий.
|
|
||||||
\end{enumerate}
|
|
||||||
|
|
||||||
\subsection{Знакомство с разделом <<Уязвимости>> Банка угроз}
|
|
||||||
|
|
||||||
Раздел <<Уязвимости>> Банка данных угроз безопасности информации ФСТЭК России предназначен для получения сведений об актуальных уязвимостях программного обеспечения и их характеристиках. В рамках работы были рассмотрены подразделы: <<Список уязвимостей>>, <<Наиболее опасные уязвимости>> и <<Инфографика>>.
|
|
||||||
|
|
||||||
Список уязвимостей представляет собой структурированный перечень выявленных уязвимостей в программном обеспечении. Для каждой уязвимости указывается идентификатор, наименование, описание, затронутое программное обеспечение и его версии, уровень опасности, а также оценка по шкале CVSS. Уязвимости классифицируются по уровню критичности (критический, высокий, средний и др.), что позволяет определить приоритетность их устранения. Также приводятся рекомендации по устранению или минимизации последствий эксплуатации уязвимости.
|
|
||||||
|
|
||||||
Подраздел <<Наиболее опасные уязвимости>> содержит перечень уязвимостей с наивысшим уровнем опасности. Как правило, к ним относятся уязвимости, позволяющие выполнить удалённое выполнение кода, повысить привилегии, обойти механизмы аутентификации или получить несанкционированный доступ к информации. Данный раздел позволяет оперативно определить наиболее критичные риски для информационных систем.
|
|
||||||
|
|
||||||
\begin{figure}[h!]
|
|
||||||
\centering
|
|
||||||
\includegraphics[width=0.7\linewidth]{img/top_vulnerabilities.png}
|
|
||||||
\caption{Пример отображения наиболее опасных уязвимостей}
|
|
||||||
\label{fig:top_vuln}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
Подраздел <<Инфографика>> предназначен для наглядного представления статистических данных по уязвимостям. В нём отображается распределение уязвимостей по уровням опасности, типам ошибок, видам программного обеспечения и производителям. Инфографика позволяет визуально оценить текущее состояние защищённости программных продуктов и выявить наиболее проблемные направления.
|
|
||||||
|
|
||||||
\begin{figure}[h!]
|
|
||||||
\centering
|
|
||||||
\includegraphics[width=0.7\linewidth]{img/vulnerability_stats.png}
|
|
||||||
\caption{Пример статистического распределения уязвимостей}
|
|
||||||
\label{fig:stats_vuln}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
\newpage
|
|
||||||
\subsection{Версии ОС, используемые в личных устройствах}
|
|
||||||
|
|
||||||
На личном компьютере используется операционная система Ubuntu 25.10 (см. Рис.~\ref{fig:laptop-os-version}).
|
|
||||||
|
|
||||||
\begin{figure}[h!]
|
|
||||||
\centering
|
|
||||||
\includegraphics[width=0.5\linewidth]{img/laptop-os-version.png}
|
|
||||||
\caption{Версия операционной системы личного компьютера}
|
|
||||||
\label{fig:laptop-os-version}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
На личном мобильном устройстве используется операционная система Android 11 с графической оболочкой MIUI Global 12.5.14, сборка 12.5.14.0 RKURUXM (см. Рис.~\ref{fig:mobile-os-version}).
|
|
||||||
|
|
||||||
\begin{figure}[h!]
|
|
||||||
\centering
|
|
||||||
\includegraphics[width=0.5\linewidth]{img/mobile-os-version.jpg}
|
|
||||||
\caption{Версия операционной системы личного мобильного устройства}
|
|
||||||
\label{fig:mobile-os-version}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
\subsection{Уязвимости ОС личного компьютера}
|
|
||||||
|
|
||||||
Для поиска уязвимостей ОС личного компьютера на сайте Банка данных угроз безопасности информации ФСТЭК России были использованы следующие критерии:
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Операционная система: Ubuntu 25.10.
|
|
||||||
\item Уровень опасности: высокий.
|
|
||||||
\end{enumerate}
|
|
||||||
Использовался уровень опасности <<высокий>>, так как критических уязвимостей для данной ОС не найдено.
|
|
||||||
|
|
||||||
По заданным критериям было найдено 10 уязвимостей (см. Рис.~\ref{fig:laptop-vulnerabilities}).
|
|
||||||
|
|
||||||
\begin{figure}[h!]
|
|
||||||
\centering
|
|
||||||
\includegraphics[width=0.8\linewidth]{img/laptop-vulnerabilities.png}
|
|
||||||
\caption{Уязвимости ОС личного компьютера}
|
|
||||||
\label{fig:laptop-vulnerabilities}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
Наиболее свежей является уязвимость BDU:2025-15300 <<Уязвимость интерфейсов cpu\_latency\_qos\_add, remove, update\_request модуля drivers/ufs/core/ufs-sysfs.c драйвера поддержки устройств SCSI ядра операционной системы Linux связана с ошибками синхронизации при использовании общего ресурса («Ситуация гонки»). Эксплуатация уязвимости может позволить нарушителю, действующему удаленно, вызвать отказ в обслуживании>>. Развёрнутое описание уязвимости представлено на рисунке~\ref{fig:laptop-vulnerabilities-details}.
|
|
||||||
|
|
||||||
Базовый вектор уязвимости CVSS 2.0: AV:A/AC:L/Au:S/C:C/I:C/A:C.
|
|
||||||
|
|
||||||
AV: A (Access Vector: Adjacent Network) — параметр, указывающий на то, что атакующий должен находиться в той же локальной сети или в непосредственной сетевой близости (например, в одной Wi-Fi сети) для реализации атаки.
|
|
||||||
|
|
||||||
AC: L (Access Complexity: Low) — низкая сложность эксплуатации уязвимости. Для успешной атаки не требуется выполнения сложных условий или специальной подготовки среды.
|
|
||||||
|
|
||||||
Au: S (Authentication: Single) — для осуществления атаки требуется прохождение аутентификации один раз. Это означает, что атакующий должен обладать учетной записью или иным способом пройти проверку подлинности.
|
|
||||||
|
|
||||||
C: C (Confidentiality Impact: Complete) — полное нарушение конфиденциальности. Уязвимость позволяет получить полный доступ к защищаемой информации.
|
|
||||||
|
|
||||||
I: C (Integrity Impact: Complete) — полное нарушение целостности. Злоумышленник может изменять или уничтожать данные без ограничений.
|
|
||||||
|
|
||||||
A: C (Availability Impact: Complete) — полное нарушение доступности. Эксплуатация уязвимости может привести к отказу в обслуживании или полной недоступности системы.
|
|
||||||
|
|
||||||
\begin{figure}[h!]
|
|
||||||
\centering
|
|
||||||
\includegraphics[width=0.9\linewidth]{img/laptop-vulnerabilities-details.png}
|
|
||||||
\caption{Развёрнутое описание уязвимости BDU:2025-15300}
|
|
||||||
\label{fig:laptop-vulnerabilities-details}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
Уязвимость была устранена в версии 25.10-6.17.0-14.14, поэтому для её устранения достаточно было обновить ОС до этой версии.
|
|
||||||
|
|
||||||
\newpage
|
|
||||||
\subsection{Уязвимости ОС личного мобильного устройства}
|
|
||||||
|
|
||||||
Для поиска уязвимостей ОС личного мобильного устройства на сайте Банка данных угроз безопасности информации ФСТЭК России были использованы следующие критерии:
|
|
||||||
\begin{enumerate}
|
|
||||||
\item Операционная система: Android 11.
|
|
||||||
\item Уровень опасности: критический.
|
|
||||||
\end{enumerate}
|
|
||||||
|
|
||||||
По заданным критериям была найдена 1 уязвимость (см. Рис.~\ref{fig:mobile-vulnerabilities}).
|
|
||||||
|
|
||||||
\begin{figure}[h!]
|
|
||||||
\centering
|
|
||||||
\includegraphics[width=0.8\linewidth]{img/mobile-vulnerabilities.png}
|
|
||||||
\caption{Уязвимости ОС личного мобильного устройства}
|
|
||||||
\label{fig:mobile-vulnerabilities}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
Развёрнутое описание уязвимости BDU:2023-08587 <<Уязвимость функции \\ callback\_thread\_event (com\_android\_bluetooth\_btservice\_AdapterService.cpp) операционной системы Android связана с использованием памяти после её освобождения. Эксплуатация уязвимости может позволить нарушителю, действующему удалённо, выполнить произвольный код>>, представлено на рисунке~\ref{fig:mobile-vulnerabilities-details}.
|
|
||||||
|
|
||||||
Базовый вектор уязвимости CVSS 2.0: AV:N/AC:L/Au:N/C:C/I:C/A:C.
|
|
||||||
|
|
||||||
AV: N (Access Vector: Network) — параметр, указывающий на то, что атакующий может осуществить атаку удалённо через сеть (например, через Интернет), без необходимости физического доступа или нахождения в локальной сети.
|
|
||||||
|
|
||||||
AC: L (Access Complexity: Low) — низкая сложность эксплуатации уязвимости. Для успешной атаки не требуется специальных условий или сложной подготовки.
|
|
||||||
|
|
||||||
Au: N (Authentication: None) — для эксплуатации уязвимости не требуется аутентификация. Атакующий может выполнить атаку без наличия учетной записи или прохождения процедуры входа в систему.
|
|
||||||
|
|
||||||
C: C (Confidentiality Impact: Complete) — полное нарушение конфиденциальности. Уязвимость позволяет злоумышленнику получить полный доступ к конфиденциальной информации.
|
|
||||||
|
|
||||||
I: C (Integrity Impact: Complete) — полное нарушение целостности. Атакующий может изменять, подменять или удалять данные без ограничений.
|
|
||||||
|
|
||||||
A: C (Availability Impact: Complete) — полное нарушение доступности. Эксплуатация уязвимости может привести к полной недоступности системы или отказу в обслуживании.
|
|
||||||
|
|
||||||
\begin{figure}[h!]
|
|
||||||
\centering
|
|
||||||
\includegraphics[width=0.9\linewidth]{img/mobile-vulnerabilities-details.png}
|
|
||||||
\caption{Развёрнутое описание уязвимости BDU:2023-08587}
|
|
||||||
\label{fig:mobile-vulnerabilities-details}
|
|
||||||
\end{figure}
|
|
||||||
|
|
||||||
\newpage
|
|
||||||
\section*{Заключение}
|
|
||||||
\addcontentsline{toc}{section}{Заключение}
|
|
||||||
|
|
||||||
В ходе выполнения практической работы №1 был проведён анализ уязвимостей программного обеспечения с использованием Банка данных угроз ФСТЭК России. Были изучены структура разделов <<Угрозы>> и <<Уязвимости>>, определены версии операционных систем личного компьютера и смартфона, а также выполнен поиск уязвимостей с уровнем опасности <<Критический>> и <<Высокий>>. Проведён анализ наиболее актуальных уязвимостей, рассмотрены их характеристики и векторы CVSS, а также рекомендации по устранению. В процессе выполнения работы были получены практические навыки поиска, анализа и оценки уязвимостей информационных систем.
|
|
||||||
|
|
||||||
\newpage
|
|
||||||
\printbibliography[heading=bibintoc]
|
|
||||||
|
|
||||||
|
|
||||||
\end{document}
|
|
||||||
1
lab2/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.pyc
|
||||||
101
lab2/README.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Lab 2 — Authentication, Authorization & Brute Force Research
|
||||||
|
|
||||||
|
Система доступа к конфиденциальным данным с управлением пользователями и исследованием стойкости паролей.
|
||||||
|
|
||||||
|
## Структура каталогов
|
||||||
|
|
||||||
|
```
|
||||||
|
$PRACTICE2_DIR/ # по умолчанию /usr/local/practice2
|
||||||
|
├── etc/passwd # логин:md5:id:права:ФИО
|
||||||
|
├── confdata/ # конфиденциальные файлы
|
||||||
|
├── bin/ # утилиты (usermgr, confaccess, bruteforce)
|
||||||
|
└── log/ # usermgr.log, access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Базовый каталог задаётся через переменную окружения `PRACTICE2_DIR`.
|
||||||
|
Если переменная не задана — используется `/usr/local/practice2`.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x setup.sh
|
||||||
|
|
||||||
|
# для пути по умолчанию (/usr/local/practice2) нужен root:
|
||||||
|
sudo ./setup.sh
|
||||||
|
|
||||||
|
# для тестирования без root:
|
||||||
|
PRACTICE2_DIR=/tmp/practice2 ./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт создаёт структуру каталогов, копирует утилиты в `bin/` и выставляет права доступа.
|
||||||
|
|
||||||
|
### Добавить bin во временный PATH
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/usr/local/practice2/bin:$PATH"
|
||||||
|
|
||||||
|
# или для тестовой директории:
|
||||||
|
export PATH="/tmp/practice2/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого утилиты доступны без полного пути:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
usermgr add alice
|
||||||
|
confaccess
|
||||||
|
bruteforce alice
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### usermgr — управление пользователями
|
||||||
|
|
||||||
|
```bash
|
||||||
|
usermgr add alice # добавить пользователя (интерактивный ввод)
|
||||||
|
usermgr list # список пользователей
|
||||||
|
usermgr edit alice --permissions rw
|
||||||
|
usermgr edit alice --full-name "Иванов Иван"
|
||||||
|
usermgr passwd alice # сменить пароль
|
||||||
|
usermgr delete alice # удалить пользователя
|
||||||
|
```
|
||||||
|
|
||||||
|
Права: `r` — чтение, `w` — запись, `d` — удаление.
|
||||||
|
|
||||||
|
Требования к паролю: первый символ — буква (A–Z, a–z), далее — буквы, цифры и `!@#$%^&*()`.
|
||||||
|
|
||||||
|
### confaccess — доступ к данным
|
||||||
|
|
||||||
|
```bash
|
||||||
|
confaccess
|
||||||
|
# или с явным указанием базовой директории:
|
||||||
|
PRACTICE2_DIR=/tmp/practice2 confaccess
|
||||||
|
```
|
||||||
|
|
||||||
|
Режим проверки учётных данных (для bruteforce): `confaccess --check <login>` — читает пароли построчно из stdin, выводит 0 или 1 на каждую строку, exit 0 при первом совпадении.
|
||||||
|
|
||||||
|
После аутентификации доступны команды:
|
||||||
|
|
||||||
|
```
|
||||||
|
create <file> создать новый пустой файл в confdata [requires: w]
|
||||||
|
read <file> вывести содержимое файла [requires: r]
|
||||||
|
append <file> <text> дописать строку в файл [requires: w]
|
||||||
|
copy <src> <dst> скопировать файл в confdata [requires: r, w]
|
||||||
|
remove <file> удалить файл из confdata [requires: d]
|
||||||
|
help / exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Пути к файлам указываются относительно `confdata/` (или абсолютные).
|
||||||
|
Для `copy`: src — любой путь, dst — внутри confdata, перезапись запрещена.
|
||||||
|
Выход — `exit` или Ctrl+C.
|
||||||
|
|
||||||
|
### bruteforce — взлом пароля
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bruteforce alice
|
||||||
|
bruteforce alice --max-length 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Перебор выполняется через утилиту access (confaccess): bruteforce не имеет доступа к passwd-файлу и проверяет пароли только через `confaccess --check`.
|
||||||
|
Алгоритм хэширования пароля в access: **MD5** (по индивидуальному заданию).
|
||||||
|
Фиксируется время перебора и количество итераций до нахождения пароля.
|
||||||
|
Перебор останавливается автоматически при достижении лимита 8 часов.
|
||||||
278
lab2/access.py
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
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 CONFDATA_DIR, LOG_DIR, PASSWD_FILE
|
||||||
|
|
||||||
|
HELP_TEXT = """\
|
||||||
|
Commands:
|
||||||
|
create <file> create new empty file [requires: w]
|
||||||
|
read <file> print file contents [requires: r]
|
||||||
|
append <file> <text> append text line to file [requires: w]
|
||||||
|
copy <src> <dst> copy file into confdata [requires: r, w]
|
||||||
|
remove <file> delete file from confdata [requires: d]
|
||||||
|
help show this help
|
||||||
|
exit exit"""
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return hashlib.md5(password.encode("ascii"), usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def log_action(login: str, action: str) -> None:
|
||||||
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
timestamp = datetime.now().isoformat(sep=" ", timespec="seconds")
|
||||||
|
with open(LOG_DIR / "access.log", "a") as f:
|
||||||
|
f.write(f"{timestamp} [{login}] {action}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def read_users() -> dict[str, dict]:
|
||||||
|
users: dict[str, dict] = {}
|
||||||
|
if not PASSWD_FILE.exists():
|
||||||
|
return users
|
||||||
|
with open(PASSWD_FILE) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(":", 4)
|
||||||
|
if len(parts) != 5:
|
||||||
|
continue
|
||||||
|
users[parts[0]] = {
|
||||||
|
"password_hash": parts[1],
|
||||||
|
"id": parts[2],
|
||||||
|
"permissions": parts[3],
|
||||||
|
"full_name": parts[4],
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate() -> tuple[str, dict]:
|
||||||
|
users = read_users()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
login = input("Login: ").strip()
|
||||||
|
password = getpass.getpass("Password: ")
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nBye.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
user = users.get(login)
|
||||||
|
if user and user["password_hash"] == hash_password(password):
|
||||||
|
return login, user
|
||||||
|
log_action(login if login else "-", "LOGIN_FAILED")
|
||||||
|
print("Invalid credentials. Try again.")
|
||||||
|
|
||||||
|
|
||||||
|
def confdata_path(arg: str) -> Path:
|
||||||
|
p = Path(arg)
|
||||||
|
if not p.is_absolute():
|
||||||
|
p = CONFDATA_DIR / p
|
||||||
|
return p.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def is_in_confdata(path: Path) -> bool:
|
||||||
|
try:
|
||||||
|
path.relative_to(CONFDATA_DIR.resolve())
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_read(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "r" not in perms:
|
||||||
|
print("Permission denied (requires: r)")
|
||||||
|
return
|
||||||
|
if len(args) != 1:
|
||||||
|
print("Usage: read <file>")
|
||||||
|
return
|
||||||
|
path = confdata_path(args[0])
|
||||||
|
if not is_in_confdata(path):
|
||||||
|
print("Access denied: file must be inside confdata")
|
||||||
|
return
|
||||||
|
if not path.exists():
|
||||||
|
print(f"File not found: {path.name}")
|
||||||
|
return
|
||||||
|
print(path.read_text(), end="")
|
||||||
|
log_action(login, f"READ {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_create(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "w" not in perms:
|
||||||
|
print("Permission denied (requires: w)")
|
||||||
|
return
|
||||||
|
if len(args) != 1:
|
||||||
|
print("Usage: create <file>")
|
||||||
|
return
|
||||||
|
path = confdata_path(args[0])
|
||||||
|
if not is_in_confdata(path):
|
||||||
|
print("Access denied: file must be inside confdata")
|
||||||
|
return
|
||||||
|
if path.exists():
|
||||||
|
print(f"File already exists: {path.name}")
|
||||||
|
return
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.touch()
|
||||||
|
log_action(login, f"CREATE {path}")
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_append(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "w" not in perms:
|
||||||
|
print("Permission denied (requires: w)")
|
||||||
|
return
|
||||||
|
if len(args) < 2:
|
||||||
|
print("Usage: append <file> <text>")
|
||||||
|
return
|
||||||
|
path = confdata_path(args[0])
|
||||||
|
if not is_in_confdata(path):
|
||||||
|
print("Access denied: file must be inside confdata")
|
||||||
|
return
|
||||||
|
if not path.exists():
|
||||||
|
print(f"File not found: {path.name}. Use 'create' first.")
|
||||||
|
return
|
||||||
|
text = " ".join(args[1:])
|
||||||
|
with open(path, "a") as f:
|
||||||
|
f.write(text + "\n")
|
||||||
|
log_action(login, f"APPEND {path}")
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_copy(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "r" not in perms or "w" not in perms:
|
||||||
|
print("Permission denied (requires: r, w)")
|
||||||
|
return
|
||||||
|
if len(args) != 2:
|
||||||
|
print("Usage: copy <src> <dst>")
|
||||||
|
return
|
||||||
|
|
||||||
|
src = Path(args[0]).resolve()
|
||||||
|
dst = confdata_path(args[1])
|
||||||
|
|
||||||
|
if not is_in_confdata(dst):
|
||||||
|
print("Access denied: destination must be inside confdata")
|
||||||
|
return
|
||||||
|
if dst.is_dir():
|
||||||
|
dst = dst / src.name
|
||||||
|
if dst.exists():
|
||||||
|
print(f"Destination already exists: {dst.name}")
|
||||||
|
return
|
||||||
|
if not src.exists():
|
||||||
|
print(f"Source not found: {args[0]}")
|
||||||
|
return
|
||||||
|
if src.is_dir():
|
||||||
|
print("Copying directories is not supported")
|
||||||
|
return
|
||||||
|
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
log_action(login, f"COPY {src} -> {dst}")
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_remove(args: list[str], login: str, perms: str) -> None:
|
||||||
|
if "d" not in perms:
|
||||||
|
print("Permission denied (requires: d)")
|
||||||
|
return
|
||||||
|
if len(args) != 1:
|
||||||
|
print("Usage: remove <file>")
|
||||||
|
return
|
||||||
|
path = confdata_path(args[0])
|
||||||
|
if not is_in_confdata(path):
|
||||||
|
print("Access denied: file must be inside confdata")
|
||||||
|
return
|
||||||
|
if not path.exists():
|
||||||
|
print(f"File not found: {path.name}")
|
||||||
|
return
|
||||||
|
path.unlink()
|
||||||
|
log_action(login, f"REMOVE {path}")
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
def 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()
|
||||||
|
perms = user["permissions"]
|
||||||
|
full_name = user["full_name"]
|
||||||
|
|
||||||
|
log_action(login, "LOGIN")
|
||||||
|
print(f"\nПривет, {full_name}")
|
||||||
|
print(HELP_TEXT)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = input(f"\n{login}> ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
log_action(login, "EXIT")
|
||||||
|
print("\nBye.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = line.split()
|
||||||
|
command, args = parts[0], parts[1:]
|
||||||
|
|
||||||
|
if command == "exit":
|
||||||
|
log_action(login, "EXIT")
|
||||||
|
print("Bye.")
|
||||||
|
break
|
||||||
|
elif command == "help":
|
||||||
|
print(HELP_TEXT)
|
||||||
|
elif command == "create":
|
||||||
|
cmd_create(args, login, perms)
|
||||||
|
elif command == "read":
|
||||||
|
cmd_read(args, login, perms)
|
||||||
|
elif command == "append":
|
||||||
|
cmd_append(args, login, perms)
|
||||||
|
elif command == "copy":
|
||||||
|
cmd_copy(args, login, perms)
|
||||||
|
elif command == "remove":
|
||||||
|
cmd_remove(args, login, perms)
|
||||||
|
else:
|
||||||
|
print(f"Unknown command: {command!r}. Type 'help' for available commands.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
141
lab2/bruteforce.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/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 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:
|
||||||
|
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 = 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:
|
||||||
|
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 access 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()
|
||||||
11
lab2/config.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = Path(os.environ.get("PRACTICE2_DIR", "/usr/local/practice2"))
|
||||||
|
|
||||||
|
ETC_DIR = BASE_DIR / "etc"
|
||||||
|
CONFDATA_DIR = BASE_DIR / "confdata"
|
||||||
|
BIN_DIR = BASE_DIR / "bin"
|
||||||
|
LOG_DIR = BASE_DIR / "log"
|
||||||
|
|
||||||
|
PASSWD_FILE = ETC_DIR / "passwd"
|
||||||
284
lab2/lab2.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# Практическая работа №2
|
||||||
|
|
||||||
|
по дисциплине «Защита информации»
|
||||||
|
|
||||||
|
**Тема работы:** «Разработка и исследование системы аутентификации и авторизации»
|
||||||
|
**Преподаватель:** Силиненко А.В.
|
||||||
|
**Email:** [a_silinenko@mail.ru](mailto:a_silinenko@mail.ru)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Цели работы
|
||||||
|
|
||||||
|
* Разработать систему доступа пользователей к конфиденциальным данным;
|
||||||
|
* Исследовать стойкость паролей к атаке методом грубой силы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Задачи работы
|
||||||
|
|
||||||
|
### 2.1.
|
||||||
|
|
||||||
|
При необходимости установить на компьютер целевую ОС (Linux или MacOS), в которой производится разработка и использование системы.
|
||||||
|
|
||||||
|
### 2.2.
|
||||||
|
|
||||||
|
Разработать систему доступа пользователей к конфиденциальным данным, включающую:
|
||||||
|
|
||||||
|
* Выделенный каталог для хранения всех файлов системы;
|
||||||
|
* Утилиту для работы с данными аутентификации и авторизации (паролями и правами доступа);
|
||||||
|
* Утилиту доступа к конфиденциальным данным, обеспечивающую аутентификацию и авторизацию пользователя.
|
||||||
|
|
||||||
|
### 2.3.
|
||||||
|
|
||||||
|
Разработать программу взлома паролей методом грубой силы и исследовать стойкость паролей в зависимости от длины пароля и алгоритма хэширования.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Требования к работе
|
||||||
|
|
||||||
|
### 3.1. ОС
|
||||||
|
|
||||||
|
Работа выполняется в ОС **Linux** или **MacOS**.
|
||||||
|
|
||||||
|
Необходим доступ с правами суперпользователя.
|
||||||
|
Если доступа нет — установить гипервизор **VirtualBox** ([https://www.virtualbox.org/](https://www.virtualbox.org/)) и развернуть гостевую ОС.
|
||||||
|
|
||||||
|
Допускается установка ОС как второй системы без VirtualBox.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2. Структура каталогов
|
||||||
|
|
||||||
|
Необходимо создать дерево каталогов:
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/practice2/
|
||||||
|
├── etc # хранение данных аутентификации и авторизации
|
||||||
|
├── confdata # хранение конфиденциальных файлов
|
||||||
|
├── bin # разработанные утилиты
|
||||||
|
└── log # файлы регистрации
|
||||||
|
```
|
||||||
|
|
||||||
|
**Права доступа:** чтение, запись и выполнение только для пользователя `root`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3. Требования к утилите управления пользователями
|
||||||
|
|
||||||
|
#### Файл хранения данных
|
||||||
|
|
||||||
|
Файл:
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/practice2/etc/passwd
|
||||||
|
```
|
||||||
|
|
||||||
|
Структура записи:
|
||||||
|
|
||||||
|
```
|
||||||
|
<логин>:<хэш_пароля>:<идентификатор>:<права>:<ФИО>
|
||||||
|
```
|
||||||
|
|
||||||
|
Одна строка — один пользователь.
|
||||||
|
|
||||||
|
**Права на файл:** чтение и запись только для `root`.
|
||||||
|
|
||||||
|
#### Права доступа
|
||||||
|
|
||||||
|
* `r` — чтение
|
||||||
|
* `w` — запись
|
||||||
|
* `d` — удаление
|
||||||
|
|
||||||
|
#### Требования к функционалу
|
||||||
|
|
||||||
|
Утилита должна обеспечивать:
|
||||||
|
|
||||||
|
* Добавление нового пользователя:
|
||||||
|
|
||||||
|
* логин
|
||||||
|
* ФИО
|
||||||
|
* права доступа
|
||||||
|
* пароль + подтверждение
|
||||||
|
* Проверку корректности данных
|
||||||
|
* Хранение пароля в виде **хэш-значения**
|
||||||
|
* Использование алгоритма хэширования согласно индивидуальному заданию
|
||||||
|
* Редактирование существующего пользователя
|
||||||
|
* Изменение пароля
|
||||||
|
* Удаление пользователя
|
||||||
|
* Регистрацию всех действий с файлом `passwd`
|
||||||
|
|
||||||
|
#### Требования к паролю
|
||||||
|
|
||||||
|
* Кодировка: ASCII
|
||||||
|
* Разрешены:
|
||||||
|
|
||||||
|
* A–Z
|
||||||
|
* a–z
|
||||||
|
* 0–9
|
||||||
|
* `!@#$%^&*()`
|
||||||
|
* Первый символ не может быть цифрой или спецсимволом
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4. Требования к утилите доступа к конфиденциальным данным
|
||||||
|
|
||||||
|
#### Авторизация
|
||||||
|
|
||||||
|
При запуске:
|
||||||
|
|
||||||
|
1. Запрос логина
|
||||||
|
2. Запрос пароля
|
||||||
|
3. Проверка корректности
|
||||||
|
|
||||||
|
Если данные корректны:
|
||||||
|
|
||||||
|
* Вывод: `Привет, <ФИО>`
|
||||||
|
* Краткая справка
|
||||||
|
* Приглашение к вводу команд
|
||||||
|
|
||||||
|
Если данные некорректны:
|
||||||
|
|
||||||
|
* Повторный запрос
|
||||||
|
|
||||||
|
Остановка — по сигналу **SIGINT (Ctrl+C)**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Поддерживаемые команды
|
||||||
|
|
||||||
|
| Команда | Описание | Требуемые права |
|
||||||
|
| -------- | ------------------------ | --------------- |
|
||||||
|
| `read` | Вывод содержимого файла | r |
|
||||||
|
| `append` | Добавление данных в файл | w |
|
||||||
|
| `copy` | Копирование файла | r + w |
|
||||||
|
| `remove` | Удаление файла | d |
|
||||||
|
| `exit` | Выход | — |
|
||||||
|
| `help` | Справка | — |
|
||||||
|
|
||||||
|
#### Ограничения copy
|
||||||
|
|
||||||
|
* Разрешено копирование:
|
||||||
|
|
||||||
|
* в каталог `confdata`
|
||||||
|
* внутри `confdata`
|
||||||
|
* Запрещено:
|
||||||
|
|
||||||
|
* из `confdata` в другие каталоги
|
||||||
|
* перезапись существующих файлов внутри `confdata`
|
||||||
|
|
||||||
|
#### Дополнительные требования
|
||||||
|
|
||||||
|
* Утилита доступна для запуска всем пользователям ОС
|
||||||
|
* Все действия с конфиденциальными данными логируются
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5. Программа взлома паролей
|
||||||
|
|
||||||
|
Должна:
|
||||||
|
|
||||||
|
* Запускать утилиту доступа
|
||||||
|
* Перебирать пароли методом brute force
|
||||||
|
* Учитывать используемый алгоритм хэширования
|
||||||
|
* Фиксировать:
|
||||||
|
|
||||||
|
* время перебора
|
||||||
|
* количество итераций до взлома
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6. Общие требования
|
||||||
|
|
||||||
|
* Язык программирования — любой
|
||||||
|
* Код должен быть снабжен комментариями
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7. Требования к исследованию
|
||||||
|
|
||||||
|
Провести исследование для длин паролей:
|
||||||
|
|
||||||
|
```
|
||||||
|
3, 4, 5, 6, 7, 8 символов
|
||||||
|
```
|
||||||
|
|
||||||
|
Необходимо:
|
||||||
|
|
||||||
|
* Рассчитать максимальное количество итераций
|
||||||
|
* По экспериментам (3–4 символа) оценить время для 5–8
|
||||||
|
* Проверить теорию экспериментально
|
||||||
|
* Прервать эксперимент, если длительность > 8 часов
|
||||||
|
* Проводить тестирование без активных задач на ПК
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Требования к отчету
|
||||||
|
|
||||||
|
В отчете необходимо указать:
|
||||||
|
|
||||||
|
* Актуальность темы
|
||||||
|
|
||||||
|
* Цель и задачи
|
||||||
|
|
||||||
|
* Требования к системе
|
||||||
|
|
||||||
|
* Характеристики ПК (процессор, память)
|
||||||
|
|
||||||
|
* ОС и среду разработки
|
||||||
|
|
||||||
|
* Используемый язык
|
||||||
|
|
||||||
|
* Алгоритм хэширования
|
||||||
|
|
||||||
|
* Примеры сборки (если применимо)
|
||||||
|
|
||||||
|
* Примеры работы утилит
|
||||||
|
|
||||||
|
* Пример расчета количества итераций и времени взлома
|
||||||
|
|
||||||
|
* Таблицу или график с результатами:
|
||||||
|
|
||||||
|
* рассчитанное количество итераций и время
|
||||||
|
* полученные экспериментальные данные
|
||||||
|
|
||||||
|
* Выводы
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Справочная информация
|
||||||
|
|
||||||
|
### Поддержка алгоритмов хэширования
|
||||||
|
|
||||||
|
**Linux:**
|
||||||
|
|
||||||
|
* `md5sum`
|
||||||
|
* `sha1sum`
|
||||||
|
* `sha256sum`
|
||||||
|
* `sha512sum`
|
||||||
|
* `b2sum`
|
||||||
|
* библиотека `openssl`
|
||||||
|
|
||||||
|
**C++:**
|
||||||
|
|
||||||
|
* `std::hash`
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
|
||||||
|
* модуль `hashlib`
|
||||||
|
|
||||||
|
**Java:**
|
||||||
|
|
||||||
|
* `java.security.MessageDigest`
|
||||||
|
* Spring Security
|
||||||
|
|
||||||
|
**Go:**
|
||||||
|
|
||||||
|
* пакет `hash`
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Примечание:
|
||||||
|
|
||||||
|
Brute Force должен запускать утилиту из-под себя и через неё пытаться подобрать пароль, он не должен лезть в файл напрямую.
|
||||||
38
lab2/setup.sh
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BASE_DIR="${PRACTICE2_DIR:-/usr/local/practice2}"
|
||||||
|
|
||||||
|
echo "Setting up directory structure at: $BASE_DIR"
|
||||||
|
|
||||||
|
mkdir -p "$BASE_DIR/etc" "$BASE_DIR/confdata" "$BASE_DIR/bin" "$BASE_DIR/log"
|
||||||
|
|
||||||
|
touch "$BASE_DIR/etc/passwd"
|
||||||
|
|
||||||
|
cp "$SCRIPT_DIR/config.py" "$BASE_DIR/bin/config.py"
|
||||||
|
cp "$SCRIPT_DIR/usermgr.py" "$BASE_DIR/bin/usermgr"
|
||||||
|
cp "$SCRIPT_DIR/access.py" "$BASE_DIR/bin/confaccess"
|
||||||
|
cp "$SCRIPT_DIR/bruteforce.py" "$BASE_DIR/bin/bruteforce"
|
||||||
|
|
||||||
|
chmod +x "$BASE_DIR/bin/usermgr" "$BASE_DIR/bin/confaccess" "$BASE_DIR/bin/bruteforce"
|
||||||
|
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
chmod 700 "$BASE_DIR" "$BASE_DIR/etc" "$BASE_DIR/confdata" "$BASE_DIR/bin" "$BASE_DIR/log"
|
||||||
|
chmod 600 "$BASE_DIR/etc/passwd"
|
||||||
|
echo "Permissions set (root-only)."
|
||||||
|
else
|
||||||
|
echo "Warning: not running as root; skipping permission hardening."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. Directory layout:"
|
||||||
|
ls -la "$BASE_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " $BASE_DIR/bin/usermgr add <login>"
|
||||||
|
echo " $BASE_DIR/bin/confaccess"
|
||||||
|
echo " $BASE_DIR/bin/bruteforce <login>"
|
||||||
|
echo ""
|
||||||
|
echo "To add bin to PATH temporarily:"
|
||||||
|
echo " export PATH=\"$BASE_DIR/bin:\$PATH\""
|
||||||
246
lab2/usermgr.py
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import hashlib
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from config import LOG_DIR, PASSWD_FILE
|
||||||
|
|
||||||
|
ALLOWED_CHARS = set(
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"
|
||||||
|
)
|
||||||
|
FIRST_CHAR_ALLOWED = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||||
|
VALID_PERM_CHARS = set("rwd")
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return hashlib.md5(password.encode("ascii"), usedforsecurity=False).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password(password: str) -> str | None:
|
||||||
|
if not password:
|
||||||
|
return "password cannot be empty"
|
||||||
|
if password[0] not in FIRST_CHAR_ALLOWED:
|
||||||
|
return "first character must be a letter (A-Z, a-z)"
|
||||||
|
invalid = [ch for ch in password if ch not in ALLOWED_CHARS]
|
||||||
|
if invalid:
|
||||||
|
return f"invalid characters: {''.join(set(invalid))!r}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_permissions(perms: str) -> str | None:
|
||||||
|
if not perms:
|
||||||
|
return "permissions cannot be empty"
|
||||||
|
for ch in perms:
|
||||||
|
if ch not in VALID_PERM_CHARS:
|
||||||
|
return f"invalid permission {ch!r}; allowed: r, w, d"
|
||||||
|
if len(set(perms)) != len(perms):
|
||||||
|
return "duplicate permissions"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def log_action(action: str) -> None:
|
||||||
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
timestamp = datetime.now().isoformat(sep=" ", timespec="seconds")
|
||||||
|
with open(LOG_DIR / "usermgr.log", "a") as f:
|
||||||
|
f.write(f"{timestamp} {action}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def read_users() -> list[dict]:
|
||||||
|
if not PASSWD_FILE.exists():
|
||||||
|
return []
|
||||||
|
users = []
|
||||||
|
with open(PASSWD_FILE) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split(":", 4)
|
||||||
|
if len(parts) != 5:
|
||||||
|
continue
|
||||||
|
users.append(
|
||||||
|
{
|
||||||
|
"login": parts[0],
|
||||||
|
"password_hash": parts[1],
|
||||||
|
"id": parts[2],
|
||||||
|
"permissions": parts[3],
|
||||||
|
"full_name": parts[4],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
def write_users(users: list[dict]) -> None:
|
||||||
|
PASSWD_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(PASSWD_FILE, "w") as f:
|
||||||
|
for u in users:
|
||||||
|
f.write(
|
||||||
|
f"{u['login']}:{u['password_hash']}:{u['id']}:{u['permissions']}:{u['full_name']}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_user(users: list[dict], login: str) -> dict | None:
|
||||||
|
return next((u for u in users if u["login"] == login), None)
|
||||||
|
|
||||||
|
|
||||||
|
def next_uid(users: list[dict]) -> str:
|
||||||
|
if not users:
|
||||||
|
return "1"
|
||||||
|
return str(max(int(u["id"]) for u in users) + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_password() -> str:
|
||||||
|
while True:
|
||||||
|
password = getpass.getpass("Password: ")
|
||||||
|
err = validate_password(password)
|
||||||
|
if err:
|
||||||
|
print(f"Invalid password: {err}")
|
||||||
|
continue
|
||||||
|
confirm = getpass.getpass("Confirm password: ")
|
||||||
|
if password != confirm:
|
||||||
|
print("Passwords do not match.")
|
||||||
|
continue
|
||||||
|
return password
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_add(args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
login = args.login
|
||||||
|
|
||||||
|
if find_user(users, login):
|
||||||
|
print(f"User '{login}' already exists.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
full_name = input("Full name: ").strip()
|
||||||
|
if not full_name:
|
||||||
|
print("Full name cannot be empty.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
perms = input("Permissions (any combination of r, w, d): ").strip()
|
||||||
|
err = validate_permissions(perms)
|
||||||
|
if err:
|
||||||
|
print(f"Invalid permissions: {err}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
password = prompt_password()
|
||||||
|
|
||||||
|
uid = next_uid(users)
|
||||||
|
users.append(
|
||||||
|
{
|
||||||
|
"login": login,
|
||||||
|
"password_hash": hash_password(password),
|
||||||
|
"id": uid,
|
||||||
|
"permissions": perms,
|
||||||
|
"full_name": full_name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
write_users(users)
|
||||||
|
log_action(f"ADD login={login} id={uid} permissions={perms} full_name='{full_name}'")
|
||||||
|
print(f"User '{login}' added (id={uid}).")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_edit(args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
user = find_user(users, args.login)
|
||||||
|
if not user:
|
||||||
|
print(f"User '{args.login}' not found.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
changed = []
|
||||||
|
|
||||||
|
if args.full_name is not None:
|
||||||
|
if not args.full_name:
|
||||||
|
print("Full name cannot be empty.")
|
||||||
|
sys.exit(1)
|
||||||
|
user["full_name"] = args.full_name
|
||||||
|
changed.append("full_name")
|
||||||
|
|
||||||
|
if args.permissions is not None:
|
||||||
|
err = validate_permissions(args.permissions)
|
||||||
|
if err:
|
||||||
|
print(f"Invalid permissions: {err}")
|
||||||
|
sys.exit(1)
|
||||||
|
user["permissions"] = args.permissions
|
||||||
|
changed.append("permissions")
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
print("Nothing to change. Use --full-name and/or --permissions.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
write_users(users)
|
||||||
|
log_action(f"EDIT login={args.login} changed={','.join(changed)}")
|
||||||
|
print(f"User '{args.login}' updated: {', '.join(changed)}.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_passwd(args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
user = find_user(users, args.login)
|
||||||
|
if not user:
|
||||||
|
print(f"User '{args.login}' not found.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
password = prompt_password()
|
||||||
|
user["password_hash"] = hash_password(password)
|
||||||
|
write_users(users)
|
||||||
|
log_action(f"PASSWD login={args.login}")
|
||||||
|
print(f"Password for '{args.login}' updated.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_delete(args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
if not find_user(users, args.login):
|
||||||
|
print(f"User '{args.login}' not found.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
users = [u for u in users if u["login"] != args.login]
|
||||||
|
write_users(users)
|
||||||
|
log_action(f"DELETE login={args.login}")
|
||||||
|
print(f"User '{args.login}' deleted.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list(_args: argparse.Namespace) -> None:
|
||||||
|
users = read_users()
|
||||||
|
if not users:
|
||||||
|
print("No users.")
|
||||||
|
return
|
||||||
|
print(f"{'Login':<16} {'ID':<4} {'Perms':<6} Full Name")
|
||||||
|
print("-" * 52)
|
||||||
|
for u in users:
|
||||||
|
print(f"{u['login']:<16} {u['id']:<4} {u['permissions']:<6} {u['full_name']}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="User management utility for practice2")
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
p_add = sub.add_parser("add", help="Add a new user")
|
||||||
|
p_add.add_argument("login", help="Username (login)")
|
||||||
|
p_add.set_defaults(func=cmd_add)
|
||||||
|
|
||||||
|
p_edit = sub.add_parser("edit", help="Edit an existing user")
|
||||||
|
p_edit.add_argument("login", help="Username to edit")
|
||||||
|
p_edit.add_argument("--full-name", dest="full_name", help="New full name")
|
||||||
|
p_edit.add_argument("--permissions", help="New permissions (e.g. rwd)")
|
||||||
|
p_edit.set_defaults(func=cmd_edit)
|
||||||
|
|
||||||
|
p_passwd = sub.add_parser("passwd", help="Change user password")
|
||||||
|
p_passwd.add_argument("login", help="Username")
|
||||||
|
p_passwd.set_defaults(func=cmd_passwd)
|
||||||
|
|
||||||
|
p_del = sub.add_parser("delete", help="Delete a user")
|
||||||
|
p_del.add_argument("login", help="Username to delete")
|
||||||
|
p_del.set_defaults(func=cmd_delete)
|
||||||
|
|
||||||
|
p_list = sub.add_parser("list", help="List all users")
|
||||||
|
p_list.set_defaults(func=cmd_list)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
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
|
||||||
1
lab5/.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.14
|
||||||
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" },
|
||||||
|
]
|
||||||
0
lab1/.gitignore → report/.gitignore
vendored
BIN
report/img/lab2-access-log.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
report/img/lab2-bruteforce.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
report/img/lab2-confaccess-auth.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
report/img/lab2-confaccess-commands.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
report/img/lab2-passwd.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
report/img/lab2-setup.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
report/img/lab2-usermgr-add.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
report/img/lab2-usermgr-list.png
Normal file
|
After Width: | Height: | Size: 13 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 |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |