'''
A quite dumb IOCTL fuzzer.

TO DO list:

* Guard against drivers accepting arbitrary IOCTL code.
* Implement heuristic scan for input and output buffer sizes.
* Improve recording performance

Reminder on how control codes are built.

 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
+-+-----------------------------+---+-+---------------------+---+
|C|       Device  Type          |RqA|c|    Function Code    |TrT|
+-+-----------------------------+---+-+---------------------+---+

C: Common bit
  Set for vendor-assigned Device Type values.

Device Type:
  Identifies the device type.
  Values of less than 0x8000 are reserved for Microsoft.

RqA: Required Access
  Indicates the type of access that a caller must request when opening
  the file object that represents the device.

c: Custom bit
  Set for vendor-assigned Function Code values.

Function Code:
  Identifies the function to be performed by the driver.
  Values of less than 0x800 are reserved for Microsoft.

TrT: Transfer Type
  Indicates how the system will pass data between the caller
  of DeviceIoControl and the driver that handles the IRP.
  See: https://msdn.microsoft.com/en-us/library/windows/hardware/ff540663%28v=vs.85%29.aspx

'''

import sys
import os
import ctypes
import argparse
import random
import time

#
# NT junk
#

NtOpenFile = ctypes.windll.ntdll.NtOpenFile
NtClose = ctypes.windll.ntdll.NtClose
NtAllocateVirtualMemory = ctypes.windll.ntdll.NtAllocateVirtualMemory
NtDeviceIoControlFile = ctypes.windll.ntdll.NtDeviceIoControlFile
SetConsoleTitle = ctypes.windll.kernel32.SetConsoleTitleA

# Standard Access Rights
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa379607(v=vs.85).aspx

FILE_READ_DATA = 0x00000001
FILE_WRITE_DATA = 0x00000002
FILE_READ_EA = 0x00000008
FILE_WRITE_EA = 0x00000010
FILE_READ_ATTRIBUTES = 0x00000080
FILE_WRITE_ATTRIBUTES = 0x00000100

READ_CONTROL = 0x00020000
SYNCHRONIZE = 0x00100000

FILE_READ_XXX = FILE_READ_ATTRIBUTES | FILE_READ_EA | FILE_READ_DATA
FILE_WRITE_XXX = FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_WRITE_DATA

FILE_GENERIC_READ = SYNCHRONIZE | READ_CONTROL | FILE_READ_XXX
FILE_GENERIC_WRITE = SYNCHRONIZE | READ_CONTROL | FILE_WRITE_XXX

FILE_SHARE_READWRITE = 3

FILE_ANY_ACCESS = 0x0000
FILE_READ_ACCESS = 0x0001
FILE_WRITE_ACCESS = 0x0002

METHOD_BUFFERED = 0
METHOD_NEITHER = 3

MEM_COMMIT = 0x00001000
MEM_RESERVE = 0x00002000
PAGE_EXECUTE_READWRITE = 0x00000040

BUFFER_BASE = 0x1000
buffer_ptr = ctypes.cast(0x1000, ctypes.POINTER(ctypes.c_byte))

def to_unsigned(x):
    if x < 0:
        return 0x80000000L | (x & 0x7FFFFFFFL)
    return long(x)

class NtError(Exception):
    def __init__(self, status):
        self.status = to_unsigned(status)
    def __str__(self):
        return 'NTSTATUS: %08X' % self.status
    def __repr__(self):
        return str(self)

class IO_STATUS_BLOCK(ctypes.Structure):
    _fields_ = [
            ('a', ctypes.c_ulong),
            ('b', ctypes.c_ulong),
        ]

class UNICODE_STRING(ctypes.Structure):
    def __init__(self, s):
        bytes = len(s) * 2
        ss = unicode(s)
        ctypes.Structure.__init__(self, bytes, bytes + 2, ss)

    _fields_ = [
            ('Length', ctypes.c_ushort),
            ('MaximumLength', ctypes.c_ushort),
            ('Buffer', ctypes.c_wchar_p),
        ]

class OBJECT_ATTRIBUTES(ctypes.Structure):
    def __init__(self, name):
        self.__name = name
        self.__us_name = UNICODE_STRING(name)
        ctypes.Structure.__init__(self,
            ctypes.sizeof(self),
            None,
            ctypes.pointer(self.__us_name),
            0,
            None,
            None)

    _fields_ = [
            ('Length', ctypes.c_ulong),
            ('RootDirectory', ctypes.c_void_p),
            ('ObjectName', ctypes.POINTER(UNICODE_STRING)),
            ('Attributes', ctypes.c_ulong),
            ('SecurityDescriptor', ctypes.c_void_p),
            ('SecurityQualityOfService', ctypes.c_void_p),
        ]

IoStatusBlock = IO_STATUS_BLOCK()

def OpenFile(path, access):
    "Wrapper around NtOpenFile funkiness"

    oa = OBJECT_ATTRIBUTES(path)
    handle = ctypes.c_ulong(-1)
    status = NtOpenFile(ctypes.byref(handle),
        access,
        ctypes.byref(oa),
        ctypes.byref(IoStatusBlock),
        FILE_SHARE_READWRITE,
        0)
    status = to_unsigned(status)
    if status != 0:
        raise NtError(status)
    return handle

def DeviceIoControl(handle, ioctl, input_ptr, input_length, output_ptr, output_length):
    "Wrapper around NtDeviceIoControl funkiness"

    # Handle, Event, ApcRoutine, ApcContext, IoStatusBlock, IoControlCode, InputBuffer, InputBufferLength, OutputBuffer, OutputBufferLength
    status = NtDeviceIoControlFile(handle,
        None, None, None,
        ctypes.byref(IoStatusBlock),
        ioctl,
        input_ptr, input_length,
        output_ptr, output_length)
    return to_unsigned(status)

def TryOpenFile(path):
    "Try opening the provided device with most access rights possible"

    print('Opening the device...')
    try:
        handle = OpenFile(path, SYNCHRONIZE | READ_CONTROL | FILE_READ_DATA | FILE_WRITE_DATA)
        print('Opened with read and write access.')
        return (handle, FILE_READ_ACCESS|FILE_WRITE_ACCESS)
    except NtError:
        pass
    try:
        handle = OpenFile(path, SYNCHRONIZE | READ_CONTROL | FILE_WRITE_DATA)
        print('Opened with write access only.')
        return (handle, FILE_WRITE_ACCESS)
    except NtError:
        pass
    try:
        handle = OpenFile(path, SYNCHRONIZE | READ_CONTROL | FILE_READ_DATA)
        print('Opened with read access only.')
        return (handle, FILE_READ_ACCESS)
    except NtError:
        pass
    try:
        handle = OpenFile(path, SYNCHRONIZE | READ_CONTROL)
        print('Opened for minimal access.')
        return (handle, 0)
    except NtError as e:
        print('Failed to open the device, NTSTATUS of the last attempt: %08X.' % e.status)
    return (None, 0)
#
# SCANNING
#

def scan_ioctl(handle, ioctl_code, probe_buffers=True):
    "Try to figure out additional info about the given IOCTL"

    # Check if IOCTL uses no inputs at all.
    status = DeviceIoControl(handle, ioctl_code, None, 0, None, 0)
    if status < 0x40000000L:
        print('  IOCTL responded with no error when no buffers were provided.')
    else:
        if probe_buffers:
            print('  Will probe for buffer sizes.')
            # Try to figure out the input buffer minimum size
            probe_status = DeviceIoControl(handle, ioctl_code, BUFFER_BASE, 0, BUFFER_BASE, 0x1000)
            if probe_status == 0xC0000023L: # STATUS_BUFFER_TOO_SMALL
                size = 0
                while probe_status == 0xC0000023L and size < 0x1000:
                    size += 2
                    probe_status = DeviceIoControl(handle, ioctl_code, BUFFER_BASE, size, BUFFER_BASE, 0x1000)
                print('  Guesstimate of minimal input buffer size: %d bytes' % size)
            else:
                print('  Input buffer size probe returned %08X' % probe_status)
            # Try to figure out the output buffer minimum size
            probe_status = DeviceIoControl(handle, ioctl_code, BUFFER_BASE, 0x1000, BUFFER_BASE, 0)
            if probe_status == 0xC0000023L: # STATUS_BUFFER_TOO_SMALL
                size = 0
                while probe_status == 0xC0000023L and size < 0x1000:
                    size += 2
                    probe_status = DeviceIoControl(handle, ioctl_code, BUFFER_BASE, 0x1000, BUFFER_BASE, size)
                print('  Guesstimate of minimal output buffer size: %d bytes' % size)
            else:
                print('  Output buffer size probe returned %08X' % probe_status)
    return status

def scan_functions(handle, device_type, req_access, transfer_type, not_implemented, ioctls):
    "Sweep-scan the `Function Code` field"

    # This is constant.
    ioctl_template = (device_type << 16) | (req_access << 14) | 0 | (transfer_type)

    func_code = 0
    while func_code <= 0xFFFL:
        ioctl_code = ioctl_template | (func_code << 2)
        # TODO: Some drivers return STATUS_NOT_IMPLEMENTED for incorrect params.
        status = DeviceIoControl(handle, ioctl_code, None, 0, None, 0)
        if status not in not_implemented:
            print('IOCTL %08X: Returned NTSTATUS: %08X' % (ioctl_code, status))
            print('  DeviceType:%04X ReqAcc:%d FunctionCode:%03X XferType:%d' % (device_type, req_access, func_code, transfer_type))
            scan_ioctl(handle, ioctl_code)
            ioctls.append(ioctl_code)
        func_code += 1

def scan_device_type_xfers(handle, device_type, access_type, not_implemented, ioctls):
    scan_functions(handle, device_type, access_type, 0, not_implemented, ioctls)
    scan_functions(handle, device_type, access_type, 1, not_implemented, ioctls)
    scan_functions(handle, device_type, access_type, 2, not_implemented, ioctls)
    scan_functions(handle, device_type, access_type, 3, not_implemented, ioctls)

def scan_device_type(handle, granted_access, device_type, not_implemented, ioctls):
    "Sweep-scan the given `Device Type` for valid IOCTLs"

    print('Scanning device type %04X...' % device_type)
    scan_device_type_xfers(handle, device_type, 0, not_implemented, ioctls)
    if granted_access & FILE_READ_ACCESS:
        scan_device_type_xfers(handle, device_type, 1, not_implemented, ioctls)
    if granted_access & FILE_WRITE_ACCESS:
        scan_device_type_xfers(handle, device_type, 2, not_implemented, ioctls)
    if granted_access == (FILE_READ_ACCESS|FILE_WRITE_ACCESS):
        scan_device_type_xfers(handle, device_type, 3, not_implemented, ioctls)

def scan_device_type_range(handle, granted_access, device_start, device_end, not_implemented, ioctls):
    "Scan the specified `Device Type` range"

    device_type = device_start
    while device_type <= device_end:
        scan_device_type(handle, granted_access, device_type, not_implemented, ioctls)
        device_type += 1

__standard_not_implemented = (
        0xC0000002L, # STATUS_NOT_IMPLEMENTED
        0xC0000010L, # STATUS_INVALID_DEVICE_REQUEST
        0xC00000BBL, # STATUS_NOT_SUPPORTED
    )

def scan_detect_not_implemented(handle, granted_access):
    "Detect how the driver responds to IOCTLs that are not implemented"
    global __standard_not_implemented

    print('Detecting how the device responds to not-implemented IOCTLs')
    stats = {}
    for counter in xrange(128):
        ioctl = random.randint(0, 0xFFFFL) << 16
        status = DeviceIoControl(handle, ioctl, None, 0, None, 0)
        try:
            count = stats[status] + 1
        except KeyError:
            count = 1
        stats[status] = count

    max_count = 0
    chosen_status = 0
    for status, count in stats.iteritems():
        if count > max_count:
            max_count = count
            chosen_status = status

    print('Not-implemented NTSTATUS: %08X (hit %d times)' % (chosen_status, max_count))
    if chosen_status not in __standard_not_implemented:
        print('WARNING! The device responds with a non-standard NTSTATUS value.')
        print('WARNING! The scan results may not report all IOCTLs.')
    return (chosen_status,)

def do_scan(args):
    "Main scanning function"

    handle, access = TryOpenFile(args.device_path)
    if handle is None:
        return
    if args.ignore_granted_access:
        access = FILE_READ_ACCESS|FILE_WRITE_ACCESS

    not_implemented = scan_detect_not_implemented(handle, access)

    ioctls = []
    if args.scan_device is None:
        device_type_low = 0x0000
        device_type_high = 0xFFFF
        if args.scan_system:
            device_type_high = 0x7FFF
        if args.scan_vendor:
            device_type_low = 0x8000
        if device_type_low <= device_type_high:
            print('Scanning device type range %04X:%04X' % (device_type_low, device_type_high))
            try:
                scan_device_type_range(handle, access, device_type_low, device_type_high, not_implemented, ioctls)
            except KeyboardInterrupt:
                print('Interrupted.')
        else:
            print('Incorrect scan range.')
    else:
        try:
            scan_device_type(handle, access, args.scan_device, not_implemented, ioctls)
        except KeyboardInterrupt:
            print('Interrupted.')
    print('Scan completed (%d IOCTL codes reported).' % len(ioctls))

    if ioctls:
        print('\nIOCTL codes discovered:')
        print('   IOCTL   Dev:Fun RW Transfer type')
        for ioctl in ioctls:
            device_type = ioctl >> 16
            function = (ioctl >> 2) & 0xFFF
            access = (ioctl >> 14) & 3
            method = ('METHOD_BUFFERED', 'METHOD_IN_DIRECT', 'METHOD_OUT_DIRECT', 'METHOD_NEITHER')[ioctl & 3]
            print('%08X  %04X:%03X %c%c %s' % (ioctl, device_type, function, 'R-'[not (access & 1)], 'W-'[not (access & 2)], method))

    NtClose(handle)

#
# FUZZING (GENERATION/RECORDING)
#

__nasty_bytes = (0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0xFF)

def fuzz_generate(buffer_ptr, input_length):
    "Generate the data passed to the ioctl"

    global __nasty_bytes

    if random.random() < 0.2:
        ctypes.memset(buffer_ptr, 0xbe, 0x1000)
    else:
        ctypes.memset(buffer_ptr, 0, 0x1000)

    for offset in xrange(input_length):
        if random.random() <= 0.2:
            buffer_ptr[offset] = random.choice(__nasty_bytes)
        offset += 1

def fuzz_ioctl(handle, ioctl_code, buffer_limits, record=False, record_last=False):
    "Perform one fuzzing interation over the given ioctl"

    # Initial conditions
    input_ptr, input_length, output_ptr, output_length = 0, 0, 0, 0

    transfer_type = ioctl_code & 3

    # Non-default input pointer?
    if buffer_limits[0][0] > 0 or random.random() < 0.5:
        input_ptr = BUFFER_BASE

        input_length = random.randint(buffer_limits[0][0], buffer_limits[0][1] + 1)
        fuzz_generate(buffer_ptr, input_length)