Introduction to IOCTL (Input/Output CONTROL)

11 minute read

Introduction

In this blog, We will be discussing the Basics of `IOCTL (Input/Output CONTROL)`, What is it ?, The modes types, What is it used for ? and I will show an example of sending an IOCTL Request. But, before we moving further with all of this we need to understand the User Mode and the Kernel Mode.

The Modes

Basically, The modes word itself referring to the different states of operation & different levels of privilege that the system or a software could run. In general, each mode different from the other and operate differently with different privileges and access levels & the main 2 modes we have and will talk about is User Mode&Kernel Mode.

What is User Mode ?

The User Mode is the mode which the levels of operations & privileges are restricted and limited in access & permissions. So, the software or any program running in the User Mode or inside it, Won’t be able to perform certain operations that could be malicious or harmful to other softwares and the system. In short, User Mode is a non-privileged mode where it can’t access the hardware resources or any low level operations directly. For example, A malware running in the User Mode can’t access or modify the CPU instructions cause if this happened the malware easly will have an elevated privileges and as it’s on the hardware. Therefore, It’s the highest privileges you can get and u won’t have any restriction.

What is Kernel Mode ?

The Kernel Mode is a non-restricted mode where you can access the hardware resources directly where you will be able to manage any related resources such as CPU & Memory and perform any low level operations easily. For example, any one get access to the Kernel Mode will be able to manipulate the resources easily and can also escalate privileges which could happen through a vulnerability or any misconfigurations. In short, The Kernel Mode opposite to the User Mode and each of one has different levels of operations & levels of privileges.

Input/Output Control (IOCTL)

IOCTL is short for “Input/Output Control” which is a system call used for input/output operations and communications from the User Mode to the Kernel Mode drivers. Basically, It allows the components in the User Mode for example a software to communicate and control the behaviour of a device driver in Kernel Mode.

IOCTLs

The IOCTLs are a set of codes and each one of these codes has a specific job to be used in and perform. For example:

  • IOCTL_DISK_GET_LENGTH_INFO: This IOCTL code is used to retrieve the length of a disk partition.
  • IOCTL_VIDEO_QUERY_SUPPORTED_BRIGHTNESS: This IOCTL code is used to query the supported brightness levels of a display.
  • IOCTL_SERIAL_SET_BAUD_RATE: This IOCTL code is used to set the baud rate of a serial port.
  • IOCTL_CDROM_GET_LAST_SESSION: This IOCTL code is used to retrieve information about the last session on a CD-ROM.
  • IOCTL_STORAGE_QUERY_PROPERTY: This IOCTL code is used to retrieve information about a storage device.
  • IOCTL_MOUSE_GET_ATTRIBUTES: This IOCTL code is used to retrieve the attributes of a mouse device.
  • IOCTL_KEYBOARD_SET_TYPEMATIC: This IOCTL code is used to set the typematic rate of a keyboard.
  • IOCTL_SMARTCARD_TRANSMIT: This IOCTL code is used to send a command to a smart card reader.
  • IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION: This IOCTL code is used to retrieve a USB descriptor from a USB device.
  • IOCTL_NDIS_QUERY_GLOBAL_STATS: This IOCTL code is used to query global statistics for a network adapter.

Those where example for the IOCTLs you can find all the IOCTL codes you need from Here.

How IOCTL works ?

When the apps or the components in the User Mode wants to query information of a device driver in the Kernel Mode. It sends a request known as IOCTL request, The IOCTL requests are made & sent using the DeviceIoControl() which is a windows API function.

60bb5e544fe70cfb763805864706f0ae.png

When the the DeviceIoControl() function gets called it is actually implemented by calling another function which is NtDeviceIoControlFile() resides in the ntdll.dll, Then the IOCTL request sent to the device driver. There are 4 buffering methods that can be used with the IOCTL request which are:

  • METHOD_BUFFERED: Copies the input and output buffers to and from the driver through an intermediate system buffer.
  • METHOD_IN_DIRECT: Its used when the input buffer is large and will not fit in the intermediate system buffer. In this case, the input buffer is mapped directly into the driver’s address space.
  • METHOD_OUT_DIRECT: In case the output buffer is large and will not fit in the intermediate system buffer. In this case, the output buffer is mapped directly into the driver’s address space.
  • METHOD_NEITHER: When the input and output buffers are not contiguous and cannot be mapped into a single buffer. The driver and application exchange pointers to the separate input and output buffers.

Note: If you wondering at which part the user get’s a Kernel Mode access. Basically, there are 2 functions/syscalls used which are kifastsystemcall and kifastsystemcallret and both are kernel functions that provide a mechanism for making syscalls from user mode to kernel mode. The kifastsystemcall function takes the parameters for a syscall and makes the transition from user mode to kernel mode using a secret number called syscall gate (which grants the Kernel Mode access) and the parameters onto the stack. Then it makes a syscall instruction to trigger the kernel to execute the syscall. And the kifastsystemcallret function is used to return from the syscall back to the User Mode by passing the return address of the User Mode code which is stored on the stack during the syscall.

Sending IOCTL Request

To understand the IOCTL request we will do in a clearly way, The DeviceIoControl() function structure is as the following:

BOOL DeviceIoControl(
  HANDLE       hDevice,
  DWORD        dwIoControlCode,
  LPVOID       lpInBuffer,
  DWORD        nInBufferSize,
  LPVOID       lpOutBuffer,
  DWORD        nOutBufferSize,
  LPDWORD      lpBytesReturned,
  LPOVERLAPPED lpOverlapped
);

each parameter in the function represents as the following:

  • hDevice: It’s a handle to the device thatwill be used to perform the operation.
  • dwIoControlCode: The IOCTL code that identifies the operation to be performed.
  • lpInBuffer: A pointer to the input buffer that contains the data required to perform the operation.
  • nInBufferSize: The size of the input buffer.
  • lpOutBuffer: A pointer to the output buffer that receives the data returned by the operation.
  • nOutBufferSize: The size of the output buffer.
  • lpBytesReturned: A pointer to the number of bytes returned by the operation.
  • lpOverlapped: A pointer to an OVERLAPPED structure that is used for asynchronous operations.

Note: the data size is in bytes & the Asynchronous operations is an operation that allows the software to continue executing while waiting for a potentially long-running operation to be completed and the OVERLAPPED structure is used to manage these operations.

Now, It’s the time to send our IOCTL request and i will be using the IOCTL_DISK_GET_DRIVE_GEOMETRY_EX code which retrieves extended information about the physical disk’s geometry like the type, number of cylinders, etc.

IOCTL request Code:

#include <stdio.h>
#include <windows.h>
#include <winioctl.h>

int main() {
  HANDLE hDevice;
  DWORD dwIoControlCode;
  BOOL bResults;
  DWORD dwBytesReturned;
  DISK_GEOMETRY_EX diskGeometry;

  hDevice = CreateFileW(L "\\\\.\\PhysicalDrive0", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  dwIoControlCode = 0x000700A0;
  bResults = DeviceIoControl(hDevice, dwIoControlCode, NULL, 0, & diskGeometry, sizeof(diskGeometry), & dwBytesReturned, NULL);

  if (hDevice == INVALID_HANDLE_VALUE) {
    printf("Handling the device drive faild");
    exit(0);
  } else if (bResults == FALSE) {
    printf("IOCTL request faild");
    CloseHandle(hDevice);
    exit(0);
  } else {

    printf("Disk size: %llu bytes\n", diskGeometry.DiskSize.QuadPart);
    CloseHandle(hDevice);

  }

  return 0;
}

Now, The above code is the one we will use to send the IOCTL Request, Let’s explain it. First, We included the needed header files that contains the needed functions & methods we will use which are:

  • stdio.h: Which is used for the standard input/output and contains functions such as printf()
  • windows.h: Which provides us with the Windows APIs as the CreateFileW() we used in the code.
  • winioctl.h: Which contains the IOCTL codes and let us perform the IOCTL Request.

After that we defined some variables in the main() function and each one of them has a role as the following:

  • HANDLE hDevice: We declared hDevice as a HANDLE object which used to access and manipulate system resources in our case it’s the device drive.
  • DWORD dwIoControlCode: Here we declared dwIoControlCode as a DWORD data type where we gonna store the IOCTL code.
  • BOOL bResults: Declared the bResults variable as a Boolean data type to store the result of DeviceIoControl() function call.
  • DWORD dwBytesReturned: Here we declared dwBytesReturned as a DWORD data type which we wll use to store the bytes returned by DeviceIoControl() function.
  • DISK_GEOMETRY_EX diskGeometry: declaring diskGeometry variable as a DISK_GEOMETRY_EX data type variable that will store the disk geometry information retrieved by the DeviceIoControl().

It’s the time now to assign and use these variables and initial our IOCTL request in the following lines of code:

hDevice = CreateFileW(L"\\\\.\\PhysicalDrive0", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

Here we used the CreatFileW() Windows API which generally it’s used to create and open files on disk. But, it’s also can be used to open the device drive. The function takes some values as parameters:

  • L"\\\\.\\PhysicalDrive0": Here is device drive and PhysicalDrive0 indicates to the first hard disk drive & the \\\\.\\ is used to communicating with localhost.
  • GENERIC_READ | GENERIC_WRITE: Specifies the access mode for the handle.
  • FILE_SHARE_READ | FILE_SHARE_WRITE: Specifies how the handle can be shared with other processes.
  • NULL: This is the security attributes. We set it to NULL for default security.
  • OPEN_EXISTING: Specifies that the function should open an existing file
  • FILE_ATTRIBUTE_NORMAL: Specifies that the file should have no other attributes set.
  • NULL: A template file that we don’t need.

Next we assign the IOCTL code which is 0x000700A0 to the dwIoControlCode variable. The 0x000700A0 hex code refers to IOCTL_DISK_GET_DRIVE_GEOMETRY_EX. After that we called the DeviceIoControl() and assign it to bResults variable.

bResults = DeviceIoControl(hDevice, dwIoControlCode, NULL, 0, &diskGeometry, sizeof(diskGeometry), &dwBytesReturned, NULL);

And as we can see the DeviceIoControl() takes some parameters and we have explained the structure of it above.

if (hDevice == INVALID_HANDLE_VALUE) {
    printf("Handling the device drive faild");
    exit(0);
  } else if (bResults == FALSE) {
    printf("IOCTL request faild");
    CloseHandle(hDevice);
    exit(0);
  } else {

    printf("Disk size: %llu bytes\n", diskGeometry.DiskSize.QuadPart);
    CloseHandle(hDevice);
  }

Finally, Here we check for errors to handle such as INVALID_HANDLE_VALUE which indicates that the handling value is invalid and in the second conditionit checks if the returned boolean of DeviceIoControl() that stored in bResults False or no, If False that means the operation didn’t done successfully. If the both of the 2 condations aren’t true that means everything goes well and will get us the disk size using diskGeometry.DiskSize.QuadPart and print it. At the end will close the Handle we created. Now, I am using CodeBlocks to write and execute the code.

Running The code

First, I will put a random name to handle. Therefore, it will result in an error:

285a0e0021dcfab64ff05518707ccadb.png

You can see that the error has been handled. This time i will run the code normally with no problems:

86e7c1f0c3d477c52e600b5485098b32.png

And here is the disk size with no issues. If we opened Procmon or Process Monitor and run our code after compiling it, We will be able to see the process very clearly.

8589866d93354c65c3552496564b2955.png

As we can see in the screen shot clearly we can see all the operations done by the compiled code process from the start going through the calls and so on, Until the process exit.

Conclusion

Now, we know what is IOCTL (Input/Output CONTROL) and how it works & This was just a basic introduction to the **IOCTL (Input/Output CONTROL)** and in the coming parts we will be diving deep by showing more examples & Reverse Engineer a driver and Identifying the IOCTL and extracting IOCTL codes and many more.