Die gerade wirksame Überwachungsrichtlinie auf einem Windows-Rechner zu kennen ist wichtig – sei es für die Überprüfung der CIS-Konformität oder weil man bestimmte Audit Logs sammeln will und sicher gehen möchte, dass sie auch tatsächlich generiert werden. Microsoft hält dafür seit jeher das Kommandozeilenprogramm „auditpol.exe“ vor, welches sogar mit dem Zusatz-Parameter „/r“ dazu bewegt werden kann, Ausgabe im CSV-Format zu generieren. Nur leider hat die Sache einen Haken:
Der Klassiker – die Ausgabe ist abhängig von der Spracheinstellung! Besonders perfide: Die Kategorie- bzw. Subkategorienamen sind auf modernen Windows-Versionen sogar schon in Englisch, die Subkategorie ist zusätzlich als allgemeingültige GUID abgebildet, doch der eingestellte Wert ist leider immer sprachabhängig. Ein anderer Ansatz muss her.
.NET ist für diese Funktion leider keine direkte Hilfe, die Windows-API, die auditpol.exe aufruft, muss also direkt angesprochen werden. Für die Aufgabe, einfach die effektive Einstellung zu ermitteln, genügen uns zwei Funktionen: AuditQuerySystemPolicy und AuditEnumerateSubCategories. Nach einigem Fummeln kommen wir also zum folgenden Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
$classDefinition = @" using System; using System.Collections.Generic; using System.Runtime.InteropServices; namespace Audit { public class Pol { [DllImport("advapi32.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true)] public static extern bool AuditEnumerateSubCategories(Guid AuditCategoryGuid, bool RetrieveAllSubCategories, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] out Guid[] auditSubCategories, out uint numSubCategories); [DllImport("advapi32.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true)] public static extern bool AuditQuerySystemPolicy([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), In] Guid[] pSubCategoryGuids, uint dwPolicyCount, out IntPtr ppAuditPolicy); public static AUDIT_POLICY_INFORMATION QueryPolicy(Guid sc) { IntPtr ppAuditPolicy; if (AuditQuerySystemPolicy(new Guid[] {sc}, 1, out ppAuditPolicy)) { return ToStructure(ppAuditPolicy); } return new AUDIT_POLICY_INFORMATION(); } public static T ToStructure(IntPtr ptr, long allocatedBytes = -1) { Type type = typeof(T).IsEnum ? Enum.GetUnderlyingType(typeof(T)) : typeof(T); if (allocatedBytes < 0L || allocatedBytes >= (long) Marshal.SizeOf(type)) { return (T) Marshal.PtrToStructure(ptr, type); } throw new InsufficientMemoryException(); } public struct AUDIT_POLICY_INFORMATION { public Guid sc; public uint ai; public Guid ca; } } } "@ Add-Type -TypeDefinition $classDefinition -Language CSharp $regLegacy = @{ 'Path' = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' 'Name' = 'SCENoApplyLegacyAuditPolicy' 'ErrorAction' = 'SilentlyContinue' } $regLegacyAudit = Get-ItemProperty @regLegacy $res = @{ 'LegacyDisabled' = ($regLegacyAudit -eq 1) } $subCategories = @() $numberOfSubcats = 0 $Category = [Guid]::Empty # this doesn't matter because we retrieve all subcats at once if ([Audit.Pol]::AuditEnumerateSubCategories($Category, $true, [ref]$subCategories, [ref]$numberOfSubcats)) { foreach ($c in $subCategories) { $Policy = -1 $Policy = [Audit.Pol]::QueryPolicy($c) $res.Add($c.Guid,$Policy.ai) } } $res |
Dieser gibt das absolut notwendige Minimum: die GUID und den eingestellten Wert: erstes Byte = Erfolg, zweites Byte = Fehler, somit
- 0 = keine Überwachung
- 1 = Erfolg
- 2 = Fehler
- 3 = Erfolg und Fehler
Zusätzlich ermitteln wir noch den aus der Gruppenrichtlinie gesetzten Registry-Wert, der die Maschine zwingt, die „modernen“ Subkategorien statt der alten, einfachen Überwachungsrichtlinie zu verwenden.
Happy Audtitng! Im Teil 2 schauen wir uns an, wie das ganze remote aussieht.