Object Handles

  • The Object Manager deals with pointers to kernel memory

  • User-mode applications can't directly read or write to kernel memory

    • To access an object, the application uses the handle returned by a system call

    • Each running process has an associated handle table that contains the following three things

      • Then handle's numeric identifier

      • The granted access to the handle (read or write)

      • The pointer to the object structure in kernel memory

  • Before the kernel is able to use a handle, the system call has to look up the kernel object pointer from the handle table using a kernel API like ObReferenceObjectByHandle

    • By providing this handle, the kernel component can return the handle number to the user-mode application without exposing the kernel object directly

Handle lookup process
  1. User process wants to use a handle, it first passes the handle's value to the system call

  2. System call implementation then calls a kernel API to convert the handle to a kernel pointer by referencing the handle's value in the process's handle table (12 in this case)

  3. To figure out if the user should be granted access, the conversion API takes into consideration the type of access the user requested and the type of object being accessed.

    • If the requested access doesn't match the granted access the API will return STATUS_ACCESS_DENIED and the operation will fail

    • If the object types don't match the API will return STATUS_OBJECT_TYPE_MISMATCH

  • These checks are crucial for security

    • The access check makes sure the user can't do an operation on a handle to which they don't have access ( things like writing to a file for which they only have read permission)

    • The type check makes sure the user hasn't passed an unrelated kernel object type which can lead to type confusion in the kernel, causing things like memory corruption.

    • If the handle conversion succeeds, the system call now has a kernel pointer to the object, which it can use to do the user's requested operation

Access Masks

  • The granted access value in the handle table is a 32-bit bitfield

  • This is the same bitfield used for the DesiredAccess parameter specified in the system call

Access Mask structure
  • Access mash has four components:

    • Type-specific access component

      • 16 bit

      • Defines the operations that are allowed on a particular kernel object type

        • A File might have bits specifying if the file is allowed to be read or written to when using the handle

        • A synchronization event might only have a single bit that allows the event to be signaled

    • standard access

      • Defines operations that can apply to any object type

      • Operations:

        • Delete - Removes the object; for example, by deleting it from disk or from the registry

        • ReadControl - Reads the security descriptor information from the object

        • WriteDac - Writes the security descriptor's discretionary access control (DAC) to the object

        • WriteOwner - Writes the owner information to the object

        • Synchronize - Waits on the object; for example, waits for a process to exit or a mutant to be unlocked

    • reserved and special access

      • Most of these are reserved but they include two access values:

        • AccessSystemSecurity - Reads or writes audit information on the object

        • MaximumAllowed - Requests the max access to an object when performing an access check

    • generic access component

      • Four broad categories of access:

        • GenericRead

        • GenericWrite

        • GenericExecute

        • GenericAll

      • When you request one of these generic access rights, the SRM first converts the access into the corresponding type-specific access

        • Because of this, you will never receive access to a handle with GenericRead but instead you be granted access to the specific access mask that represents read operations for that type

        • To make the conversion easier, each type has a generic mapping table, which maps the four generic categories to a type-specific access

Handle Duplication

  • NtDuplicateObject can be used to duplicate handles

  • Reasons for doing this:

    • Allow a process to take an additional reference to a kernel object

    • Kernel object won't be destroyed until all handles to it are closed, so creating a new handle maintains the kernel object

    • Duplication can be used to transfer handles between processes if the source and destination process handles have DupHandle access

    • Can be used to reduce the access rights on a handle

      • For example, when you pass a file handle to a new process, you could grant the duplicated handle only read access, preventing the new process from writing to the object

      • This shouldn't be relied on to reduce the handle's granted access because if the process with the handle has access to the resource, it can just reopen it to get write access

  1. Initial duplication, keeping the same granted access

  2. First column shows that the handles are different but the call to Compare-NtObject returns True. This means that the two handles refer to the same underlying kernel object.

  3. The output shows the granted access is no only ModifyState

  • Other handle attributes relevant to duplication

    • Inherit - Allows a new process to inherit the handle when it's created, this let's you pass handles to a new process to perform tasks such as redirecting console output text to a file.

    • ProtectFromClose - protects the handle from being closed.

Last updated