• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья Снимаем защиту на изменение данных в документах MS Office, LibreOffice с помощью Python

Форматы офисных документов, такие как: «.docx», «.xlsx», «.odt», «.ods» содержат в себе механизмы, которые могут защитить текст или определенные ячейки в таблице от изменений. Более того, такая защита предусматривает механизм установления пароля для ее снятия. Таким образом, посторонний человек не сможет снять защиту и изменить документ по своему усмотрению.

Обратите внимание, что в данном случае речь не идет о шифровании документа. Такую защиту достаточно легко убрать. Вспомним, что настройки документа хранятся в архиве «zip», которым и являются современные офисные форматы.

f99d6c679662465e84225dd1844e36f9_fuse_67069409_0.jpg

Все действия, которые описаны ниже, достаточно легко проделать руками. Однако давайте автоматизируем данный процесс, а заодно потренируемся распаковывать и упаковывать простые архивы. Для процесса автоматизации будем использовать язык программирования python.


Установка пароля на редактирование в документе «.docx»

Для того чтобы потренироваться, защитим документ «.docx» паролем от изменения. Думаю, что все знают, как это делается. Может быть, в различных редакциях будут слегка отличаться меню, но общая суть остается одинаковой. Жмем кнопку «Файл -> Защитить документ -> Ограничить редактирование».

screenshot1.png

Выбираем необходимые ограничения, для кого они будут действовать, и жмем на кнопку «Да, включить защиту».

screenshot2.png

Открывается окно, в котором предлагается ввести пароль (или не ввести, тут уже на выбор), и после нажатия на кнопку «ОК» защита будет установлена.

screenshot3.png

Теперь при попытке редактирования текста и нажатии на кнопку отключения защиты будет появляться поле для ввода пароля.

screenshot4.png

Но, мы же помним, что «.docx» это архив. Поэтому меняем расширение на «zip» и распаковываем в директорию с названием файла. После распаковки переходим в папку «word» и ищем файл «settings.xml», именно в нем содержаться настройки, с которыми мы будем работать.

screenshot5.png

Открываем данный файл «.xml» и ищем строку, которая начинается с тэга <w:documentProtection…. Если мы удалим данный тэг в любом текстовом редакторе и снова упакуем документ, защиты на нем уже не будет.

screenshot6.png

Данные пояснения были даны для того, чтобы понимать механизм и принцип, по которому будет сниматься защита на редактирование в документах. Практически во всех документах всё более-менее одинаково, различаются только названия тэгов и их расположение. Поэтому, в дальнейшем я не буду подробно описывать установку и снятие защиты, а более сосредоточусь на коде. В случае необходимости я буду пояснять, какие тэги необходимо удалить и где они располагаются.


Снимаем защиту с документа «.docx»

Импорт библиотек


Для работы будущего скрипта сторонних библиотек не потребуется. Достаточно будет тех, что идут с python «из коробки». Импортируем необходимые библиотеки:

Python:
import re
import shutil
from pathlib import Path


Смена расширения у документа

Для того чтобы распаковать документ, необходимо изменить его расширение на «.zip». А так как данная операция будет выполняться в процессе снятия защиты два раза: первый раз при распаковке, второй раз после упаковки, необходимо создать функцию, которая в качестве аргументов будет принимать путь к файлу для смены расширения и само расширение, на которое нужно изменить текущее.

Создадим функцию def rename_document(path: str, new_suffix) -> Path. В данном случае path – строковый параметр, в котором передается путь к документу, а new_suffix – новое расширение документа. Чтобы иметь возможность работать в дальнейшем с переименованным документом, возвращать из функции будем путь к документу с измененным расширением.

Python:
def rename_document(path: str, new_suffix) -> Path:
    """Изменение расширения файла."""
    doc_name = Path(path).parent / f'{Path(path).name.removesuffix(Path(path).suffix)}{new_suffix}'
    Path(path).rename(doc_name)
    return doc_name

Для начала формируем новое имя документа с нужным нам расширением и помещаем его в переменную doc_name. После переименовываем с помощью передачи необходимых параметров в класс Path библиотеки pathlib. Возвращаем из функции путь к документу с измененным расширением.


Распаковка архива

Следующее действие, которое необходимо выполнить – распаковать полученный в результате смены расширения архив. Для этого создадим функцию def extract_doc(path: Path) -> Path, которая будет принимать в качестве аргумента путь к файлу для распаковки, а возвращать путь к директории, в которую был распакован файл. Во избежание перегрузки скрипта библиотеками, библиотеку zipfile использовать не будем, а воспользуемся библиотекой shutil, которая предоставляет функцию unpack_archive. В данном случае ее будет вполне достаточно.

Python:
def extract_doc(path: Path) -> Path:
    """Распаковка архива."""
    path_unpack = path.parent / Path(path).name.removesuffix(Path(path).suffix)
    shutil.unpack_archive(path, path_unpack, "zip")
    return path_unpack

Формируем путь для распаковки архива и передаем его в переменную path_unpack, которая будет возвращена из функции. Затем с помощью shutil.unpack_archive, куда передаем: путь к архиву, путь к директории для распаковки, тип архива, - распаковываем архив. Все параметры, которые передаются в unpack_archive, должны быть строковыми. Возвращаем из функции путь к директории, куда был распакован архив.


Упаковка файлов в архив

Хотя, если придерживаться порядка повествования, следующим действием необходимо снимать защиту от изменения, однако, немного его нарушим и напишем функцию, которая будет упаковывать файлы в директории. Создадим функцию def zip_document(doc: str, path: Path) -> None, которая принимает в качестве аргументов имя архива для упаковки, - doc и путь к директории, содержимое которой необходимо упаковать, - path.

Дальше, с помощью shutil и его функции make_archive упаковываем содержимое директории. В качестве аргументов make_archive принимает: имя архива (возможно указание пути, куда поместить архив), тип архива, путь к директории для упаковки. Функция zip_document ничего не возвращает.

Python:
def zip_document(doc: str, path: Path) -> None:
    """Упаковка директории в архив."""
    shutil.make_archive(str(path.parent / doc), 'zip', path)


Получение пути к документу, передача параметров в функции

И снова мы пока не будем создавать функцию для снятия защиты, а для начала создадим функцию main(), которая является точкой входа в скрипт. Чтобы было немного понятнее, что откуда приходит и что где делается, подпишу все комментариями.

Для начала запрашиваем у пользователя путь к документу и передаем в переменную path_doc_file. Выполняем проверку, существует ли данный файл и является ли он файлом с расширением «.docx». Если не выполняется хоть одно из условий, выходим из скрипта. Если все в порядке – двигаемся дальше. Сохраняем в переменной origin_suffix первоначальное расширение файла. Переименовываем файл, точнее меняем расширение и получаем в переменную rename_doc путь к нему. Распаковываем файл и получаем в переменную path_doc путь к директории, куда был распакован архив. Затем удаляем оригинальный файл. Здесь можно немного изменить, при необходимости скрипт и просто переименовать оригинальный файл или переместить в другое место. Это уже как вы захотите.

Снимаем защиту с помощью функции delete_protection, которую мы еще не написали. Упаковываем содержимое директории. Снова меняем расширение файла с «.zip» на «.docx» и удаляем директорию, куда был распакован архив.

Python:
def main():
    path_doc_file = input("Введите путь к документу: ")
    if not Path(path_doc_file).exists():
        print("Указанного файла не существует")
        exit()
    elif Path(path_doc_file).suffix not in [".docx"]:
        print("Неверный тип файла")
        exit()
    origin_suffix = Path(path_doc_file).suffix  # оригинальное расширение файла
    rename_doc = rename_document(path_doc_file, ".zip")  # имя файла после смены расширения
    path_doc = extract_doc(rename_doc)  # директория куда распакован файл
    Path(rename_doc).unlink()  # удаление оригинального файла
    delete_protection(path_doc / "word" / "settings.xml")  # удаление защиты
    zip_document(f'{Path(rename_doc).name.removesuffix(Path(rename_doc).suffix)}',
                 Path(path_doc))  # упаковка файла в архив
    rename_document(str(rename_doc), origin_suffix)  # переименование архива в оригинальное имя
    shutil.rmtree(path_doc)  # удаление директории с распакованным документом


Снятие защиты с документа

Пришло время написать функцию, которая будет снимать защиту с документа. На самом деле, она довольно проста. Ее логика заключается в поиске и удалении того тэга, про который было сказано в начале. Конечно, документ для обработки является XML-документов, и можно было бы воспользоваться библиотекой xml. Но, в данном случае глобальной задачи не стоит. Поэтому просто обработаем документ как текст, найдем в нем тэг с помощью библиотеки re и удалим его. После чего, сохраним изменения.

Создадим функцию delete_protection(file: Path) -> None, которая в качестве аргумента принимает путь к файлу «.xml». Открываем данный файл и считываем его содержимое в переменную txt_xml. Выполняем с помощью re.sub поиск и замену тэга, после чего сохраняем модифицированный текст.

Python:
def delete_protection(file: Path) -> None:
    """Удаление защиты из документа. Сохранение без удаленного элемента."""
    with open(file, "r", encoding="utf-8") as set_xml:
        txt_xml = set_xml.read()
        updated_xml_string = re.sub(r'<w:documentProtection.*?/>', '', txt_xml)
    with open(file, "w", encoding="utf-8") as set_xml:
        set_xml.write(updated_xml_string)


Python:
import re
import shutil
from pathlib import Path


def rename_document(path: str, new_suffix) -> Path:
    """Замена расширения файла."""
    doc_name = Path(path).parent / f'{Path(path).name.removesuffix(Path(path).suffix)}{new_suffix}'
    Path(path).rename(doc_name)
    return doc_name


def extract_doc(path: Path) -> Path:
    """Распаковка архива."""
    path_unpack = path.parent / Path(path).name.removesuffix(Path(path).suffix)
    shutil.unpack_archive(path, path_unpack, "zip")
    return path_unpack


def delete_protection(file: Path) -> None:
    """Удаление защиты из документа. Сохранение без удаленного элемента."""
    with open(file, "r", encoding="utf-8") as set_xml:
        txt_xml = set_xml.read()
        updated_xml_string = re.sub(r'<w:documentProtection.*?/>', '', txt_xml)
    with open(file, "w", encoding="utf-8") as set_xml:
        set_xml.write(updated_xml_string)


def zip_document(doc: str, path: Path):
    """Упаковка директории в архив."""
    shutil.make_archive(str(path.parent / doc), 'zip', path)


def main():
    path_doc_file = input("Введите путь к документу: ")
    if not Path(path_doc_file).exists():
        print("Указанного файла не существует")
        exit()
    elif Path(path_doc_file).suffix not in [".docx"]:
        print("Неверный тип файла")
        exit()
    origin_suffix = Path(path_doc_file).suffix  # оригинальное расширение файла
    rename_doc = rename_document(path_doc_file, ".zip")  # имя файла после смены расширения
    path_doc = extract_doc(rename_doc)  # директория куда распакован файл
    Path(rename_doc).unlink()  # удаление оригинального файла
    delete_protection(path_doc / "word" / "settings.xml")  # удаление защиты
    zip_document(f'{Path(rename_doc).name.removesuffix(Path(rename_doc).suffix)}',
                 Path(path_doc))  # упаковка файла в архив
    rename_document(str(rename_doc), origin_suffix)  # переименование архива в оригинальное имя
    shutil.rmtree(path_doc)  # удаление директории с распакованным документом


if __name__ == "__main__":
    main()


Модифицируем скрипт. Удаляем защиту на изменение с офисных документов другого типа

Снятие защиты с документов «.odt»


А что же другие форматы документов? Например «.odt». Здесь тоже все достаточно просто. Данный тип файла также является «zip» архивом. Если его переименовать и распаковать, мы увидим содержащиеся в нем XML документы с настройками и содержимым. Нас интересует файл: content.xml, именно в нем, помимо содержимого документа, находится также и защита.

Для примера скриншот защищенной области при попытке внести изменения:

screenshot7.png

В файле content.xml нас интересует тэг:

<text:section text:style-name="Sect1" text:name="Раздел1" text:protected="true" text:protection-key="btWDPPNShuv4Zit7WUnw10K77D8=">

Параметры данного тэга приведены для примера. Но признаком защиты будет наличие параметров:

text:protected и text:protection-key

Однако удалять его полностью не нужно, необходимо только удалить параметры с ключами и данными о защите. Поэтому, давайте модифицируем предыдущий скрипт, переименуем функцию delete_protection –> delete_protection_docx, создадим функцию delete_protection_odt(file: Path) -> None, куда в качестве параметра передадим путь к файлу «.xml». А дальше все похоже на предыдущую функцию. Открываем документ, передаем текст в переменную txt_xml, находим и меняем тэг, после чего сохраняем измененный xml.

Причем, следует обратить внимание, что здесь удаляются и другие параметры, так как защита в документ «.odt» устанавливается несколько другим образом, а именно вставляется защищенный раздел. Потому, наличие параметров с упоминанием раздела не обязательно. Причем, защищенных разделов в документе может быть довольно много, поэтому, для начала найдем их все и будем убирать защиту с каждого из них, причем, оставляя сами разделы, чтобы не нарушить структуру документа.

Python:
def delete_protection_odt(file: Path) -> None:
    """Удаление защиты из документа. Сохранение без удаленного элемента."""
    with open(file, "r", encoding="utf-8") as set_xml:
        txt_xml = set_xml.read()
        protect_sections = re.findall(r'<text:section.*?>', txt_xml)
        for protect_section in protect_sections:
            replace_section = " ".join([x for x in protect_section.split()
                                        if not x.startswith(("text:protected", "text:protection-key"))]) + ">"
            txt_xml = re.sub(protect_section, replace_section, txt_xml)
    with open(file, "w", encoding="utf-8") as set_xml:
        set_xml.write(txt_xml)

Также, необходимо модифицировать функцию main(), чтобы для различного типа файлов запускать различные функции. Но, это немного позже. А пока давайте подумаем, ведь и остальные форматы, такие как «.xlsx» и «.ods» позволяют установить защиту от изменений. И они также являются zip-архивами, а значит можно их распаковать и снять защиту. Давайте это и сделаем далее.


Снимаем защиту от изменения с документа «.xlsx»

Создадим функцию delete_protection_xlsx(path: Path) -> None, которая принимает в качестве параметра путь к директории для поиска файлов XML. Почему путь к директории, а не к файлу? Тут все достаточно просто. В базовом варианте, если вы не удалите, после создания, лишние листы, из у вас 3. Да и листов в документе может быть больше, чем один. Находятся они в распакованном архиве по пути: «xl –> worksheets».

screenshot8.png

Поэтому, необходимо получить каждый файл в этой директории и обработать по отдельности. Искать здесь нужно тэг <sheetProtection... и просто удалить. Формируем путь к директории с файлами листов и передаем в переменную path_sheets. Получаем список файлов в директории в переменной files. В цикле пробегаемся по каждому файлу. Открываем его, с помощью библиотеки re выполняем поиск и замену тэга, после чего записываем измененный текст в файл XML.

Python:
def delete_protection_xlsx(path: Path) -> None:
    """Удаление защиты из документа. Сохранение без удаленного элемента."""
    path_sheets = path / "xl" / "worksheets"
    files = [x for x in Path(path_sheets).iterdir()]
    for file in files:
        with open(file, "r", encoding="utf-8") as set_xml:
            txt_xml = set_xml.read()
            updated_xml_string = re.sub(r'<sheetProtection.*?/>', '', txt_xml)
        with open(file, "w", encoding="utf-8") as set_xml:
            set_xml.write(updated_xml_string)


Снятие защиты с документов «.ods»

И еще один тип таблиц с защитой – «.ods». Здесь немного больше операций, но не намного. В отличие от «.xlsx», листы в «.ods» находятся в одном файле. Поэтому итерироваться по директории здесь не нужно. Но, так как каждый лист находится в одном файле, и у каждого из листов может быть защита, следовательно, тэгов для модификации может быть несколько. Поэтому необходимо для начала выпоить их поиск. Название файла: content.xml.

Так как здесь может быть установлено два типа защиты, от изменения содержимого и от изменения структуры, удалим все типы защиты. Создадим функцию delete_protection_ods(file: Path) -> None, которая принимает путь к файлу для обработки. Открываем файл и считываем его содержимое в переменную txt_xml. Удаляем защиту от изменения структуры.

Выполняем поиск всех тэгов содержащих таблицы: <table:table table:name и передаем список в переменную tables. Итерируемся по списку. Разобьем найденный тэг на параметры, отфильтруем те, что относятся к защите. Сформируем тэг, который необходимо найти find_tag, а также тэг, на который будем менять найденный: remove_tag. Выполняем поиск тэга и его замену. Также необходимо найти и заменить тэг <loext:table-protection…, а точнее – удалить.

После чего сохранить модифицированный текст.

Python:
def delete_protection_ods(file: Path) -> None:
    """Удаление защиты из документа. Сохранение без удаленного элемента."""
    with open(file, "r", encoding="utf-8") as set_xml:
        txt_xml = set_xml.read()
        txt_xml = re.sub(r'<office:spreadsheet.*?>', '<office:spreadsheet>', txt_xml)
        tables = re.findall(r'<table:table table:name.*?>', txt_xml)
        for table in tables:
            tag = [x for x in table.split() if not x.startswith(("table:protected", "table:protection-key"))]
            find_tag = " ".join(tag[0:2]) + ".*?>"
            remove_tag = " ".join(tag) + ">"
            txt_xml = re.sub(find_tag, remove_tag, txt_xml)
            txt_xml = re.sub(r'<loext:table-protection.*?/>', '', txt_xml)
    with open(file, "w", encoding="utf-8") as set_xml:
        set_xml.write(txt_xml)


Модификация функции main()

И, последний штрих, это модификация функции main(). Здесь необходимо добавить определение расширения и выполнение определенной функции в зависимости от него. Все остальное, кроме небольшой модификации проверки на расширение файла, остается без изменения.

Python:
def main() -> None:
    path_doc_file = input("Введите путь к документу: ")
    if not Path(path_doc_file).exists():
        print("Указанного файла не существует")
        exit()
    elif Path(path_doc_file).suffix not in [".docx", ".odt", ".xlsx", ".ods"]:
        print("Неверный тип файла")
        exit()
    origin_suffix = Path(path_doc_file).suffix  # оригинальное расширение файла
    rename_doc = rename_document(path_doc_file, ".zip")  # имя файла после смены расширения
    path_doc = extract_doc(rename_doc)  # директория куда распакован файл
    Path(rename_doc).unlink()  # удаление оригинального файла
    if origin_suffix == ".docx":
        delete_protection_docx(path_doc / "word" / "settings.xml")  # удаление защиты
    elif origin_suffix == ".odt":
        delete_protection_odt(path_doc / "content.xml")  # удаление защиты
    elif origin_suffix == ".xlsx":
        delete_protection_xlsx(path_doc)  # удаление защиты
    elif origin_suffix == ".ods":
        delete_protection_ods(path_doc / "content.xml")
    zip_document(f'{Path(rename_doc).name.removesuffix(Path(rename_doc).suffix)}',
                 Path(path_doc))  # упаковка файла в архив
    rename_document(str(rename_doc), origin_suffix)  # переименование архива в оригинальное имя
    shutil.rmtree(path_doc)  # удаление директории с распакованным документом


Python:
import re
import shutil
from pathlib import Path


def rename_document(path: str, new_suffix) -> Path:
    """Изменение расширения файла."""
    doc_name = Path(path).parent / f'{Path(path).name.removesuffix(Path(path).suffix)}{new_suffix}'
    Path(path).rename(doc_name)
    return doc_name


def extract_doc(path: Path) -> Path:
    """Распаковка архива."""
    path_unpack = path.parent / Path(path).name.removesuffix(Path(path).suffix)
    shutil.unpack_archive(path, path_unpack, "zip")
    return path_unpack


def delete_protection_docx(file: Path) -> None:
    """Удаление защиты из документа. Сохранение без удаленного элемента."""
    with open(file, "r", encoding="utf-8") as set_xml:
        txt_xml = set_xml.read()
        updated_xml_string = re.sub(r'<w:documentProtection.*?/>', '', txt_xml)
    with open(file, "w", encoding="utf-8") as set_xml:
        set_xml.write(updated_xml_string)


def delete_protection_odt(file: Path) -> None:
    """Удаление защиты из документа. Сохранение без удаленного элемента."""
    with open(file, "r", encoding="utf-8") as set_xml:
        txt_xml = set_xml.read()
        protect_sections = re.findall(r'<text:section.*?>', txt_xml)
        for protect_section in protect_sections:
            replace_section = " ".join([x for x in protect_section.split()
                                        if not x.startswith(("text:protected", "text:protection-key"))]) + ">"
            txt_xml = re.sub(protect_section, replace_section, txt_xml)
    with open(file, "w", encoding="utf-8") as set_xml:
        set_xml.write(txt_xml)


def delete_protection_xlsx(path: Path) -> None:
    """Удаление защиты из документа. Сохранение без удаленного элемента."""
    path_sheets = path / "xl" / "worksheets"
    files = [x for x in Path(path_sheets).iterdir()]
    for file in files:
        with open(file, "r", encoding="utf-8") as set_xml:
            txt_xml = set_xml.read()
            updated_xml_string = re.sub(r'<sheetProtection.*?/>', '', txt_xml)
        with open(file, "w", encoding="utf-8") as set_xml:
            set_xml.write(updated_xml_string)


def delete_protection_ods(file: Path) -> None:
    """Удаление защиты из документа. Сохранение без удаленного элемента."""
    with open(file, "r", encoding="utf-8") as set_xml:
        txt_xml = set_xml.read()
        txt_xml = re.sub(r'<office:spreadsheet.*?>', '<office:spreadsheet>', txt_xml)
        tables = re.findall(r'<table:table table:name.*?>', txt_xml)
        for table in tables:
            tag = [x for x in table.split() if not x.startswith(("table:protected", "table:protection-key"))]
            find_tag = " ".join(tag[0:2]) + ".*?>"
            remove_tag = " ".join(tag) + ">"
            txt_xml = re.sub(find_tag, remove_tag, txt_xml)
            txt_xml = re.sub(r'<loext:table-protection.*?/>', '', txt_xml)
    with open(file, "w", encoding="utf-8") as set_xml:
        set_xml.write(txt_xml)


def zip_document(doc: str, path: Path) -> None:
    """Упаковка директории в архив."""
    shutil.make_archive(str(path.parent / doc), 'zip', path)


def main() -> None:
    path_doc_file = input("Введите путь к документу: ")
    if not Path(path_doc_file).exists():
        print("Указанного файла не существует")
        exit()
    elif Path(path_doc_file).suffix not in [".docx", ".odt", ".xlsx", ".ods"]:
        print("Неверный тип файла")
        exit()
    origin_suffix = Path(path_doc_file).suffix  # оригинальное расширение файла
    rename_doc = rename_document(path_doc_file, ".zip")  # имя файла после смены расширения
    path_doc = extract_doc(rename_doc)  # директория куда распакован файл
    Path(rename_doc).unlink()  # удаление оригинального файла
    if origin_suffix == ".docx":
        delete_protection_docx(path_doc / "word" / "settings.xml")  # удаление защиты
    elif origin_suffix == ".odt":
        delete_protection_odt(path_doc / "content.xml")  # удаление защиты
    elif origin_suffix == ".xlsx":
        delete_protection_xlsx(path_doc)  # удаление защиты
    elif origin_suffix == ".ods":
        delete_protection_ods(path_doc / "content.xml")
    zip_document(f'{Path(rename_doc).name.removesuffix(Path(rename_doc).suffix)}',
                 Path(path_doc))  # упаковка файла в архив
    rename_document(str(rename_doc), origin_suffix)  # переименование архива в оригинальное имя
    shutil.rmtree(path_doc)  # удаление директории с распакованным документом


if __name__ == "__main__":
    main()

Таким образом, мы сняли защиту на изменение данных в различных типах документов: «.docx», «.xlsx», «.odt» и «.ods» с помощью скрипта на python.

К сожалению, не могу проверить данный скрипт на документах последнего офиса, однако, еще на документах 2013 данный код был рабочим.

А на этом все. Спасибо за внимание. Надеюсь, данная информация будет вам полезна
 

Вложения

  • protection_remove.zip
    1,5 КБ · Просмотры: 24
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!