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:
-
- Older and based on Python 2.7
- Not as much support for modern operating systems
- Lots of plugins for operating systems it does support
-
- 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.