Setup Info

  • Volatility 3 2.0.2
Volatility 3 Framework 2.0.2
usage: volatility [-h] [-c CONFIG] [--parallelism [{processes,threads,off}]] [-e EXTEND] [-p PLUGIN_DIRS] [-s SYMBOL_DIRS] [-v] [-l LOG] [-o OUTPUT_DIR] [-q] [-r RENDERER] [-f FILE] [--write-config] [--clear-cache] [--cache-path CACHE_PATH] [--offline]
                  [--single-location SINGLE_LOCATION] [--stackers [STACKERS ...]] [--single-swap-locations [SINGLE_SWAP_LOCATIONS ...]]
                  plugin ...
  • Windows 11 Dump
  Variable                         Value
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Kernel Base                      0xf80745a00000
  DTB                              0x1ae000
  Symbols                          file:///C:/Users/daddy/development/volatility3/volatility3/symbols/windows/ntkrnlmp.pdb/F55005B80C686C739E763E6CF2559D9F-1.json.xz
  Is64Bit                          True
  IsPAE                            False
  layer_name                       0 WindowsIntel32e
  memory_layer                     1 WindowsCrashDump64Layer
  base_layer                       2 FileLayer
  KdDebuggerDataBlock              0xf80746602190
  NTBuildLab                       22000.1.amd64fre.co_release.2106
  CSDVersion                       0
  KdVersionBlock                   0xf80746609758
  Major/Minor                      15.22000
  MachineType                      34404
  KeNumberProcessors               4
  SystemTime                       2022-02-26 07:09:23
  NtSystemRoot                     C:\Windows
  NtProductType                    NtProductWinNt
  NtMajorVersion                   10
  NtMinorVersion                   0
  PE MajorOperatingSystemVersion   10
  PE MinorOperatingSystemVersion   0
  PE Machine                       34404
  PE TimeDateStamp                 Sat Sep 16 14:16:51 1972

Volatility

This year, I’ve had a pretty large interest in offensive forensics: the idea of using forensic concepts (particually host-based forensics) to further an attack path. A simple example of this would be looking through a targets Recycle Bin to look for files that have credentials. While looking for credentials has an offensive focus, the process of determining original file names and original locations for files in the Recycle Bin can be considered a forensic skill.

In particular, I’ve been looking at ways to perform memory forensics to determine how to find secrets in memory dumps of entire hosts or processes. At the core of memory forensics is a framework called Volatility, traditionally used by forensicators and often co-opted by attackers.

You inevitably will come across two versions of Volatility:

  • Volatility

    • Older and based on Python 2.7
    • Not as much support for modern operating systems
    • Lots of plugins for operating systems it does support
  • Volatility3

    • Newer and based on Python 3.6+
    • Much better support for modern operating systems
    • Not as much plugin support yet

Note: For the sake of clarity, throughout the rest of this series. I will be referring to the original Volatility as Volatility2.

While some of the community plugins that exist for the Volatility2 might seem interesting for offensive forensics (such as this mimikatz plugin), many of those plugins are not maintained and require updates to make them compatible with the new Volatility 3 framework. Additionally, since operating systems (especially Windows) has changed over time, it’s likely that some of these plugins might need a rework to deal with new or changed structured objects in memory.


How dumpcerts Works

While looking for ideas in which to use Volatility3 during red team engagements, I came across the dumpcerts plugin that is built into Volatility2. This plugin was written based on work by Tobias Klein in which he wrote about finding and extracting RSA certificates and keys in large amounts of data.

Let’s see how this works.

ASN.1 Structures

Both PKCS #8 and x509 are storage standards for holding the crytographic data for RSA private keys and certificates, respectively. The standards and ASN.1 structures for each of these object types are backed by RFC 5208 for PKCS #8 and RFC 5280 for x509 v3 certificates. By analyzing the data structures, we can determine what the object looks like in memory. As noted in the article by Tobias, we don’t need to dig particularly deep in the structures to attempt to identify them.

PKCS #8

PrivateKeyInfo ::= SEQUENCE {
    version                   Version,
    privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
    privateKey                PrivateKey,
    attributes           [0]  IMPLICIT Attributes OPTIONAL }

Version ::= INTEGER
PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
PrivateKey ::= OCTET STRING
Attributes ::= SET OF Attribute

Starting with the PrivateKeyInfo (which is also represented by the CRYPT_PRIVATE_KEY_INFO struct found in the Windows API), we can determine what the bytes of the object are in memory. Firstly, a SEQUENCE is represented by 30 82 ?? ??, where 30 82 is the tag and the unknown bytes represent the length of the SEQUENCE. In this case, since we are starting with the SEQUENCE, the length represented by the bytes that come directly after the tage is the length of the entire PKCS #8 object.

Next comes the Version INTEGER. An INTEGER is defined by the tag 02 followed by the length of the integer in bytes, followed by value. Since the RFC states this value should be 0, the full structure for this Version is 02 01 00.

The next field is a privateKeyAlgorithm whose possibilities are defined in RFC 2898 as part of the PKCS-5 standard with several possible values depending on the type of encryption used. It’s possible to be extremely specific and add these possible values for this field to reduce the number of hits found by the underlying Yara engines in Volatility2/3 but it’s overkill.

Therefore, we can identify potential RSA Private keys in the PKCS #8 format with 30 82 ?? ?? 02 01 00 as a set of bytes to look for. This is how the dumpcerts plugin finds them!

x509

Certificate  ::=  SEQUENCE  {
    tbsCertificate       TBSCertificate,
    signatureAlgorithm   AlgorithmIdentifier,
    signatureValue       BIT STRING  }

TBSCertificate  ::=  SEQUENCE  {
    version         [0]  EXPLICIT Version DEFAULT v1,
    serialNumber         CertificateSerialNumber,
    signature            AlgorithmIdentifier,
    issuer               Name,
    validity             Validity,
    subject              Name,
    subjectPublicKeyInfo SubjectPublicKeyInfo,
    issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                            -- If present, version MUST be v2 or v3
    subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                            -- If present, version MUST be v2 or v3
    extensions      [3]  EXPLICIT Extensions OPTIONAL
                            -- If present, version MUST be v3
    }
...snipped...

Version  ::=  INTEGER  {  v1(0), v2(1), v3(2)  }

Similarly, the x509 structure starts with a SEQUENCE. However, the next item is a TBSCertificate and that is also a SEQUENCE. Therefore, we will see two SEQUENCE tags in a row 30 82 ?? ?? 30 82 ?? ??. According to [Section 4.1.2.1] of RFC 5280, the Version is an INTEGER that can be either v1, v2, v3. This set of bytes is not accounted for in the original plugin, and during attempts to add it in the updated version, I noticed a huge reduction in the number of certificates being found. Therefore, we use 30 82 ?? ?? 30 82 ?? ?? as the indicator for an x509 certificate.


Updating to Volatility3

The convenience of Volatility is that you scan specific process address spaces and scan only those as opposed to scanning the entire dump of memory. This makes it possible to correlate and provide context to any results found, as certs will be referred to in conjunction with their processes. Specifically, the fields that are output in the original dumpcerts include the process ID (PID), process name, the offset where the object was found, the rule that matched, and other data for the user to look through. The new plugin only outputs the offset found, PID, process name, the rule that matched and the subject name or key size, depending on the object found.

Creating the class

A Volatility3 plugin must be a class that subclasses PluginInterface. We also have to define the minimal Volatility3 framework version as well as the version of the plugin itself.

class Dumpcerts(interfaces.plugins.PluginInterface):
    """Dump public and private RSA keys based on ASN-1 structure"""

    _required_framework_version = (2, 0, 0)
    _version = (1, 0, 0)

Defining the requirements

Writing a plugin for Volatility3 requires a classmethod called get_requirements where the author must defined what is required for the plugin to run as well as defines the command line options for the user.

@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
    return [
        # We need to access the kernel module layer that we will scan
        requirements.ModuleRequirement(
            name="kernel",
            description="Kernel layer",
            architectures=["Intel32", "Intel64"],
        ),

        # We need the vadyarascanner, which allows us to perform yara scans on specific Virtual Address Descriptors (VADs)
        requirements.PluginRequirement(
            name="vadyarascanner", plugin=vadyarascan.VadYaraScan, version=(1, 0, 0)
        ),

        # We can specify PIDs to scan. If not provided, the plugin will scan all PIDs
        requirements.ListRequirement(
            name="pid",
            element_type=int,
            description="Process IDs to include (all other processes are excluded)",
            optional=True,
        ),

        # Allows the user to determine if they want to scan only for public certs or private keys
        requirements.ChoiceRequirement(
            ["all", "private", "public"],
            name="type",
            default="all",
            description="Types of keys to dump",
            optional=True,
        ),

        # Gives the user the option to dump any objects found to disk.
        requirements.BooleanRequirement(
            name="dump", description="Dump keys", default=False, optional=True
        ),

        # Allows the user to scan all physical memory instead of just process memory
        requirements.BooleanRequirement(
            name="physical",
            description="Scan physical memory instead of processes",
            default=False,
            optional=True,
        ),
    ]

Defining the run method

The run method in a plugin needs to be defined and return a TreeGrid object. This TreeGrid defines the columns and value representation of the output that results from the self._generator function.

def run(self) -> renderers.TreeGrid:
    physical = self.config.get("physical")
    if physical:
        return renderers.TreeGrid(
            [
                (f'{"Offset":<8}', format_hints.Hex),
                ("Rule", str),
                ("Value", str),
            ],
            self._generator(physical),
        )
    else:
        return renderers.TreeGrid(
            [
                (f'{"Offset":<8}', format_hints.Hex),
                ("PID", int),
                (f'{"Process":<8}', str),
                ("Rule", str),
                ("Value", str),
            ],
            self._generator(physical),
        )

In this plugin’s case, there are two possible table formats: one for process scanning and another for physical memory scanning. Since we can’t correlate the offset address of matches to processes during physical scanning without a bunch of overhead, we only print out the offset, the rule that matched, and the subject name or key size depending on the object. The format_hints module in Volatility3 provides some helper types to change how the value in that column is displayed. The string formatting seen for Offset and Process are attempts to line up the column headers with the table as much as possible in Volatility3’s default output.

Defining the Yara rules

At its core, the dumpcerts plugin uses the internal Yara scanners and processes the matches to determine if a certificate or key has been found based on the given rules.

Volatility3 values having reusable code that can be accessed from external plugins. This gives a collaborative feel to the plugins as opposed to each plugin attempting to implement code on its own. I can’t imagine a reason an external plugin would need these yara rules, but no hard is done by exposing this functionality as a classmethod.

@classmethod
def get_yara_rules(cls, key_type: str):
    sources = {}
    if key_type in ["all", "public"]:
        sources["x509"] = "rule x509 {strings: $a = {30 82 ?? ?? 30 82 ?? ??} condition: $a}"

    if key_type in ["all", "private"]:
        sources["pkcs"] = "rule pkcs {strings: $a = {30 82 ?? ?? 02 01 00} condition: $a}"

    return yara.compile(sources=sources)

Defining the get_*_certificates functions

When I originally re-wrote this plugin, the code to start the scanning was located in the _generator function, as I thought there would not be a reason for another plugin to use this code. However, circumstances changed during one of our red team ops and I needed to expose this functionality to another plugin (which will be discussed in Part 2 of this series!). Therefore, two functions were created: get_process_certificates and get_physical_certificates. The only major difference between the two is the type of yara scanner that gets created and the arguments passed it.

For process memory, we use Vadyarascanner since it has the ability to isolate the sections scanned by process by locating the memory ranges that belong to that process. Additionally, the choice to create another classmethod for get_certs_by_process was a result of a need to expose the functionality to get the certs by process to a later plugin.

for proc in pslist.PsList.list_processes(
    context=context,
    layer_name=layer_name,
    symbol_table=symbol_table,
    filter_func=filter_func,
):
    for offset, proc, rule_name, cert_or_pem in cls.get_certs_by_process(
        context, proc, key_types
    ):
        yield (offset, proc, rule_name, cert_or_pem)

For physical memory, we use the normal yarascanner. You may notice there is no additional call to another classmethod here. This is because this function is sufficient enough to be used by both the plugin itself and external plugins. The get_certs_by_process function is more comparable to this function.

@classmethod
def get_physical_certificates(
    cls,
    context: interfaces.context.ContextInterface,
    layer_name: str,
    key_type: str,
) -> Iterable[Tuple[int, str, Union[Certificate, PRIVATE_KEY_TYPES],]]:

    layer = context.layers[layer_name]
    for offset, rule_name, _, value in layer.scan(
        context=context,
        scanner=yarascan.YaraScanner(rules=cls.get_yara_rules(key_type)),
    ):
        cert_or_pem = cls.get_cert_or_pem(layer, rule_name, offset, value)
        if cert_or_pem:
            yield (offset, rule_name, cert_or_pem)

Parsing the RSA object

In the original version of dumpcerts, there was an --ssl switch that could allow the user to verify the certificate by invoking the openssl command line tool. Python has come a long since this original version was created and we can use the cryptography module to do this for us without relying on external tools.

@classmethod
def get_cert_or_pem(
    self,
    layer: interfaces.layers.DataLayerInterface,
    rule_name: str,
    offset: int,
    value: bytes,
) -> Union[Certificate, PRIVATE_KEY_TYPES]:
    
    try:
        # This is 30 82 ?? ??, where ?? ?? represents the cert size
        _, cert_size = unpack(">HH", value[0:4])
        data = layer.read(offset, cert_size + 4)

        # If x509 triggered, try to create a DER x509 certificate to validate
        if rule_name == "x509":
            rsa_object = load_der_x509_certificate(data, default_backend())

        # If pkcs triggered, try to create a PEM private key to validate
        elif rule_name == "pkcs":
            pem = (
                b"-----BEGIN RSA PRIVATE KEY-----\n"
                + b64encode(data)
                + b"\n-----END RSA PRIVATE KEY-----"
            )
            rsa_object = load_pem_private_key(pem, None, default_backend())

        return rsa_object
    except:
        return None

First we need to grab the size of the certificate, which is represented in the ASN.1 structure 30 82 ?? ?? we’ve seen before. If we can successfully create a x509 Certificate object or pem-encoded private key, then we must have a valid object in memory! If there is an exception, then we can just move on. After finding an object, we display the data to the user and save it as a cer or .key file (if --dump has been passed).

Results

Process Scanning

Offset          PID   Process     Rule   Value
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  0x21c00a31000   824   lsass.exe   x509   968407DF0B1CF65814DFD7333557519B154D8CE7_CN=*.github.com,O=GitHub\, Inc.,L=San Francisco,ST=California,C=US
  0x21c00a81044   824   lsass.exe   x509   FD949FBE4FF3CF89F8FB5209A31439E346C46B76_CN=config.teams.microsoft.com,O=Microsoft Corporation,L=Redmond,ST=WA,C=US
  0x21c00a818c4   824   lsass.exe   x509   6C3AF02E7F269AA73AFD0EFF2A88A4A1F04ED1E5_CN=Microsoft Azure TLS Issuing CA 05,O=Microsoft Corporation,C=US
  0x21c00a81ed0   824   lsass.exe   x509   C2E068F2B81258F24368BA745A7876F9192DA160_CN=storage.live.com,OU=Microsoft Corporation,O=Microsoft Corporation,L=Redmond,ST=WA,C=US
  0x21c00a83370   824   lsass.exe   x509   C2E068F2B81258F24368BA745A7876F9192DA160_CN=storage.live.com,OU=Microsoft Corporation,O=Microsoft Corporation,L=Redmond,ST=WA,C=US
  0x21c00a84874   824   lsass.exe   x509   C2E068F2B81258F24368BA745A7876F9192DA160_CN=storage.live.com,OU=Microsoft Corporation,O=Microsoft Corporation,L=Redmond,ST=WA,C=US
  0x21c00a85d03   824   lsass.exe   x509   B0C2D2D13CDD56CDAA6AB6E2C04440BE4A429C75_CN=Microsoft RSA TLS CA 02,O=Microsoft Corporation,C=US
  0x21c00a8731a   824   lsass.exe   x509   626D44E704D1CEABE3BF0D53397464AC8080142C_CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US
  0x21c00a87864   824   lsass.exe   x509   CF62F0FEEC1F40AD7E1DBBFDF0D097866C9CE222_CN=in.applicationinsights.azure.com
  0x21c00a884bf   824   lsass.exe   x509   B0C2D2D13CDD56CDAA6AB6E2C04440BE4A429C75_CN=Microsoft RSA TLS CA 02,O=Microsoft Corporation,C=US
...

Physical Scanning

 Offset         Rule    Value
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0x94072ba91095  x509    312860D2047EB81F8F58C29FF19ECDB4C634CF6A_CN=Microsoft Windows,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072ba9159f  x509    580A6F4CC4E4B669B9EBDC1B2B3E087B80D0678D_CN=Microsoft Windows Production PCA 2011,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072ced7095  x509    EA7D7CCB8A4CC08F0D9B0F3A9BAE39F617EB82B3_CN=Microsoft Corporation,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072ced7593  x509    8BFE3107712B3C886B1C96AAEC89984914DC9B6B_CN=Microsoft Code Signing PCA 2010,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072ced8019  x509    745DAE981FF2CB0C452C54C2F2B27D16FF5CF6B7_CN=Microsoft Time-Stamp Service,OU=Thales TSS ESN:7BF1-E3EA-B808,OU=Microsoft America Operations,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072ced8729  x509    36056A5662DCADECF82CC14C8B80EC5E0BCC59A6_CN=Microsoft Time-Stamp PCA 2010,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072cfe5095  x509    FE51E838A087BB561BBB2DD9BA20143384A03B3F_CN=Microsoft Windows,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072cfe559f  x509    580A6F4CC4E4B669B9EBDC1B2B3E087B80D0678D_CN=Microsoft Windows Production PCA 2011,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072cfe5ff2  x509    3D622BEA4F4E11EA8296B9C6C3E6898AEE595B8C_CN=Microsoft Time-Stamp Service,OU=Thales TSS ESN:FC41-4BD4-D220,OU=Microsoft Ireland Operations Limited,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072cfe670a  x509    36056A5662DCADECF82CC14C8B80EC5E0BCC59A6_CN=Microsoft Time-Stamp PCA 2010,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072ddcc095  x509    FE51E838A087BB561BBB2DD9BA20143384A03B3F_CN=Microsoft Windows,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
0x94072ddcc59f  x509    580A6F4CC4E4B669B9EBDC1B2B3E087B80D0678D_CN=Microsoft Windows Production PCA 2011,O=Microsoft Corporation,L=Redmond,ST=Washington,C=US
...

Conclusion

The new dumpcerts plugin is currently available on my Github. Additionally, since this plugin is just a complex yara scan, this should be compatible on all operating systems that Volatility3 supports.

While this method was extremely effective in extracting public certificates from processes that may not have been in the registry, there were not a lot of results for private keys at all. In fact, in my Windows 11 test dump, the only PKCS matches happened to be from the vmmem processes that my machine has as a result of being a VMWare Virtual Machine. 🤷🏿‍♂️ As an attacker, I am significantly more interested in the private keys than the public ones. In Part 2, I’ll discuss writing another Volatility3 plugin to follow up on this one to extract private keys.

Special thanks to the Volatility team for putting together such a dope framework as well as to Tobias Klein for writing the original dumpcerts plugin almost 10 years ago and providing a starting point to understanding the structures.