14
READING OFFLINE REGISTRY HIVES

image

The Windows NT registry is a gold mine of information for useful data such as patch levels and password hashes. And that information isn’t just useful for offensive pentesters looking to exploit a network; it’s also useful for anyone in the incident response or data forensics area of information security.

Say, for example, you’re handed the hard drive of a computer that has been breached and you need to find out what happened. What do you do? Being able to read key information from the hard drive regardless of whether Windows can run is imperative. The Windows registry is actually a collection of files on the disk, called registry hives, and learning your way around the registry hives will allow you to better use these hives that hold so much useful information. Registry hives are also a great introduction to parsing binary file formats, which are made to store data efficiently for computers but are not so great for human consumption.

In this chapter, we discuss the Windows NT registry hive data structure, and we write a small library with a few classes to read offline hives from which we can extract useful information, such as the boot key. This is useful if you want to extract password hashes from the registry later.

The Registry Hive Structure

At a high level, the registry hive is a tree of nodes. Each node may have key/value pairs, and it may have child nodes. We’ll use the terms node key and value key to classify the two types of data in the registry hive and create classes for both key types. Node keys contain information about the structure of the tree and its subkeys, whereas value keys hold value information that applications access. Visually, the tree looks a bit like Figure 14-1.

image

Figure 14-1: A visual representation of a simple registry tree with nodes, keys, and values

Every node key has some specific metadata stored alongside it, such as the last time its value keys were modified and other system-level information. All of this data is stored very efficiently for a computer to read—but not for a human. While we implement our library, we’ll skip over some of this metadata in order to make the end result simpler, but I will call these instances out as we go.

As you can see in Figure 14-1, after the registry header, the node tree begins with the root node key. The root node key has two child nodes, which in this example we call Foo and Bar. The Foo node key contains two value keys, Baz and Bat, which have values of true and "AHA", respectively. Bar, on the other hand, only has child node BarBuzz, which has a single value key. This example of a registry hive tree is very contrived and simple. The registry hives on your machine are more complex and likely have millions of keys!

Getting the Registry Hives

During normal operation, Windows locks the registry hives to prevent tampering. Altering the Windows registry can have potentially devastating results, such as an unbootable computer, so it’s not something to take lightly. You can, however, use cmd.exe to export a given registry hive if you have Administrator access to the machine. Windows ships with reg.exe, which is a useful command line utility for reading and writing to the registry. We can use this tool to copy the hives that we’re interested in so that we can read them offline, as shown in Listing 14-1. This will prevent any accidental catastrophes.

Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.
C:Windowssystem32>reg save HKLMSystem C:system.hive
The operation completed successfully.

Listing 14-1: Using reg.exe to copy a registry hive

Using the save subcommand , we specify the registry path we want to save as well as the file to save to. The first argument is the HKLMSystem path, which is the root registry node for the system registry hive (where information such as the boot key resides). By choosing this registry path, we save a copy of the system’s registry hive off the machine for further analysis later. This same technique can be used for HKLMSam (where usernames and hashes are stored) and HKLMSoftware (where patch levels and other software information are stored). But remember, saving these nodes requires administrator access!

There’s also another method for getting the registry hives if you have a hard drive you can mount on your machine. You can simply copy the registry hives from the System32 folder where the raw hives are stored by the operating system. If Windows isn’t running, the hives won’t be locked, and you should be able to copy them to another system. You can find the raw hives currently in use by the operating system in the directory C:Windows System32config (see Listing 14-2).

Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.
C:Windowssystem32>cd config
C:WindowsSystem32config>dir
Volume in drive C is BOOTCAMP
Volume Serial Number is B299-CCD5
Directory of C:WindowsSystem32config
01/24/2016  02:17 PM    <DIR>          .
01/24/2016  02:17 PM    <DIR>          ..
05/23/2014  03:19 AM            28,672 BCD-Template
01/24/2016  02:24 PM        60,555,264 COMPONENTS
01/24/2016  02:24 PM         4,456,448 DEFAULT
07/13/2009  08:34 PM    <DIR>          Journal
09/21/2015  05:56 PM        42,909,696 prl_boot
01/19/2016  12:17 AM    <DIR>          RegBack
01/24/2016  02:13 PM           262,144 SAM
01/24/2016  02:24 PM           262,144 SECURITY
01/24/2016  02:36 PM       115,867,648 SOFTWARE
01/24/2016  02:33 PM        15,728,640 SYSTEM   
06/22/2014  06:13 PM    <DIR>          systemprofile
05/24/2014  10:45 AM    <DIR>          TxR
8 File(s)    240,070,656 bytes
6 Dir(s)  332,737,015,808 bytes free
C:WindowsSystem32config>

Listing 14-2: The contents of the C:WindowsSystem32config folder with registry hives

Listing 14-2 shows the registry hives in the directory. The SECURITY , SOFTWARE , and SYSTEM hives are the ones with the most commonly sought information. Once hives are copied onto your system, you can easily verify that you have saved the registry hives you want to read with the file command if you are using Linux or OS X, as shown in Listing 14-3.

$ file system.hive
system.hive: MS Windows registry file, NT/2000 or above
$

Listing 14-3: Confirming which registry hive you saved in Linux or OS X

Now we’re ready to start digging into a hive.

Reading the Registry Hive

We’ll start by reading the registry hive header, a 4,096-byte chunk of data at the beginning of the registry hive. Don’t worry, only the first 20 bytes or so are actually useful for parsing, and we’ll only read the first four to verify the file is a registry hive. The remaining 4,000+ bytes are just buffer.

Creating a Class to Parse a Registry Hive File

We’ll create a new class to begin parsing the file: the RegistryHive class. This is one of the simpler classes we’ll implement in order to read offline registry hives. It has only a constructor and a few properties, as shown in Listing 14-4.

public class RegistryHive
{
  public RegistryHive(string file)
  {
    if (!File.Exists(file))
      throw new FileNotFoundException();

    this.Filepath = file;

    using (FileStream stream = File.OpenRead(file))
    {
      using (BinaryReader reader = new BinaryReader(stream))
      {
        byte[] buf = reader.ReadBytes(4);

        if (buf[0] != 'r' || buf[1] != 'e' || buf[2] != 'g' || buf[3] != 'f')
          throw new NotSupportedException("File not a registry hive.");

        //fast-forward
       reader.BaseStream.Position = 4096 + 32 + 4;

        this.RootKey = new NodeKey(reader);
      }
    }
  }
  public string Filepath { get; set; }
  public NodeKey RootKey { get; set; }
  public bool WasExported { get; set; }
}

Listing 14-4: The RegistryHive class

Let’s look at the constructor where the magic first happens. The constructor accepts a single argument, which is the file path to the offline registry hive on the filesystem. We check whether the path exists using File.Exists() , and we throw an exception if it doesn’t.

Once we have determined the file exists, we need to make sure it is a registry file. But this is not hard. The first four magic bytes of any registry hive should be r, e, g, and f. To check whether our file matches, we open a stream to read the file using File.OpenRead() . Then we create a new BinaryReader by passing the file stream to the BinaryReader constructor. We use this to read the first four bytes of the file and store them in a byte array. Then, we check whether they match . If they don’t, we throw an exception: the hive is either too damaged to be read normally or is not a hive at all!

If the header checks out, though, we fast-forward to the end of the registry header block to the root node key (skipping some metadata we don’t need at the moment). In the next section, we create a NodeKey class to handle our node keys so we can read the key by passing the BinaryReader to a NodeKey constructor , and we assign the new NodeKey to the RootKey property for later use.

Creating a Class for Node Keys

The NodeKey class is the most complex class we need to implement to read the offline registry hive. There is a bit of metadata stored in the registry hive for node keys that we can skip, but there’s a lot that we can’t. However, the constructor for the NodeKey class is quite simple, though it has quite a few properties, as Listing 14-5 shows.

public class NodeKey
{
  public NodeKey(BinaryReader hive)
  {
    ReadNodeStructure(hive);
    ReadChildrenNodes(hive);
    ReadChildValues(hive);
  }

  public List<NodeKey> ChildNodes { get; set; }
  public List<ValueKey> ChildValues { get; set; }
  public DateTime Timestamp { get; set; }
  public int ParentOffset { get; set; }
  public int SubkeysCount { get; set; }
  public int LFRecordOffset { get; set; }
  public int ClassnameOffset { get; set; }
  public int SecurityKeyOffset { get; set; }
  public int ValuesCount { get; set; }
  public int ValueListOffset { get; set; }
  public short NameLength { get; set; }
  public bool IsRootKey { get; set; }
  public short ClassnameLength { get; set; }
  public string Name { get; set; }
  public byte[] ClassnameData { get; set; }
  public NodeKey ParentNodeKey { get; set; }

Listing 14-5: The NodeKey class constructor and properties

The NodeKey class constructor takes a single argument, which is a BinaryReader for the registry hive. The constructor calls three methods that read and parse specific parts of the node, which we’ll implement next. After the constructor, we define several properties that will be used throughout the next three methods. The first three properties are particularly useful: ChildNodes , ChildValues , and Timestamp .

The first method called in the NodeKey constructor is ReadNodeStructure(), which reads the node key data from the registry hive but not any of its child nodes or values. This is detailed in Listing 14-6.

private void ReadNodeStructure(BinaryReader hive)
{
  byte[] buf = hive.ReadBytes(4);
  if (buf[0] != 0x6e || buf[1] != 0x6b) //nk
    throw new NotSupportedException("Bad nk header");

  long startingOffset = hive.BaseStream.Position;
  this.IsRootKey = (buf[2] == 0x2c) ? true : false;
  this.Timestamp = DateTime.FromFileTime(hive.ReadInt64());

  hive.BaseStream.Position += 4; //skip metadata

  this.ParentOffset = hive.ReadInt32();
  this.SubkeysCount = hive.ReadInt32();

  hive.BaseStream.Position += 4; //skip metadata

  this.LFRecordOffset = hive.ReadInt32();

  hive.BaseStream.Position += 4; //skip metadata

  this.ValuesCount = hive.ReadInt32();
  this.ValueListOffset = hive.ReadInt32();
  this.SecurityKeyOffset = hive.ReadInt32();
  this.ClassnameOffset = hive.ReadInt32();

  hive.BaseStream.Position = startingOffset + 68;

  this.NameLength = hive.ReadInt16();
  this.ClassnameLength = hive.ReadInt16();

  buf = hive.ReadBytes(this.NameLength);
  this.Name = System.Text.Encoding.UTF8.GetString(buf);

  hive.BaseStream.Position = this.ClassnameOffset + 4 + 4096;
  this.ClassnameData = hive.ReadBytes(this.ClassnameLength);
}

Listing 14-6: The ReadNodeStructure() method of the NodeKey class

To begin the ReadNodeStructure() method, we read the next four bytes of the node key with ReadBytes() to check that we are at the beginning of a node key (note that the second two bytes are junk that we can ignore for our purposes; we only care about the first two bytes). We compare the first two of these bytes to 0x6e and 0x6b, respectively. We are looking for the two hexadecimal byte values that represent the ASCII characters n and k (for node key). Every node key in the registry hive starts with these two bytes, so we can always be sure that we are parsing what we expect. After determining we are reading a node key, we save our current position in the file stream so that we can easily return to it.

Next, we begin assigning values to some of the NodeKey properties, starting with the IsRootKey and Timestamp properties. Notice that every few lines, we skip ahead by four in the current stream position without reading anything. We’re skipping pieces of metadata that aren’t necessary for our purposes.

Then, we use the ReadInt32() method to read four bytes and return an integer representing them that C# can read. This is what makes the BinaryReader class so useful. It has many convenient methods that will cast bytes for you. As you can see, most of the time, we will use the ReadInt32() method, but occasionally we will use ReadInt16() or other methods to read specific types of integers, such as unsigned and really long integers.

Finally, we read the name of the NodeKey and assign the string to the Name property. We also read the class name data , which we will use later when dumping the boot key.

Now we need to implement the ReadChildrenNodes() method. This method iterates over each child node and adds the node to the ChildNodes property so that we can analyze it later, as Listing 14-7 shows.

private void ReadChildrenNodes(BinaryReader hive)
{
  this.ChildNodes = new List<NodeKey>();
  if (this.LFRecordOffset != -1)
  {
    hive.BaseStream.Position = 4096 + this.LFRecordOffset + 4;
    byte[] buf = hive.ReadBytes(2);

    //ri
    if (buf[0] == 0x72 && buf[1] == 0x69)
    {
      int count = hive.ReadInt16();
     for (int i = 0; i < count; i++)
      {
        long pos = hive.BaseStream.Position;
        int offset = hive.ReadInt32();

        hive.BaseStream.Position = 4096 + offset + 4;
         buf = hive.ReadBytes(2);

         if (!(buf[0] == 0x6c && (buf[1] == 0x66 || buf[1] == 0x68)))
           throw new Exception("Bad LF/LH record at:"
                 + hive.BaseStream.Position);

        ParseChildNodes(hive);

        hive.BaseStream.Position = pos + 4; //go to next record list
        }
      }
      //lf or lh
      else if (buf[0] == 0x6c && (buf[1] == 0x66 || buf[1] == 0x68))
      ParseChildNodes(hive);
      else
        throw new Exception("Bad LF/LH/RI record at: "
              + hive.BaseStream.Position);
  }
}

Listing 14-7: The ReadChildrenNodes() method of the NodeKey class

Like most of the methods we will be implementing for the NodeKey class, the ReadChildrenNodes() method takes a single argument, which is the BinaryReader for the registry hive. We create an empty list of node keys for the ChildNodes property to read to. Then we must parse any child nodes in the current node key. This gets a bit tricky because there are three different ways to point to child node keys, and one type is read differently than the other two. The three types are the ri (for index root), lf (for fast leaf), and lh (for hash leaf) structures.

We check whether we are on an ri structure first. The ri structure is a container and is stored slightly differently. It is used for pointing to multiple lf or lh records and allows a node key to have more child nodes than a single lf or lh record can handle. As we loop over each set of child nodes in a for loop , we jump to each child record and call ParseChildNodes() , which we will implement next, by passing the BinaryReader for the hive as the only argument. After parsing the child nodes, we can see that our stream position has changed (we’ve moved around in the registry hive), so we set the stream position back to the ri list , where we were before reading the children, in order to read the next record in the list.

If we are dealing with an lf or lh record , we just pass the BinaryReader to the ParseChildNodes() method and let it read the nodes directly.

Luckily, once the child nodes have been read, they can all be parsed in the same way, regardless of the structure used to point to them. The method to do all of the actual parsing is relatively easy, as shown in Listing 14-8.

private void ParseChildNodes(BinaryReader hive)
{
  int count = hive.ReadInt16();
  long topOfList = hive.BaseStream.Position;

 for (int i = 0; i < count; i++)
  {
    hive.BaseStream.Position = topOfList + (i*8);
    int newoffset = hive.ReadInt32();
    hive.BaseStream.Position += 4; //skip over registry metadata
    hive.BaseStream.Position = 4096 + newoffset + 4;
    NodeKey nk = new NodeKey(hive) { ParentNodeKey = this };
    this.ChildNodes.Add(nk);
  }
  hive.BaseStream.Position = topOfList + (count * 8);
}

Listing 14-8: The ParseChildNodes() method for the NodeKey class

ParseChildNodes() takes a single argument, the BinaryReader for the hive. The number of nodes we need to iterate over and parse is stored in a 16-bit integer, which we read from the hive . After storing our position so we can return to it later, we begin iterating in a for loop , jumping to each new node and passing the BinaryReader to the NodeKey class constructor . Once the child NodeKey is created, we add the node to the ChildNodes list and begin the process again, until no more nodes are available to be read.

The last method, called in the NodeKey constructor, is the ReadChildValues() method. This method call, detailed in Listing 14-9, populates the ChildValues property list with all the key/value pairs we have found in the node key.

private void ReadChildValues(BinaryReader hive)
 {
   this.ChildValues = new List<ValueKey>();
   if (this.ValueListOffset != -1)
   {
   hive.BaseStream.Position = 4096 + this.ValueListOffset + 4;
    for (int i = 0; i < this.ValuesCount; i++)
    {
      hive.BaseStream.Position = 4096 + this.ValueListOffset + 4 + (i*4);
      int offset = hive.ReadInt32();
      hive.BaseStream.Position = 4096 + offset + 4;
      this.ChildValues.Add(new ValueKey(hive));
    }
  }
}

Listing 14-9: The ReadChildValues() method for the NodeKey class

Within the ReadChildValues() method, we first instantiate a new list to store the ValueKeys in and assign it to the ChildValues property. If the ValueListOffset doesn’t equal -1 (which is a magic value that means there are no child values), we jump to the ValueKey list and begin reading each value key in a for loop, adding each new key to the ChildValues property so we can access it later.

With this step, the NodeKey class is complete. The last class to implement is the ValueKey class.

Making a Class to Store Value Keys

The ValueKey class is much simpler and shorter than the NodeKey class. Most of the ValueKey class is just the constructor, as Listing 14-10 shows, though there are a handful of properties as well. This is all that is left to implement before we can start reading the offline registry hive.

public class ValueKey
{
  public ValueKey(BinaryReader hive)
  {
    byte[] buf = hive.ReadBytes(2);

    if (buf[0] != 0x76 || buf[1] != 0x6b) //vk
      throw new NotSupportedException("Bad vk header");

    this.NameLength = hive.ReadInt16();
    this.DataLength = hive.ReadInt32();

    byte[] databuf = hive.ReadBytes(4);

    this.ValueType = hive.ReadInt32();
    hive.BaseStream.Position += 4; //skip metadata

    buf = hive.ReadBytes(this.NameLength);
    this.Name = (this.NameLength == 0) ? "Default" :
                     System.Text.Encoding.UTF8.GetString(buf);

    if (this.DataLength < 5)
    this.Data = databuf;
    else
    {
      hive.BaseStream.Position = 4096 + BitConverter.ToInt32(databuf, 0) + 4;
      this.Data = hive.ReadBytes(this.DataLength);
    }
  }

  public short NameLength { get; set; }
  public int DataLength { get; set; }
  public int DataOffset { get; set; }
  public int ValueType { get; set; }
  public string Name { get; set; }
  public byte[] Data { get; set; }
  public string String { get; set; }
}

Listing 14-10: The ValueKey class

In the constructor , we read the first two bytes and make sure that we are reading a value key by comparing the two bytes to 0x76 and 0x6b, as we did earlier. In this case, we are looking for vk in ASCII. We also read the lengths of the name and data and assign those values to their respective properties.

Something to note is that the databuf variable can hold either a pointer to the value key data or the value key data itself. If the data length is five or more, the data is generally in a four-byte pointer. We use the DataLength property to check whether the ValueKey length is less than five. If so, we assign the data in the databuf variable directly to the Data property and finish up. Otherwise, we turn the databuf variable into a 32-bit integer , which is an offset from the current position in the file stream to the actual data to read, and then jump to that position in the stream and read the data with ReadBytes(), assigning it to the Data property.

Testing the Library

Once we’ve finished writing the classes, we can write a quick Main() method, shown in Listing 14-11, to test that we are successfully parsing the registry hive.

public static void Main(string[] args)
{
  RegistryHive hive = new RegistryHive(args[0]);
  Console.WriteLine("The rootkey's name is " + hive.RootKey.Name);
}

Listing 14-11: The Main() method to print the root key name of a registry hive

In the Main() method, we instantiate a new RegistryHive class by passing the first argument of the program as the file path to the offline registry hive on the filesystem. Then, we print the name of the registry hive root NodeKey, which is stored in the RegistryHive class RootKey property:

$ ./ch14_reading_offline_hives.exe /Users/bperry/system.hive
The rootkey's name is CMI-CreateHive{2A7FB991-7BBE-4F9D-B91E-7CB51D4737F5}
$

Once we have confirmed that we are successfully parsing the hive, we are ready to search the registry for the information we’re interested in.

Dumping the Boot Key

Usernames are nice, but password hashes are probably a lot more useful. Therefore, we’ll look at how to find these now. In order to access the password hashes in the registry, we must first retrieve the boot key from the SYSTEM hive. The password hashes in the Windows registry are encrypted with the boot key, which is unique to most Windows machines (unless they are images or virtual machine clones). Adding four more methods to the class with our Main() method will allow us to dump the boot key from a SYSTEM registry hive.

The GetBootKey() Method

The first method is the GetBootKey() method, which takes a registry hive and returns an array of bytes. The boot key is broken up across multiple node keys in the registry hive, which we must first read and then decode using a special algorithm that will give us the final boot key. The beginning of this method is shown in Listing 14-12.

static byte[] GetBootKey(RegistryHive hive)
{
  ValueKey controlSet = GetValueKey(hive, "Select\Default");
  int cs = BitConverter.ToInt32(controlSet.Data, 0);

  StringBuilder scrambledKey = new StringBuilder();
  foreach (string key in new string[] {"JD", "Skew1", "GBG", "Data"})
  {
    NodeKey nk = GetNodeKey(hive, "ControlSet00" + cs +
                 "\Control\Lsa\" + key);

    for (int i = 0; i < nk.ClassnameLength && i < 8; i++)
      scrambledKey.Append((char)nk.ClassnameData [i*2]);
  }

Listing 14-12: Beginning of the GetBootKey() method to read the scrambled boot key

The GetBootKey() method starts by grabbing the SelectDefault value key with the GetValueKey() method (which we’ll implement shortly). It holds the current control set being used by the registry. We need this so that we read the correct boot key registry values from the correct control set. Control sets are sets of operating system configurations kept in the registry. Copies are kept for backup purposes in case the registry is corrupted, so we want to pick the control set that is selected by default at boot, which is dictated by the SelectDefault registry value key.

Once we’ve found the correct default control set, we iterate over the four value keys—JD, Skew1, GBG, and Data—that contain the encoded boot key data . As we iterate, we find each key with GetNodeKey() (which we’ll also implement shortly), iterate over the boot key data byte by byte, and append it to the total scrambled boot key.

Once we have the scrambled boot key, we need to descramble it, and we can use a straightforward algorithm. Listing 14-13 shows how we can turn our scrambled boot key into the key used to decrypt the password hashes.

  byte[] skey = StringToByteArray(scrambledKey.ToString());
  byte[] descramble = new byte[] { 0x8, 0x5, 0x4, 0x2, 0xb, 0x9, 0xd, 0x3,
                                   0x0, 0x6, 0x1, 0xc, 0xe, 0xa, 0xf, 0x7 };

  byte[] bootkey = new byte[16];
 for (int i = 0; i < bootkey.Length; i++)
    bootkey[i] = skey[descramble[i]];

  return bootkey;
}

Listing 14-13: Finishing the GetBootKey() method to descramble the boot key

After converting the scrambled key into a byte array for further processing with StringToByteArray() , which we’ll implement soon, we create a new byte array to descramble our current value. We then create another new byte array to store the final product and begin iterating over the scrambled key in a for loop , using the descramble byte array to find the correct values for the final bootkey byte array. The final key is then returned to the caller .

The GetValueKey() Method

The GetValueKey() method, shown in Listing 14-14, simply returns a value for a given path in the hive.

static ValueKey GetValueKey(RegistryHive hive, string path)
{
  string keyname = path.Split('').Last();
  NodeKey node = GetNodeKey(hive, path);
  return node.ChildValues.SingleOrDefault(v => v.Name == keyname);
}

Listing 14-14: The GetValueKey() method

This simple method accepts a registry hive and the registry path to find in the hive. Using the backslash character to separate the nodes in the registry path, we split the path and take the last segment of the path as the value key to find. We then pass the registry hive and registry path to GetNodeKey() (implemented next), which will return the node that contains the key. Finally, we use the LINQ method SingleOrDefault() to return the value key from the node’s child values.

The GetNodeKey() Method

The GetNodeKey() method is a bit more complicated than the GetValueKey() method. Shown in Listing 14-15, the GetNodeKey() method iterates through a hive until it finds a given node key path and returns the node key.

static NodeKey GetNodeKey(RegistryHive hive, string path)
{
  NodeKey node = null;
  string[] paths = path.Split('');
  foreach (string ch in paths)
  {

    if (node == null)
      node = hive.RootKey;

   foreach (NodeKey child in node.ChildNodes)
    {
      if (child.Name == ch)
      {
        node = child;
        break;
      }
    }
    throw new Exception("No child found with name: " + ch);
  }

 return node;
}

Listing 14-15: The GetNodeKey() method

The GetNodeKey() method accepts two arguments—the registry hive to search and the path of the node to return—separated by backslash characters. We start by declaring a null node for keeping track of our position while traversing the registry tree paths; then we split the path at each backslash character, returning an array of path segment strings. We then iterate over each path segment, traversing the registry tree until we find the node at the end of the path. We start traversing using a foreach loop that will progressively loop over each path segment in the paths array . As we iterate over each segment, we use a foreach loop inside the for loop to find the next segment in the path until we have found the last node. Finally, we return the node we found.

The StringToByteArray() Method

Finally, we implement the StringToByteArray() method used in Listing 14-13. This very simple method is detailed in Listing 14-16.

static byte[] StringToByteArray(string s)
{
  return Enumerable.Range(0, s.Length)
    .Where(x => x % 2 == 0)
    .Select(x => Convert.ToByte(s.Substring(x, 2), 16))
    .ToArray();
}

Listing 14-16: The StringToByteArray() method used by GetBootKey()

The StringToByteArray() method uses LINQ to convert each two-character string into a single byte. For example, if the string "FAAF" were passed in, a byte array of { 0xFA, 0xAF } would be returned by the method. Using Enumerable.Range() to iterate over each character in the string, we skip the odd-numbered characters with Where() and then use Select() to convert each pair of characters into the byte the pair represents.

Getting the Boot Key

We can finally try dumping the boot key from the system hive. By calling our new GetBootKey() method, we can rewrite the Main() method we used previously to print the root key name to print the boot key instead. Listing 14-17 shows this.

public static void Main(string[] args)
{
  RegistryHive systemHive = new RegistryHive(args[0]);
  byte[] bootKey = GetBootKey(systemHive);

Console.WriteLine("Boot key: " + BitConverter.ToString(bootKey));
}

Listing 14-17: The Main() method testing the GetBootKey() method

This Main() method will open the registry hive , which is passed as the only argument to the program. Then the new hive is passed to the GetBootKey() method . With the new boot key saved, we print the boot key with Console.WriteLine() .

Then, we can run the test code to print the boot key, shown in Listing 14-18.

$ ./ch14_reading_offline_hives.exe ~/system.hive
Boot key: F8-C7-0D-21-3E-9D-E8-98-01-45-63-01-E4-F1-B4-1E
$

Listing 14-18: Running the final Main() method

It worked! But how can we be sure this is the actual boot key?

Verifying the Boot Key

We can verify that our code is working correctly by comparing it to the result of bkhive, a popular tool used to dump the boot key of a system hive, just as we have done. Included in the repository of code for this book (linked from the book’s page at https://www.nostarch.com/grayhatcsharp/) is a copy of the source code for the bkhive tool. Compiling and running this tool on the same registry hive we have been testing on should verify our results, as Listing 14-19 shows.

$ cd bkhive-1.1.1
$ make
$ ./bkhive ~/system.hive /dev/null
bkhive 1.1.1 by Objectif Securite
http://www.objectif-securite.ch
original author: [email protected]

Root Key : CMI-CreateHive{2A7FB991-7BBE-4F9D-B91E-7CB51D4737F5}
Default ControlSet: 001
Bootkey: f8c70d213e9de89801456301e4f1b41e
$

Listing 14-19: Verifying that the boot key returned by our code is what bkhive prints

The bkhive tool verifies that our own boot key dumper works like a charm! Although bkhive prints the boot key in a slightly different form than we do (all lowercase with no hyphens), the data it prints is still the same (F8C70D21...) as ours.

You might wonder why go through all the effort with the C# classes to dump the boot key when we could just use bkhive. The bkhive tool is highly specialized and will read a specific part of the registry hive, but the classes we implemented can be used to read any part of the registry hive, such as the password hashes (which are encrypted with the boot key!) and patch-level information. Our classes are much more flexible than the bkhive tool, and you’ll be able to use them as starting points if you want to expand your application.

Conclusion

The obvious next step for an offensive or incident response–focused registry library is to dump the actual usernames and password hashes. Getting the boot key is the most difficult part of this, but it’s also the only step that requires the SYSTEM registry hive. Dumping the usernames and password hashes requires the SAM registry hive instead.

Reading registry hives (and other binary file formats in general) is an important C# skill to develop. Incident response and offensive security professionals often must be able to implement code that reads and parses binary data in a variety of formats, either over the wire or on disk. In this chapter, you first learned how to export the registry hives so that we could copy them to other machines and read them offline. We then implemented classes to read the registry hives using BinaryReader. With these classes built, we were able to read the offline hive and print the root key name. Then, we took it a step further and dumped the boot key, used to encrypt the password hashes stored in the Windows registry, from the system hive.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset