esDynamic
Manage your attack workflows in a powerful and collaborative platform.
Expertise Modules
Executable catalog of attacks and techniques.
Infrastructure
Integrate your lab equipment and remotely manage your bench.
Lab equipments
Upgrade your lab with the latest hardware technologies.
Side Channel Attacks
Evaluate cryptography algorithms from data acquitition to result visualisation.
Fault Injection Attacks
Laser, Electromagnetic or Glitch to exploit a physical disruption.
Security Failure Analysis
Explore photoemission and thermal laser stimulation techniques.
Evaluation Lab
Our team is ready to provide expert analysis of your hardware.
Starter Kits
Build know-how via built-in use cases developed on modern chips.
Cybersecurity Training
Grow expertise with hands-on training modules guided by a coach.
esReverse
Static, dynamic and stress testing in a powerful and collaborative platform.
Extension: Intel x86, x64
Dynamic analyses for x86/x64 binaries with dedicated emulation frameworks.
Extension: ARM 32, 64
Dynamic analyses for ARM binaries with dedicated emulation frameworks.
Penetration Testing
Identify and exploit system vulnerabilities in a single platform.
Vulnerability Research
Uncover and address security gaps faster and more efficiently.
Malevolent Code Analysis
Effectively detect and neutralise harmful software.
Digital Forensics
Collaboratively analyse data to ensure thorough investigation.
Software Assessment
Our team is ready to provide expert analysis of your binary code.
Cybersecurity training
Grow expertise with hands-on training modules guided by a coach.
Semiconductor
Security Labs
Governmental agencies
Academics
Why eShard?
Our team
Careers
Youtube
Gitlab
Github
A dive into REVEN® Analysis API.
In REVEN 2.11 we introduced multiple new changes to the Python API, mainly:
Using the new preview API, we are able to retrieve the path of a file corresponding to a handle, e.g when a process is reading a file with the handle 0xa4
:
>> # Connection to a server is already established and a `ctx` variable contains a Context object >> from reven2.preview.windows import Context as WindowsContext >> ctx = WindowsContext(ctx) >> print(ctx.handle(0xa4).object().filename_with_device) \Device\HarddiskVolume2\reven\foo.exe
As you can see, the retrieved path is prefixed by the device name (\Device\HarddiskVolume2\
) rather than the drive letter that is typical of Windows paths (e.g. C:\reven\foo.exe
).
The goal of this article is to demonstrate how you can advantageously use the newly introduced API features to retrieve the path containing the drive letter!
The final script we'll be working toward in the article is downloadable here.
The starting point will be a C++ example provided by Microsoft in its documentation, where we can see the implementation of a function taking a handle as a parameter and printing the full path with the drive letter, using multiple syscalls to the kernel.
Here is what the C++ code is doing:
FileObject.filename_with_device
so we can skip this part)GetLogicalDriveStrings
QueryDosDevice
to retrieve the device associated with the driveWe will implement each of these steps using the Python API, and just by looking into the kernel memory as we no longer have the ability to make syscalls at the point where we're analyzing a REVEN trace.
The first step is to parse the arguments for our script, connect to a REVEN server, retrieve a context where a file handle is valid, retrieve the file handle and the path with device in it. For that we will start by adding a main and arguments parsing.
import argparse from typing import Iterator, Optional, Tuple import reven2 from reven2.preview.windows import Context as WindowsContext, FileObject, Object from reven2.preview.windows.utils import read_unicode_string def main() -> None: # Parse the arguments given to our script parser = argparse.ArgumentParser() parser.add_argument( "--host", type=str, default="localhost", help='Reven host, as a string (default: "localhost")', ) parser.add_argument( "-p", "--port", type=int, default=13370, help="Reven port, as an int (default: 13370)", ) parser.add_argument( "--transition", type=int, required=True, help="The transition id, as an int, used to retrieve the file handle", ) parser.add_argument( "--handle", type=int, required=True, help="The file handle, as an int" ) args = parser.parse_args() # TODO: # - Connect to the REVEN server # - Retrieve the context from the arguments # - Retrieve the path of the file object from the arguments raise NotImplementedError("TODO") if __name__ == "__main__": main()
With the script above we have a main function capable of parsing some arguments:
Inside the main
function we can then add the connection to the REVEN server, it's quite simple:
def main() -> None: # Previous code... # Connect to the REVEN server server = reven2.RevenServer(args.host, args.port)
Now, to retrieve the handle we need to wrap a REVEN context in an enhanced windows.Context
from the new preview API that exposes Windows 10 information. For the base REVEN context, we will use the context before the transition that was passed as argument to the script: the handle should be valid at this context given it is valid during the passed transition.
def main() -> None: # Previous code... # Retrieve the context from the argument # Use the new API to get an enhanced context containing Windows 10 specific information ctx = WindowsContext(server.trace.context_before(args.transition))
The next step is to retrieve the file object and its path. To do so the WindowsContext
class is exposing a handle
method, which takes the handle value as a parameter and returns a Handle
object. This Handle
object can be used to retrieve the Object
associated with it, and we want to make sure it is a FileObject
:
def main() -> None: # Previous code... # Retrieve the handle from the argument at the given context file_object_handle = ctx.handle(args.handle) if file_object_handle is None: print(f"Couldn't find the handle 0x{args.handle:x}") return # Retrieve the object associated with the handle and check if it is an object of type 'File' file_object = file_object_handle.object() if not isinstance(file_object, FileObject): print(f"Object is of type '{file_object.type_name}' instead of 'File'") return print( f"File object found with path: {file_object.filename_with_device}, trying to find the path with the drive letter..." )
To better prepare for the next step, we will add a function get_path_with_drive_letter
that will be completed in the next steps so that we don't have to change the main
later.
def main() -> None: # Previous code... # Try to retrieve the path with the drive letter instead of the device name path_with_drive_letter = get_path_with_drive_letter(ctx, file_object) if path_with_drive_letter is None: print("Couldn't find the path with the drive letter") else: print(f"Path with the drive letter: {path_with_drive_letter}") def get_path_with_drive_letter( ctx: WindowsContext, file_object: FileObject ) -> Optional[str]: # Sort of implement the algorithm from the C++ code # - Retrieve the drive letters # - For each drive find the device associated with it # - If the device match the one in the path, we should use this drive letter in the path # TODO
The first step to implement from the C++ code in our get_path_with_drive_letter
function is to retrieve the drive letters. For that, we will build a new function named get_drive_letters
taking as an argument the context and returning an iterator of str
(each element being a drive letter).
To do that we need to understand how GetLogicalDriveStrings
is working. Here is some pseudo-Python code to describe its implementation:
def GetLogicalDriveStrings(): drive_map = retrieve_drive_map() for i in range(26): if drive_map & (1 << i): # The drive corresponding to the bit i is present # - Bit 0 present: A drive is present # - Bit 1 present: B drive is present # - ... pass # ...
As you can see, Windows is using what it is calling a "drive map", which is a 32-bit integer corresponding to a kind of bitfield where each bit corresponds to a drive. If the bit is set, the drive exists, if not the drive doesn't exist.
The question is how to retrieve the drive map from the memory. I will not go into the details, but if you reverse the code you can find a path that is easily accessible from the global variable PspHostSiloGlobals
of type _ESERVERSILO_GLOBALS
, PspHostSiloGlobals.ObSiloState.SystemDeviceMap->DriveMap
.
So, let's start our function get_path_with_drive_letter
by reading the global variable PspHostSiloGlobals
. For that, we will need to build its address from the RVA of the data symbol PspHostSiloGlobals
and the base address of the kernel in memory, which we can get using the kernel mapping. We will also use the newly introduced API Binary.get_exact_type
to retrieve the type _ESERVERSILO_GLOBALS
.
def get_drive_letters(ctx: WindowsContext) -> Iterator[str]: # Retrieve the kernel mapping and the types that will be used in this function ntoskrnl_mapping = ctx.kernel_mapping() EServerSiloGlobalsType = ntoskrnl_mapping.binary.exact_type("_ESERVERSILO_GLOBALS") # Retrieve the symbol `PspHostSiloGlobals` try: PspHostSiloGlobals = next( ntoskrnl_mapping.binary.data_symbols("^PspHostSiloGlobals$") ) except StopIteration: print("Symbol 'PspHostSiloGlobals' not found in the kernel") return # Dereference `PspHostSiloGlobals` as a `_ESERVERSILO_GLOBALS` psp_host_silo_globals: reven2.types.StructInstance = ctx.read( ntoskrnl_mapping.base_address + PspHostSiloGlobals.rva, EServerSiloGlobalsType, ) # TODO
From the global variable we will follow the path that I wrote above to access the drive map:
def get_drive_letters(ctx: WindowsContext) -> Iterator[str]: # Previous code... # Retrieve the drive map from `PspHostSiloGlobals.ObSiloState.SystemDeviceMap->DriveMap` drive_map = ( psp_host_silo_globals.field("ObSiloState") .read_struct() .field("SystemDeviceMap") .deref_struct() .field("DriveMap") .read_int() )
As you can see, we are capable of acceding fields of the structures, dereferencing pointers, etc., using this new API feature.
Now that we have the drive map, we can just mimick the pseudo-Python code of GetLogicalDriveStrings
:
def get_drive_letters(ctx: WindowsContext) -> Iterator[str]: # Previous code... # Yield the drive letters accordingly to the bits inside the drive map # Each bit corresponding to a drive letter (bit 0 set = drive A present, etc) for i in range(26): if drive_map & (1 << i): yield chr (ord('A') + i)
Running this function will give you the list of Windows drives available at the specified context!
Now that we have the drive letters, we need to find the device associated with each of them. The C++ code is using QueryDosDevice
for that.
If we record a call of QueryDosDevice
inside REVEN, we are able to see the calltree:
If we look at the arguments of these functions we can see what is happening:
NtOpenDirectoryObject
to open the directory object \??
NtOpenSymbolicLinkObject
to open the symbolic link object C:
(if the drive given to QueryDosDevice
is C:
) inside the directory \??
previously openedNtQuerySymbolicLinkObject
to retrieve the target of the symbolic link object C:
(the target is the device, here \Device\HarddiskVolume2
)The \??
directory is a virtual directory that will point to an overlay between the local DOS devices directory and the global DOS devices directory of the current thread inside the namespace of the object manager. For this article we will assume that the drive letter we are looking for isn't opened only for the current thread, user, etc., but system wide, so we will only use the global DOS devices directory.
After retrieving the global DOS devices directory, we will need to list its child objects to find the symbolic link object C:
(or another name if the drive isn't C
) and when found, retrieve its target link.
A simple path to retrieve the global DOS devices directory is to start from the current structure _EPROCESS
that contains a field DeviceMap
pointing to a structure of type _DEVICE_MAP
, itself containing a pointer to the local and global DOS devices directories of the current process.
So let's start an implementation of a function named get_device_from_drive_letter
taking in arguments a context and the driver letter and returning optionally the device associated with the drive letter.
def get_device_from_drive_letter( ctx: WindowsContext, drive_letter: str ) -> Optional[str]: # Retrieve the kernel mapping and the types that will be used in this function ntoskrnl_mapping = ctx.kernel_mapping() ObjectSymbolicLinkType = ntoskrnl_mapping.binary.exact_type("_OBJECT_SYMBOLIC_LINK") DeviceMapType = ntoskrnl_mapping.binary.exact_type("_DEVICE_MAP") # Retrieve the global DOS devices directory from `_EPROCESS.DeviceMap->GlobalDosDevicesDirectory` dos_devices_directory = ( ctx.get_eprocess() .field("DeviceMap") .read_ptr() .cast_inner(DeviceMapType) .assert_struct() .deref() .field("GlobalDosDevicesDirectory") .deref_struct() ) # TODO
As in the previous step, we are retrieving types from the kernel binary (the type _OBJECT_SYMBOLIC_LINK
will be used in a later step), and following a path inside structures to find our data. A subtlety is that the field DeviceMap
inside the structure _EPROCESS
is typed void*
, but as we know that this field is pointing to a _DEVICE_MAP
we can use the method cast_inner
to indicate it.
The type pointed by GlobalDosDevicesDirectory
is an _OBJECT_DIRECTORY
.
Directory
objectNow that we have our global DOS devices directory, we need to iterate on its children to find the one we are interested in. For that we will provide a utility function named list_directory_object_entries
taking in arguments a context and an instance of a structure _OBJECT_DIRECTORY
and returning an iterator of tuples for each child object accompanied by its address.
If we look into the _OBJECT_DIRECTORY
structure, listing its child objects is quite easy as it contains a hash map.
The field HashBuckets
is an array of linked lists of type _OBJECT_DIRECTORY_ENTRY
. Each _OBJECT_DIRECTORY_ENTRY
has a pointer to a child object.
A possible implementation of list_directory_object_entries
is:
def list_directory_object_entries( ctx: WindowsContext, directory: reven2.types.StructInstance ) -> Iterator[Tuple[reven2.address._AbstractAddress, Object]]: assert directory.type.name == "_OBJECT_DIRECTORY" # Read the hash map of entries inside the directory for bucket in directory.field("HashBuckets").read_array().assert_ptr(): if bucket.address == 0: continue # Take the first _OBJECT_DIRECTORY_ENTRY and go trough the entire linked list of entries # Using an `Optional` here as this variable will be used to store `None` when we are at the end of the linked list object_directory_entry: Optional[ reven2.types.StructInstance ] = bucket.assert_struct().deref() while object_directory_entry is not None: # For each entry, retrieve the object by computing its header address and giving it to the API obj_address = object_directory_entry.field("Object").read_ptr().address obj = Object.from_header( ctx, Object.header_address_from_object(ctx, obj_address) ) yield (obj_address, obj) # Follow the linked list by fetching the next entry inside the field `ChainLink` next_object_directory_entry_ptr = object_directory_entry.field( "ChainLink" ).read_ptr() if next_object_directory_entry_ptr.address == 0: object_directory_entry = None else: object_directory_entry = ( next_object_directory_entry_ptr.assert_struct().deref() )
As you can see in the code below, we are:
NULL
or not_OBJECT_DIRECTORY_ENTRY
using the field ChainLink
_OBJECT_DIRECTORY_ENTRY
we are parsing the associated object using the newly introduced API class Object
Parsing the associated object will give us meaningful information (e.g the type of the object, and if it's a known object of the API, other information like the filename of a file object). This will give us the same type of object returned by the method Handle.object
.
Now that we are capable of iterating on our child objects, let's find our object of interest.
def get_device_from_drive_letter( ctx: WindowsContext, drive_letter: str ) -> Optional[str]: # Previous code... for obj_address, obj in list_directory_object_entries(ctx, dos_devices_directory): # We know that drives will be symbolic links inside the dos devices directory if obj.type_name != "SymbolicLink": continue # Retrieve the name of the object from the optional header (`_OBJECT_NAME_INFORMATION`) of the object obj_name_info_header = obj.raw_name_info_header if obj_name_info_header is None: continue obj_name = read_unicode_string(obj_name_info_header.field("Name").read_struct()) # If the name is matching our drive letter we found the symbolic link of the drive pointing to the target device # We just need to retrieve the target of the symbolic link and it's our device if obj_name != f"{drive_letter}:": continue # The object is the right one # TODO: Find the target device of this symbolic link
We know that our object of interest will:
C:
for the drive C
(the name of an object is given by an optional header _OBJECT_NAME_INFORMATION
of the object retrievable using the property Object.raw_name_info_header
)If an object matches all the above conditions, we found our symbolic link corresponding to the drive and we just need to retrieve its target.
For that, we also know that an object of type SymbolicLink
will have a body of type _OBJECT_SYMBOLIC_LINK
. To make it simpler in this article, we will assume that the field LinkTarget
is used and that the Callback
isn't.
Here is the final code of get_device_from_drive_letter
:
def get_device_from_drive_letter( ctx: WindowsContext, drive_letter: str ) -> Optional[str]: # Retrieve the kernel mapping and the types that will be used in this function ntoskrnl_mapping = ctx.kernel_mapping() ObjectSymbolicLinkType = ntoskrnl_mapping.binary.exact_type("_OBJECT_SYMBOLIC_LINK") DeviceMapType = ntoskrnl_mapping.binary.exact_type("_DEVICE_MAP") # Retrieve the global dos devices directory from `_EPROCESS.DeviceMap->GlobalDosDevicesDirectory` dos_devices_directory = ( ctx.get_eprocess() .field("DeviceMap") .read_ptr() .cast_inner(DeviceMapType) .assert_struct() .deref() .field("GlobalDosDevicesDirectory") .deref_struct() ) for obj_address, obj in list_directory_object_entries(ctx, dos_devices_directory): # We know that drives will be symbolic links inside the dos devices directory if obj.type_name != "SymbolicLink": continue # Retrieve the name of the object from the optional header (`_OBJECT_NAME_INFORMATION`) of the object obj_name_info_header = obj.raw_name_info_header if obj_name_info_header is None: continue obj_name = read_unicode_string(obj_name_info_header.field("Name").read_struct()) # If the name is matching our drive letter we found the symbolic link of the drive pointing to the target device # We just need to retrieve the target of the symbolic link and it's our device if obj_name != f"{drive_letter}:": continue # Read manually the body of the object as a `_OBJECT_SYMBOLIC_LINK` body: reven2.types.StructInstance = ctx.read( obj_address, ObjectSymbolicLinkType ) return read_unicode_string(body.field("LinkTarget").read_struct()) return None
read_unicode_string
is a utility function provided by the new API taking as parameter an instance of a structure _UNICODE_STRING
and returning a Python string.
get_path_with_drive_letter
Now that we have all the blocks we need, we can just assemble them into get_path_with_drive_letter
:
def get_path_with_drive_letter( ctx: WindowsContext, file_object: FileObject ) -> Optional[str]: path = file_object.filename_with_device # Sort of implement the algorithm from the C++ code # - Retrieve the drive letters # - For each drive find the device associated with it # - If the device match the one in the path, we should use this drive letter in the path for drive_letter in get_drive_letters(ctx): device = get_device_from_drive_letter(ctx, drive_letter) if device is None: continue if path.startswith(device): return f"{drive_letter}:{path[len(device):]}" return None
When launching this script with the file object displayed at the very beginning of this article, we have:
File object found with path: \Device\HarddiskVolume2\reven\foo.exe, trying to find the path with the drive letter... Path with the drive letter: C:\reven\foo.exe
We successfuly retrieved the path with the drive letter!
The full script is downloadable here.
Currently the algorithm isn't optimal as we tried to follow closely what the C++ code was doing. For example, we are parsing multiple times the global DOS devices directory (once per drive). A better solution would have been to iterate on it once and cache it or build a hash map drive letter => device name.
In the future, following this article, we can see multiple possible improvements to the API:
SymbolicLink
and Directory
objects so that the iteration on the child objects or the retrieval of the target is done by the API and not by the userObject
class and a way to parse its body directly with a user known type (as you have seen in this article, we needed to return the address of the object and not just the object to be able to parse its body manually using a Context.read
)DriveType
in the same structure as the field DriveMap
)Object.open_by_name("\??\C:").target
)You can try to optimize or improve this script if you want! If you are encountering any issue doing so, we are available to help you in our community if you are a free / professional / enterprise edition user, or also directly by email if you are an enterprise edition user.