1 Einleitung

Ein wichtiges, vielleicht sogar das entscheidende Merkmal für den jahrelangen Erfolg von Microsofts ActiveDirectory ist die einzigartige Möglichkeit einfach und effizient auch große Mengen Clients und Server mittels GPOs verwalten zu können. 
Aus diesem Grund will ich hier etwas hinter die Kulissen von GPOs schauen, bevor ich auf die Möglichkeiten der Powershell zu diesem Thema eingehe. Angesichts der bereits existierenden, riesigen Menge an Informationen über die Funktionisweise und Anwendung von GPOs, beschränke ich mich nur auf die Beschreibung der GPO-Objekte.

Eine gute Beschreibung über den Aufbau und die Verwendung von GPOs liefern diese TechnetArtikel:
Technet: Group Policy Basics – Part 1: Understanding the Structure of a Group Policy Object
Technet: Group Policy Basics – Part 2: Understanding Which GPOs to Apply
Technet: Group Policy Basics – Part 3: How Clients Process GPOs

Technet: How Core Group Policy Works

1.1 Aufbau einer GPO

Eine GPO besteht aus drei Komponenten, die an unterschiedlichen Stellen liegen

1.1.1 GPC - Group Policy Container - der GPC liegt im LDAP und wird über die AD-Replikation zwischen den DCs repliziert
1.1.2 GPT - Group Policy Template - das GPT liegt im Sysvolverzeichnis und wird über die Filereplikation (FRS bzw. DFSR) zwischen den DCs repliziert
1.1.3 CSE - Client Side Extenstion - CSEs sind auf den Clients gespeichert und setzen die GPO-Settings dort um

1.1.1 GPC - Group Policy Container

Den GPC einer GPO mit seinenAttributes kann man sich je nach Geschmack beispielsweise über LDP.exe oder das "users and computers" - Snapin in einer GUI anzeigen lassen.

beide Darstellungen (LDP.exe/ "Users and Computers") liefern natürlich dieselben Informationen, wie

- whenCreated, whenChanged
- displayname
- distinguishedname
- gPCFileSysPath (=Link zum GPT im Sysvol-Verzeichnis)
- gPCMachineExtensionNames, gPCMachineExtensionNames (=Liste der CSEs in dieser GPO, siehe 1.1.3)
- gPCUserExtensionNames, gPCUserExtensionNames (=Liste der CSEs in dieser GPO, siehe 1.1.3)
- versionNumber (=Versionsnummer der Policy, die auch in der GPT.ini im Sysvolverzeichnis zu finden ist, siehe 1.1.2)

Beispiel 1: Auslesen von Properties aus einem Group Policy Container

Clear-Host
Set-StrictMode -Version "2.0"

Function Main{
  #Set here the policy you want to to check"
  $GPO2Check = "Default Domain Policy"

    $Domain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
 
  $rootDSE = [ADSI]"LDAP://rootDSE"
  $DomainDN = $rootDSE.DefaultNamingContext
  $rootDSE.psbase.AuthenticationType = [System.DirectoryServices.AuthenticationTypes]::FastBind
  $PDCe = $Domain.PDCRoleOwner.Name
  $rootDSE.psbase.Path = "LDAP://$PDCe/CN=Policies,CN=System," + $DomainDN

  $DirectorySearcher = ([ADSISearcher]"LDAP://")
  $DirectorySearcher.SearchRoot=$rootDSE
  $DirectorySearcher.Filter = "(objectclass=groupPolicyContainer)"
  $DirectorySearcher.Filter = "(objectclass=*)"
  $DirectorySearcher.SearchScope = [System.DirectoryServices.SearchScope]::OneLevel
  $DirectorySearcher.Pagesize = 1000
  $GPOs = $DirectorySearcher.Findall()

  Foreach($GPO in $GPOs){
     If ( $($GPO.properties.displayname) -eq $GPO2Check ){
       write-host "displayname: $($GPO.properties.displayname)"
       write-host "distinguishedname: $($GPO.properties.distinguishedname)"
       write-host "whenChanged: $($GPO.properties.whenchanged)"
       write-host "whenChanged: $($GPO.properties.whencreated)"
       write-host "gPCFileSysPath: $($GPO.properties.gpcfilesyspath)"
       write-host "VersionNumber: $($GPO.properties.versionnumber)"
       Try{
         write-host "gPCMachineExtensionNames: $($GPO.properties.gpcmachineextensionnames)"
       }Catch{   
         write-host "gPCMachineExtensionNames: "
       }   
       Try{
          write-host "gPCUserExtensionNames: $($GPO.properties.gpcuserextensionnames)"
       }Catch{   
         write-host "gPCUserExtensionNames: "
       }   
     }
  }#ForEach $GPO
 
}#End Main

Main
 
#mögliche Ausgabe

displayname: Default Domain Policy
distinguishedname: CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=f1,DC=intern
whenChanged: 10/12/2015 19:37:24
whenChanged: 10/12/2015 19:29:10
gPCFileSysPath: \\f1.intern\sysvol\f1.intern\Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}
VersionNumber: 3
gPCMachineExtensionNames: [{35378EAC-683F-11D2-A89A-00C04FBBCFA2}{53D6AB1B-2488-11D1-A28C-00C04FB94F17}][{827D319E-6EAC-11D2-A4EA-00C0
4F79F83A}{803E14A0-B4FB-11D0-A0D0-00A0C90F574B}][{B1BE8D72-6EAC-11D2-A4EA-00C04F79F83A}{53D6AB1B-2488-11D1-A28C-00C04FB94F17}]
gPCUserExtensionNames:
 

- der erste Teil des DistinguishedNames ist die GUID der GPO

- Auf die Eigenschaft "VersionNumber" gehe ich weiter unten bei den Policy Templates näher ein.

- Beachtet hier die beiden Eigenschaften "gPCMachineExtensionNames" und "gPCUserExtensionNames"! Die aufgeführten GUIDs dieser Eigenschaften findet man in der Registry der Clients wieder, auf denen diese Policies umgesetzt werden. (siehe 1.1.3 unten)

- Ist eine oder mehrerd GPO(s) auf ein ADObjekt (OU/ Site/ Domain) verlinkt, so taucht oder tauchen der oder die DistinguishedName(n) der GPO(s) in der Eigenschaft "gplink" der OU wieder auf. Vererbt werden die Properties allerdings nicht.

besser als in der ADUC sind die beiden hier verlinkten GPO-DNs (0C9.. und 03F...) in LDP.exe zu sehen:


1.1.2 GPT - Group Policy Template

Das zu einer GPO gehörende Template (GPT) liegt im Sysvol-Verzeichnis. Die Verbindung zwischen GPC und GPT erfolgt über die GUID aus dem DistinguishedName (siehe oben Kapitel 1.1.1)

Im Folgenden wird das Sysvolverzeichnis mit seinen Unterordnern genauer beschrieben:

a) Das default-Verzeichnis aller Policy-Files liegt auf jedem DC unter "%SystemRoot%\Sysvol\sysvol\%DomainName%".

Ihr seht auf dieser Ebene drei Ordner:

- Polices: Hier liegen die GPT Dateien für jede erstellte GPO
- scripts: Hier liegen Start-/ Shutdown-/ Logon- und Logoff-Skripte der GPOs
- StarterGPOs: Sofern in der Domäne StarterGPOs verwendet werden, liegen diese hier

Die GPT-Teil der GPOs liegt also im Sysvolverzeichnis und unterliegt damit der Sysvolreplikation, die andere Mechanismen und Replikationsintervalle der AD-Replikation besitzt. Nach der Änderung einer Policy ist die Versionsnummer der GPT und GPC solange synchron sind, bis beide Replikationsmechanismen vollständig durchgeführt wurden.

b) Sehen wir uns mal einen beliebigen PolicyContainer unterhalb von Policies etwas genauer an:

Normalerweise sieht man im PolicyOrdner die beiden Unterordner "User" und "Machine", sowie die GPT.ini mit der Versionsnummer. Bei der Verwendung von Legacy ADM-Files kann hier noch ein ADM-Verzeichnis liegen, sofern diese nicht im "central store", Wenn der GPO ein Kommentar gegeben wurde, liegt hier auch noch die Textdatei GPO.cmt, die den Kommentar enthält.

In der GPT.ini, die in jedem PolicyOrdner liegt, sieht man auch die Versionsnummer der Policy. Im folgenden Bild habe ich von einer Policy den Blick ins LDAP, den Blick in die GPT.ini und den Blick auf die GUI nebeneinander gestellt.

Die Versionsnummern werden nicht incrementell erhöht, sondern nach der folgenden Formel berechnet:
versionNumber = {User Node: upper 16 bits}{Machine Node: lower 16 bits}
oder in weniger mathematischer Form:
versionNumber = (Number of User Configuration changes * 65536) + (Number of Computer Configuration changes)

c) Sehen wir jetzt noch kurz in den Machine- und User Ordner hinein

Mit dem CommandLine-Tool "Tree /F"  wird der Verzeichnisbaum innerhalb einer Beispielpolicygezeigt

Die Zugehörigkeit der meisten Unteräste, wie Skripts oder Preferences dürften wohl klar sein.

Nur kurz beschrieben:
GptTmpl.inf -> enthält die Settings aus "Windows Settings \ Security Settings"
Registry.pol -> enthält die in den "Administrative Templates" gesetzten Registry Settings, ebenso wie Settings aus den "Software Restriction Policies"
Fdeploy.ini -> Enthält Einstellungen aus "User Configuration \ Windows Settings \ Folder Redirection

Näher auf die einzelnen Dateien will ich jetzt auch nicht mehr eingehen.

 

(Security-)Anmerkung zu Passwörtern in Group Policy Preferences

Als Microsoft mit Server 2008 die GPPs eingeführt hat, war die Begeisterung groß. Administratoren wurde mit GPPs der langgehegte Wunsch erfüllt, von zentraler Stelle aus Passwörter auf Clients zu setzen, ohne dass die Endanwender diese Passwörter sehen konnten. Die verschlüsselten Hashs dieser Passwörter wurden in den Preferences-Files auf jedem DC im Sysvol gespeichert, beispielsweise für

  • Drive Maps
  • Local Users and Groups
  • Scheduled Tasks
  • Services
  • Data Sources

Kurz gesagt, diese verschlüsselten Passworthashes sind in Sekunden dekodiert. Sollte Euch als Administratoren dieser Sachverhalt nicht bekannt vorkommen, lest bitte ganz dringend diese Artikel

MS14-025: Vulnerability in Group Policy Preferences could allow elevation of privilege: May 13, 2014

Compass Security Blog: Exploit credentials stored in Windows Group Policy Preferences

 

1.1.3 CSEs - Client Site Extensions

Client Site Extensions (kurz CSEs) sind etwa 50 Erweiterungen, die die resultierenden Anweisungen aus den GPO auf einem Clientrechner tatsächlich umsetzen. Eine Liste findet man hier
Technet: Group Policy Client Side Extension List
Im Beispiel oben aus 1.1.1 wird beispielsweise die CSE mit der GUID "35378EAC-683F-11D2-A89A-00C04FBBCFA2" für eine Machineextension benötigt. In dem gerade genannten Technetartikel seht ihr, dass die Erweiterung mit dieser GUID für RegistrySettings zuständig ist.
Die CSEs werden vom Client bei Bedarf aus DLL-Dateien geladen. Welche CSEs der Client geladen hat und wie die zugehörige DLL-Datei lautet, kann man bei den meisten CSEs in der Registry am Client nachsehen.

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions

Auch die anderen Einträge wie "NoSlowLink" verraten einiges über diese CSE. Die "1" von "NoSlowLink" zeigt, dass die CSE für "Microsoft Disk Quota" auch über bei einem Slow Link angewendet wird. Bei 0 würde sie es nicht.
Laut dem Technetartikle Client-side Extension Preferences soll man an diesen Werten nichts verändern, daher gehe ich hier auch nicht mehr tiefer.

2 .Net

Beispiel 1: Alle GPos einer Domäne exportieren (.Net-Klassen)

$Assembly = [Reflection.Assembly]::Load("Microsoft.GroupPolicy.Management, Version=2.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35")
$Domain = New-Object Microsoft.GroupPolicy.GPDomain
$Domain.GetAllGpos()


3 LDAP

Im folgenden Beispiel lese ich am PDC-Emulator alle Policynamen per LDAP aus. Anschließend vergleiche ich auf jedem Domaincontroller die Versionsnummern zwischen dem LDAPVerzeichnis und der der GPT.ini des PDC-Emulators.

Beispiel 1: Konsistenzcheck aller GPOs einer Domäne (LDAP)

Außer eventuell der Variable $Outfile braucht man an dem Skript nichts anpassen  

#this script checks on all DCs of a domain if all policy version numbers are consistent
 
Clear-Host
Set-StrictMode -Version "2.0"
 
Function Main{
  #get some properties of the current domain
  $Domain=[System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()
  $DCs = @($Domain.FindAllDomainControllers()) | Foreach{  $( ($_.name).Split(".") )[0]}
 
  #define the output path
  $OutFile = "PolicyConsistency-$Domain.log"
  $FilePath = "$(Get-ScriptParentPath)\$OutFile"
  #remove eventually existing file
  Remove-Item $FilePath -recurse -EA 0
  Print-Out "$(Get-Date)`n" $FilePath
 
  #function call to get an array of all Domainpolicies from the Domain PDCe
  $PDCeGPOs = Get-PoliciesFromDC $Domain $($Domain.PdcRoleOwner.Name)
  
  #go through every policy existing on the PDCe
  Foreach($PDCeGPO in $PDCeGPOs){
     Print-Out "Policyname: $($PDCeGPO.DisplayName) - $($PDCeGPO.Name)" $FilePath
   
     #go through every DC   
     $DcGPOs =@()
     Foreach($DC in $DCs){
       #function call to get an array of all domainpolicies from every DC 
       $DcGPOs = Get-PoliciesFromDC $Domain $DC
     
       #Select the DCGPO which has $PDCeGPO-displayname 
       $DcGPO = $DCGPOs | Where { $_.Displayname -eq $($PDCeGPO.DisplayName) }
     
       #errorhandling, if duplicate names occur
       If( $($DCGPO.gettype().name) -eq "Object[]") {
           Print-Out "Error: duplicate policyname - $($PDCeGPO.DisplayName) on $DC" $FilePath
           Continue
       }
 
       #output ("error" or not) depends if the versionnumber in the DC-GPT.ini 
       #is consistent to the LDAP-Version and the PDCe-GPT.ini
       If ( $($PDCeGPO.LDAPVersion) -ne $($DCGPO.LDAPVersion) ){
         $VersionINI = "Error $DC`: LDAPVersion on DC / Version GPT.ini on PDCe  are not consistent: "
         $VersionINI += "$($DCGPO.LDAPVersion)/$($PDCeGPO.LDAPVersion)"
         Print-Out $VersionINI $FilePath
       }Else{ #ok
         $VersionINI = "$DC`: LDAPVersion on DC / Version GPT.ini on PDCe  are consistent: "
         $VersionINI += "$($DCGPO.LDAPVersion)/$($PDCeGPO.LDAPVersion)"
         Print-Out $VersionINI $FilePath
       }#Else/If
     }#Foreach $DC
 
  #Separator after every policy
  Print-Out "###############################################" $FilePath
  }#ForEach $GPO
  Write-Host "`nOutPutfile: $filePath" -backgroundcolor green -foregroundcolor yellow
}#End Main
 
Function Get-PoliciesFromDC{
  #Function reads all policies in the LDAP-Container "CN=Policies,CN=System,<Domain>" of a DC
  Param($Domain,$DC)
  
  #Write-Host "$DC"
  $rootDSE = [ADSI]"LDAP://rootDSE"
  $DomainDN = $rootDSE.DefaultNamingContext
  $rootDSE.psbase.AuthenticationType = [System.DirectoryServices.AuthenticationTypes]::FastBind
  $rootDSE.psbase.Path = "LDAP://$DC/CN=Policies,CN=System," + $DomainDN
  $PDCe = $Domain.PDCRoleOwner.Name 
 
  $DirectorySearcher = ([ADSISearcher]"LDAP://")
  $DirectorySearcher.SearchRoot=$rootDSE
  $DirectorySearcher.Filter = "(objectclass=groupPolicyContainer)"
  $DirectorySearcher.Filter = "(objectclass=*)"
  $DirectorySearcher.SearchScope = [System.DirectoryServices.SearchScope]::OneLevel
  $DirectorySearcher.Pagesize = 1000
  $GPOs = $DirectorySearcher.Findall()
  
  #create a PsObjectArray $PSo_GPOs containing all GPOs of a DC 
  $PSo_GPOs = @()
  $i=0
  Foreach ($GPO in $GPOs){
    #$i += 1 
    [psobject]$PSo_GPO = "" | Select-Object DisplayName,Name, LDAPVersion, versionINI
    $PSo_GPO.DisplayName = $($GPO.properties.displayname).ToString()
    #$PSo_GPO.DisplayName
    $PSo_GPO.Name = $($GPO.properties.name).ToString()
    $PSo_GPO.LDAPVersion = $($GPO.properties.versionnumber) -as [int]
    If ($DC -eq $PDCe){
      $PSo_GPO.VersionINI = Get-VersionNumber_from_GPTini $($GPO.properties.name) $DC $Domain
    }
 
    #Write-host $i
    $PSo_GPOs += $PSo_GPO
  }#foreach
  Return ,$PSo_GPOs
}#End Get-PoliciesFromDC
 
Function Get-VersionNumber_from_GPTini{
  Param($name,$DC,$Domain)
  $Path = "\\$DC\sysvol\$($Domain.Name)\Policies\$name\GPT.ini"
  $Pattern = "version="
 
  If (Test-Path $path){
   $Hit = Select-String -Path $Path -Pattern $Pattern
      If ($Hit -ne $Null){
        $VersionNumber = $($Hit.Line).split("=")[1] -as [int]
      }Else{
       $VersionNumber = "`n" + 'line "version=<number>" not found ' + "in $Path "
      }#Hit   
  }Else{ 
   $VersionNumber = "NotFound "
  }#Else/If   
  Return $VersionNumber
}#end Get-VersionNumber_from_GPT.ini
 
Function Get-ScriptParentPath{
  $Invocation = (Get-Variable MyInvocation -Scope 2).Value
  Try{
    $ThisScriptPath = Split-Path $Invocation.MyCommand.Path
  }Catch{
   Write-Host "please save this script before using"
   break
  }
  $ThisScriptFullPath = $($MyInvocation.InvocationName)
  Return $ThisScriptPath
}#end Get-ScriptParentPath
 
Function Print-Out{
 Param($outString,$FilePath)
   if ($OutString -match "Error"){
     Write-Host $OutString  -BackgroundColor Red -ForeGroundColor Yellow
   }Else{
     Write-Host $OutString  
   }#If
   Out-File -FilePath $FilePath -InputObject $OutString -Append
}#End Print-Out
 
Main

#mögliche Ausgabe

 

10/25/2014 21:40:41
 
Policyname: CTL - {2BE7720F-E718-4580-8DD4-DD16A04B448B}
DC01: LDAPVersion on DC / Version GPT.ini on PDCe  are consistent: 17/17
Dom1DC02: LDAPVersion on DC / Version GPT.ini on PDCe  are consistent: 17/17
###############################################
Policyname: Default Domain Policy - {31B2F340-016D-11D2-945F-00C04FB984F9}
DC01: LDAPVersion on DC / Version GPT.ini on PDCe  are consistent: 9/9
Dom1DC02: LDAPVersion on DC / Version GPT.ini on PDCe  are consistent: 9/9
###############################################
Policyname: Default Domain Controllers Policy - {6AC1786C-016F-11D2-945F-00C04fB984F9}
DC01: LDAPVersion on DC / Version GPT.ini on PDCe  are consistent: 7/7
Dom1DC02: LDAPVersion on DC / Version GPT.ini on PDCe  are consistent: 7/7
###############################################
Policyname: test02 - {EA8ED2C3-1F17-40DF-A295-40B60D3FD9FA}
DC01: LDAPVersion on DC / Version GPT.ini on PDCe  are consistent: 1/1
Dom1DC02: LDAPVersion on DC / Version GPT.ini on PDCe  are consistent: 1/1
###############################################
 
OutPutfile: C:\temp\30-GPOConsistencychecker\PolicyConsistency-dom1.intern.log

Wiegesagt, sind die GPO-Versionen nicht insync, so sollte man erst einmal etwas abwarten, ob  nach erfolgter AD- und Filereplikation die "Inkonsistenz" immer noch besteht!

Beispiel 2: Backup aller GPOs der Domäne

Set-StrictMode -Version "2.0"
Clear-Host
 
Function Main{
 
  $BackupFolder = "c:\temp\GPOBackup" 
  $Backupcomment = (get-date).ToString("yyyy-MM-dd_hhmmss")
 
  $Assembly = [Reflection.Assembly]::Load("Microsoft.GroupPolicy.Management,`
                  Version=2.0.0.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35")
  $Domain = New-Object Microsoft.GroupPolicy.GPDomain
  $AllGPOs = $Domain.GetAllGpos()
  
  BackupGPOs $BackupFolder $Backupcomment $AllGPOs 
}
 
Function BackupGPOs{
  Param($BackUpfolder,$Backupcomment,$AllGPOs)
  
  #creation of a new subfolder named by the current datetime
  $CurrentBackupFolder = "$BackUpfolder\$Backupcomment"
  $Null = New-Item $CurrentBackupFolder -Type Directory
  
  Foreach ($GPO in $AllGPOs){
     $GPOBackupFolder = "$CurrentBackupFolder\$($GPO.Displayname)"
     $Null = New-Item $GPOBackupFolder -Type Directory
     Try{
       $Null = $GPO.backup($GPOBackupFolder,$Backupcomment)
     }Catch{
          $($GPO.displayname)
          Out-file -FilePath "$CurrentBackupFolder\_Errors.log" -inputobject `
                           "$($GPO.displayname) backup was not successfully"
     } #Try/ catch
  }#foreach
}#function
 
Main