Перейти к содержанию
Обсудить задачу →
Статьи и инструкции

Кто удалил файл: аудит NTFS и выгрузка событий в PowerShell

Как настроить аудит NTFS: политика File System, SACL, события 4656/4663/4660, JSONL и сопоставление удалённого файла, проверка и возврат.
Иллюстрация к инструкции: Кто удалил файл: аудит NTFS и выгрузка событий в PowerShell

Windows может записать действия с файлами, если аудит настроен до события. Для этого нужны одновременно политика File System и SACL выбранного объекта. Удаление, разрешение на удаление и использование доступа — разные события; попытка сделать вывод по одному ID часто приводит к ошибке. Здесь приведены пилотная настройка, проверка и выгрузка с осторожным сопоставлением имени удалённого файла.

1. Определите область и права оператора

Начните с отдельного каталога C:\AuditPilot и согласованной тестовой группы. Не включайте аудит всех операций Everyone на всём системном диске: поток быстро заполнит Security и усложнит расследование. Задайте конкретную цель — запись изменения и удаления рабочих файлов — и оцените объём событий.

Оператору нужны права чтения Security, а для изменения SACL — необходимые полномочия, включая Manage auditing and security log. Права администратора приложения не равны этим полномочиям. Сохраните исходную политику и SACL; DACL, которая разрешает доступ к файлам, не должна заменяться настройкой аудита.

2. Включите File System в расширенном аудите

В GPO компьютера откройте Advanced Audit Policy → Object Access → Audit File System. Включите Success для подтверждённых операций, а Failure — если требуется исследовать отказы. Сначала примените политику к тестовому файловому серверу. Проверьте результирующие параметры:

auditpol /get /category:*
gpresult /scope computer /r

Названия подкатегорий локализованы, поэтому пример читает всё, а не пытается устанавливать англоязычную строку на любой ОС. File System работает вместе с SACL и сам не начинает писать все операции. Требования описаны в Audit File System.

До изменения сохраните политику и состояние журнала. Команды выполняются на выбранном пилотном сервере; каталог отчётов создайте и ограничьте заранее:

auditpol /backup /file:C:\AdminReports\FileAudit\auditpol-before.csv
wevtutil gl Security

В выводе Security проверьте enabled, maxSize и режим хранения. Не задавайте универсальный размер «достаточно 100 MB»: оцените поток на пилоте и нужный период. Пример: при 200 MB событий в сутки журнал 1 GB без пересылки не гарантирует месяц истории. Увеличение журнала требует места на системном томе; стратегия хранения и центральная копия задаются отдельно. Утилита описана в wevtutil.

3. Сохраните исходную SACL и добавьте пилотное правило

Создайте каталог заранее с согласованными рабочими DACL. Затем прочитайте ACL с аудитом и сохраните исходную часть Audit отдельно. Каталог отчётов должен быть защищён от чтения обычными пользователями.

$path = 'C:\AuditPilot'
$reportDir = 'C:\AdminReports\FileAudit'
New-Item -ItemType Directory -Path $reportDir -Force | Out-Null
$acl = Get-Acl -LiteralPath $path -Audit
$section = [Security.AccessControl.AccessControlSections]::Audit
$before = $acl.GetSecurityDescriptorSddlForm($section)
$before | Set-Content (Join-Path $reportDir 'sacl-before.txt') -Encoding UTF8
$principal = [Security.Principal.NTAccount]::new('EXAMPLE\AuditPilotUsers')
$rights = [Security.AccessControl.FileSystemRights]::Delete -bor
          [Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
          [Security.AccessControl.FileSystemRights]::WriteData
$inherit = [Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
           [Security.AccessControl.InheritanceFlags]::ObjectInherit
$rule = [Security.AccessControl.FileSystemAuditRule]::new(
    $principal, $rights, $inherit,
    [Security.AccessControl.PropagationFlags]::None,
    [Security.AccessControl.AuditFlags]::Success)
$acl.AddAuditRule($rule)
Set-Acl -LiteralPath $path -AclObject $acl
$afterAcl = Get-Acl -LiteralPath $path -Audit
$afterAcl.GetSecurityDescriptorSddlForm($section) |
    Set-Content (Join-Path $reportDir 'sacl-after.txt') -Encoding UTF8
$afterAcl.Audit | Format-List

Замените домен и группу действительным субъектом. Правило наследуется дочерними объектами, если их ACL допускает это наследование. DeleteSubdirectoriesAndFiles относится к удалению дочерних объектов; право может участвовать в операции наряду с Delete самого файла. Точное количество событий проверяйте на пилоте, прежде чем расширять область.

Проверьте SACL на реальном дочернем тестовом объекте и защиту наследования:

$testPath = 'C:\AuditPilot\audit-permission-test.txt'
Get-Acl -LiteralPath $testPath -Audit |
    Select-Object AreAuditRulesProtected, AuditToString
Get-Acl -LiteralPath 'C:\AuditPilot' -Audit |
    Select-Object AreAuditRulesProtected, AuditToString

Файл сначала создаётся согласованным тестовым пользователем. AreAuditRulesProtected=True у дочернего объекта означает, что наследование SACL защищено; правило родителя не следует считать применённым автоматически. В данном скрипте добавлен только Success. Если нужен аудит отказа, добавьте отдельное согласованное Failure-правило и соответствующую категорию, затем проверяйте 4656 с Audit Failure — не ищите «неудачный 4663».

4. Различайте 4656, 4663 и 4660

4656 показывает запрос handle с правами, включая отказ при соответствующей настройке. 4663 отражает использование права и содержит имя объекта; его наличие с DELETE не является безусловным доказательством завершённого удаления. 4660 подтверждает удаление объекта, но не содержит удобного имени файла.

В описании 4663 приведены маски доступа и поля. Описание 4660 объясняет необходимость корреляции. Переименование и другие операции могут сопровождаться правами удаления; поэтому формулировка «пользователь удалил файл» требует дополнительных фактов.

Сопоставляйте компьютер, SubjectLogonId, ProcessId, HandleId и близкое время. Handle может использоваться повторно, а журнал — быть неполным. Нулевой или неизвестный handle не подходит для уверенного сопоставления. Сохраняйте исходные события, а не только текст итогового сообщения.

5. Выгрузите события с ограниченным сопоставлением

Код ниже рассчитан на Windows PowerShell 5.1 и локальный запуск на файловом сервере. Он читает последние 30 минут Security, разбирает XML и создаёт JSONL. Для 4660 имя берётся из ранее наблюдаемого файлового события того же handle в пределах 120 секунд; это помечается как сопоставление, а не прямое поле события удаления.

$start = (Get-Date).AddMinutes(-30)
$filter = @{ LogName = 'Security'; Id = @(4656,4658,4660,4663); StartTime = $start }
try {
    $events = @(Get-WinEvent -FilterHashtable $filter -MaxEvents 20000 -ErrorAction Stop |
        Sort-Object RecordId)
} catch {
    if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*') { $events = @() }
    else { throw }
}
if ($events.Count -ge 20000) { throw 'Collection limit reached; reduce window' }
$handles = @{}
$rows = foreach ($event in $events) {
    [xml]$xml = $event.ToXml()
    $data = @{}
    foreach ($d in $xml.Event.EventData.Data) {
        $data[[string]$d.Name] = [string]$d.'#text'
    }
    $usable = $data.HandleId -and $data.HandleId -ne '0x0' -and
              $data.ProcessId -and $data.SubjectLogonId
    $key = @($event.MachineName, $data.SubjectLogonId,
             $data.ProcessId, $data.HandleId) -join '|'
    if ($usable -and $event.Id -in @(4656,4663) -and
        $data.ObjectType -eq 'File' -and $data.ObjectName) {
        $handles[$key] = @{ Name = $data.ObjectName; Time = $event.TimeCreated }
    }
    $name = $data.ObjectName
    $mapping = 'DirectFieldOrAbsent'
    if ($event.Id -eq 4660) {
        $mapping = 'DeleteEvent_NameUnknown'
        if ($usable -and $handles.ContainsKey($key)) {
            $age = ($event.TimeCreated - $handles[$key].Time).TotalSeconds
            if ($age -ge 0 -and $age -le 120) {
                $name = $handles[$key].Name
                $mapping = 'DeleteEvent_NameCorrelatedWithin120s'
            }
        }
    }
    if ($event.Id -eq 4658) {
        $handles.Remove($key)
        continue
    }
    [pscustomobject]@{
        Computer = $event.MachineName; EventId = $event.Id
        RecordId = $event.RecordId
        TimeUtc = $event.TimeCreated.ToUniversalTime().ToString('o')
        Subject = "$($data.SubjectDomainName)\$($data.SubjectUserName)"
        LogonId = $data.SubjectLogonId; ProcessId = $data.ProcessId
        ProcessName = $data.ProcessName; HandleId = $data.HandleId
        ObjectName = $name; Mapping = $mapping
        AccessMask = $data.AccessMask; OriginalData = $data
    }
}
$out = 'C:\AdminReports\FileAudit\events.jsonl'
$lines = @($rows | ForEach-Object { $_ | ConvertTo-Json -Depth 6 -Compress })
[IO.File]::WriteAllLines($out, [string[]]$lines, [Text.UTF8Encoding]::new($false))

В данном примере файл events.jsonl заменяется при запуске. Для постоянного архива используйте имена с UTC-временем, сроки хранения и защиту записи. Сопоставление не гарантирует полноту, если предыдущий handle открыт раньше окна, событие не записалось или handle переиспользован. Неизвестное имя должно оставаться неизвестным, а не угадываться по ближайшей строке.

Аудит файлов: политика, SACL, тестовая операция, XML событий и корреляция удаления с именем
Порядок проверки и настройки. Команды, ограничения и диагностика приведены в статье.

Для выборки использованного права DELETE преобразуйте AccessMask как hexadecimal, а не сравнивайте только строку 0x10000: маска может включать несколько прав одновременно.

$rows | Where-Object EventId -EQ 4663 | Where-Object {
    if (!$_.AccessMask) { return $false }
    $mask = [Convert]::ToUInt64(($_.AccessMask -replace '^0x',''), 16)
    ($mask -band 0x10000) -ne 0
} | Select-Object TimeUtc, Subject, ObjectName, ProcessName, AccessMask

Такой результат означает использование права DELETE, а не самостоятельный вывод о завершённом удалении. Для CSV используйте явное разворачивание полей; OriginalData остаётся в JSONL и не превращается в строку System.Collections.Hashtable:

$rows | Select-Object Computer, EventId, RecordId, TimeUtc, Subject,
    LogonId, ProcessId, ProcessName, HandleId, ObjectName, Mapping, AccessMask |
    Export-Csv 'C:\AdminReports\FileAudit\events.csv' -NoTypeInformation -Encoding UTF8

CSV нужен для просмотра, исходные EVTX/XML — для проверки события. Не обещайте доказанное удаление по одной строке отчёта, особенно если Mapping указывает Unknown или журнал неполон.

6. Проверьте настройки контролируемым тестом

Под согласованным тестовым пользователем создайте новый файл в пилотной папке, измените его и удалите только этот созданный файл. Запишите точное имя, время и пользователя. Не проверяйте удаление на рабочем документе.

$testFile = Join-Path 'C:\AuditPilot' ("audit-test-{0}.txt" -f [guid]::NewGuid())
New-Item -ItemType File -Path $testFile -ErrorAction Stop | Out-Null
Add-Content -LiteralPath $testFile -Value 'Controlled audit test'
Remove-Item -LiteralPath $testFile -ErrorAction Stop

После теста выполните выгрузку и найдите нужные записи. Проверьте 4660 и путь из сопоставления, а также исходные XML-поля. Для SMB повторите действие с тестового клиентского компьютера: локальная и сетевая операция могут отличаться по процессу и контексту. При отсутствии события сначала проверьте SACL на самом файле, политику и выбранный сервер.

7. Учитывайте SMB, сервисы и журналы входа

События 5140/5145 помогают исследовать доступ через общий ресурс, но не заменяют доказательство удаления NTFS. Subject может быть служебной учётной записью или контекстом процесса. Имя аккаунта в Security не всегда однозначно устанавливает конкретного человека: сопоставьте LogonId с входом, клиентским адресом, приложением и заявкой.

Если удаление выполнял агент backup, синхронизации или антивирус, исследуйте его собственные журналы. Не делайте вывод о злонамеренности только по имени процесса. Для спорных случаев сохраните исходные EVTX и временную линию; изменение прав и чистка логов во время расследования могут уничтожить полезные данные.

Сохраните первичные события отдельно от производного JSON. Следующий фильтр экспортирует последние полчаса интересующих ID из Security без очистки журнала:

wevtutil epl Security C:\AdminReports\FileAudit\file-window.evtx "/q:*[System[(EventID=4656 or EventID=4658 or EventID=4660 or EventID=4663) and TimeCreated[timediff(@SystemTime) <= 1800000]]]" /ow:false
Get-FileHash -Algorithm SHA256 C:\AdminReports\FileAudit\file-window.evtx

Если файл уже существует, выберите новое имя; /ow:false защищает предыдущую выгрузку. Удаление файла с ACL-аудитом не равно очистке Security, поэтому не используйте wevtutil cl для подготовки теста. Для чтения сохранённого набора можно выполнить Get-WinEvent -Path ... -Oldest, ограничив фильтр и сохранив источник/RecordId.

8. Организуйте постоянный сбор

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

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

9. Верните только пилотные настройки

Перед возвратом сравните текущую SACL с сохранённой после изменения. Если она уже менялась другим администратором, остановитесь и согласуйте объединение, чтобы не стереть новые правила. Следующий пример восстанавливает только раздел Audit из исходной копии, сохраняя текущую DACL.

$section = [Security.AccessControl.AccessControlSections]::Audit
$current = Get-Acl -LiteralPath 'C:\AuditPilot' -Audit
$expected = (Get-Content 'C:\AdminReports\FileAudit\sacl-after.txt' -Raw).Trim()
if ($current.GetSecurityDescriptorSddlForm($section) -ne $expected) {
    throw 'SACL changed after pilot: review required'
}
$baseline = (Get-Content 'C:\AdminReports\FileAudit\sacl-before.txt' -Raw).Trim()
if ([string]::IsNullOrWhiteSpace($baseline)) { $baseline = 'S:' }
$current.SetSecurityDescriptorSddlForm($baseline, $section)
Set-Acl -LiteralPath 'C:\AuditPilot' -AclObject $current

Убедитесь, что файлы копий относятся к тому же каталогу и сохранены до нужного изменения. Результат и наследование проверьте повторно. Политику GPO возвращайте отдельно по исходному состоянию; уже собранные журналы сохраняйте.

10. Зафиксируйте границы результата

Отчёт должен указывать период, серверы, папки, субъектов SACL, включённые категории, фактическую полноту и способ сопоставления. Разделяйте «событие удаления зарегистрировано», «имя сопоставлено» и «личность человека подтверждена дополнительными источниками».

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

Связанные инструкции: аудит изменений AD, проверка назначения группы AD. Разбор трёх результатов. Есть 4663 DELETE, но нет 4660: зафиксировано использованное право, проверьте переименование, полноту и время журнала. Есть 4660, но имя Unknown: удаление зарегистрировано, сопоставление имени не доказано; расширьте окно и исследуйте исходные события. Есть 4660 и согласованная корреляция плюс известный тестовый файл: пилот подтвердил механизм наблюдения, но для рабочего расследования всё равно требуется контекст SubjectLogonId и сервиса. Такой разбор позволяет объяснить вывод, а не только прислать уведомление.