Vulnerability Primitives

Vulnerability primitives are the fundamental components or techniques that can be exploited to create vulnerabilities, serving as the building blocks for understanding and analysing security flaws.

Vulnerability primitives like buffer overflows can serve as the initial step in an exploit chain. Discovering a buffer overflow primitive may initially result in a denial of service (crashing the program or causing it to behave unexpectedly).

However, with further development and analysis, we may be able to leverage that primitive to achieve more severe consequences, such as remote code execution.

By understanding and manipulating the vulnerability primitive, we might be able to craft and deliver payloads that exploit the vulnerability in a way that goes beyond simple denial of service and enables execution of arbitrary code on the target system.

This section provides a brief overview of the most common vulnerability primitives. We will use examples from the HackSys Extreme Vulnerable Driver project; this has been purposefully written to contain vulnerabilties.

Stack-based Buffer Overflow

Work in progress

One of the most widely recognised vulnerability primitives is the stack-based buffer overflow.

A stack-based buffer overflow is a type of software vulnerability that occurs when a program writes more data into a buffer (a temporary storage area in the computer's memory) than it can hold. The buffer is typically located on the stack, a region of memory used for local variables and function calls.

When a program exceeds the buffer's capacity, the extra data overflows into adjacent memory locations, potentially overwriting critical information such as return addresses or function pointers. This can lead to various security issues, including the ability for an attacker to execute arbitrary code, crash the program, or gain unauthorised access to the system.

Work in progress end

Heap-based Buffer Overflow

Work in progress

Type Confusion

A type confusion vulnerability occurs when a program assumes a certain data type for an object, but due to a flaw, the object's type is manipulated or misinterpreted, leading to unpredictable behavior and potential security exploits. This can allow us to manipulate memory or execute arbitrary code by confusing the program's interpretation of data types:

We will look at an example from the HackSys Extreme Vulnerable Driver project.

In C, the union keyword is used to define a special data type that enables different variables to share the same memory space. It allows the same memory location to be interpreted in multiple ways, based on the type of data being accessed at a given time.

The purpose of using a union is to save memory by allowing overlapping storage of different types of variables. All members of a union share the same memory space, and the size of the union is determined by the largest member within it.

Usage can lead to type-related issues, particularly type confusion vulnerabilities:

typedef struct _USER_TYPE_CONFUSION_OBJECT {
  ULONG_PTR ObjectID;
  ULONG_PTR ObjectType;
} USER_TYPE_CONFUSION_OBJECT, *PUSER_TYPE_CONFUSION_OBJECT;
 
typedef struct _KERNEL_TYPE_CONFUSION_OBJECT {
  ULONG_PTR ObjectID;
  union {
    ULONG_PTR ObjectType;
    FunctionPointer Callback;
  };
} KERNEL_TYPE_CONFUSION_OBJECT, *PKERNEL_TYPE_CONFUSION_OBJECT;

Line 8 shows the union keyword being used.

The second variable in _KERNEL_TYPE_CONFUSION_OBJECT can be either a ULONG_PTR data type or a FunctionPointer data type.

The vulnerability arises when user provided data is assigned to KernelTypeConfusionObject.

We can provide an arbitrary address in the UserTypeConfusionObject->ObjectType data and this will be copied to KernelTypeConfusionObject->ObjectType which we have seen can be a FunctionPointer data type:

KernelTypeConfusionObject = (PKERNEL_TYPE_CONFUSION_OBJECT)ExAllocatePoolWithTag(
  NonPagedPool,
  sizeof(KERNEL_TYPE_CONFUSION_OBJECT),
  (ULONG)POOL_TAG
);
    
KernelTypeConfusionObject->ObjectID = UserTypeConfusionObject->ObjectID;
KernelTypeConfusionObject->ObjectType = UserTypeConfusionObject->ObjectType;
 
Status = TypeConfusionObjectInitializer(KernelTypeConfusionObject);

Line 10 shows that an initialisation function is called:

NTSTATUS TypeConfusionObjectInitializer(_In_ PKERNEL_TYPE_CONFUSION_OBJECT KernelTypeConfusionObject) {
  NTSTATUS Status = STATUS_SUCCESS;
  KernelTypeConfusionObject->Callback();
  return Status;
}

Because Callback() is invoked on line 3 in the TypeConfusionObjectInitializer function our shellcode is executed.

Arbitrary Write

Arbitrary Write refers to a technique used by exploit developers to write arbitrary data to a specific memory location within a vulnerable program or system. By gaining the ability to write arbitrary values to memory, an attacker can manipulate critical data structures, overwrite function pointers, modify variables, or even inject malicious code into the target system.

Arbitrary Write vulnerabilities are valuable to us because they provide a powerful mechanism for achieving further exploitation or gaining control over a compromised system. By carefully crafting malicious inputs or leveraging existing vulnerabilities, an attacker can exploit an Arbitrary Write primitive to bypass security mechanisms, escalate privileges, or execute arbitrary code within the target environment.

If we will look at the example from the HackSys Extreme Vulnerable Driver project, interaction with the driver presents a "Write What Where" opportunity. The driver takes the following struct as input:

Write-what-where struct
typedef struct _WRITE_WHAT_WHERE {
    PULONG_PTR What;
    PULONG_PTR Where;
 } WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

Because the driver is running in kernel mode we can effectively write anywhere we want. A common technique is to write over a HalDispatchTable entry (step 1) to point to our shellcode and then trigger the call using a user mode API (step 3):

The Hardware Abstraction Layer Dispatch Table, is a data structure. It is a table of function pointers that provides an interface between the hardware and the kernel. The functions within the HalDispatchTable handle low-level hardware operations and facilitate communication between the operating system and the hardware devices.

By modifying the pointer located at HalDispatchTable+0x08, we gain control over the kernel call chain from user space and can execute our code within the kernel space.

Use-After-Free

A use-after-free vulnerability is a type of software vulnerability that occurs when a program continues to use a memory address after it has been freed or deallocated. This can lead to unpredictable behavior and security issues.

The vulnerability typically arises when a program frees a memory block but still holds a reference or pointer to that memory. If the program later attempts to access or use the freed memory, it can result in various problems, such as accessing invalid or corrupted data, causing a crash, or potentially allowing an attacker to execute arbitrary code.

Use-after-free vulnerabilities often occur in programs that manage dynamic memory allocation, such as when objects or data structures are created, modified, and freed during program execution.

In it's purest form a use-after-free vulnerability is shown below:

// Allocate memory for an integer
int* data = (int*)malloc(sizeof(int));

// Store a value in the allocated memory
*data = 42;

// Free the allocated memory
free(data);

// Using a dangling pointer after it has been freed
printf("Value: %d\n", *data);

Work in progress

Interger Overflow/Underflow

An integer overflow vulnerability is a type of software vulnerability that occurs when an arithmetic operation on integers exceeds the maximum value that can be represented by the data type. This can lead to unexpected and potentially dangerous behavior in a program.

In programming languages, integers have a limited range of values they can represent based on their data type (e.g., 32-bit signed integer, 64-bit unsigned integer). When an arithmetic operation, such as addition or multiplication, results in a value larger than the maximum representable value, an overflow occurs.

In a 32-bit system, if an integer variable holds the value 0xFFFFFFFF (the maximum value that can be represented by a 32-bit integer), and you add 1 to it, the result would be 0x00000000.

This behavior occurs because the addition operation causes an integer overflow. The result of the addition exceeds the maximum representable value for a 32-bit integer (0xFFFFFFFF), and the overflow "wraps around" to the minimum representable value (0x00000000) due to the finite range of the data type.

An integer underflow is the opposite of an integer overflow. It occurs when an arithmetic operation on integers results in a value that is smaller than the minimum representable value for the given data type.

The example given in the HackSys Extreme Vulnerable Driver project is shown below:

if ((Size + TerminatorSize) > sizeof(KernelBuffer))
{
    Status = STATUS_INVALID_BUFFER_SIZE;
    return Status;
}

while (Count < (Size / sizeof(ULONG)))
{
    if (*(PULONG)UserBuffer != BufferTerminator)
    {
        KernelBuffer[Count] = *(PULONG)UserBuffer;
        UserBuffer = (PULONG)UserBuffer + 1;
        Count++;
    }
    else
    {
        break;
    }
}

Line 1 is the conditional statement that checks to ensure the size of the user buffer boes not exceed the size of the kernel buffer. However this code is flawed and is vulnerable to an integer overflow.

The integer overflow is essentially exploited to circumvent the intended logic, allowing for the injection of a sufficiently large buffer that triggers a buffer overflow within the kernel (starting on line 9).

Format String Specifiers

Work in progress

Off-by-One

Work in progress

Last updated