twinBASIC Kernel mode driver demo
This is a demonstration of using twinBASIC (Current releases and community on GitHub) to create a kernel mode driver compatible with x64 versions of Windows. I became fascinated with the idea after The_trick figured out how to make them in VB6, and when I saw twinBASIC could compile VB6 code to 64bit... the prospect was fascinating, since VB6 was limited to x86 and there's no WOW64 for kernel mode. We could make drivers for the Windows 64bit OS everyone is running, and even easier with far more features since tB has no runtime... if twinBASIC supported a few features to replicate the hacks that made it possible in VB6. So I made a feature request, and the awesome Wayne Phillips was interested.
For testing purposes, I first made a working VB6 version (included), and then a tB version with all the definitions updated to x64 and taking advantage of a feature tB has to put in-project API declares into the IAT without a typelib, and a controller (written in tB) to load/unload the driver.
Running the project
Build the binaries:
The tB version only needs to be built. The required settings applied for making kernel mode drivers were creating a standard exe, removing the current references, enabling the settings Project: Native subsystem->YES, Project: Override entry point->DriverEntry, Project: Runtime binding of DLL declares->NO.
If you want to build the VB6 version to compare: The VBP includes the undocumented link switches and enables the optimizations needed, so just needs to be opened and compiled. After compiling the .sys, use The_trick's Patcher project (included) to strip the msvbvm60.dll dependency from the .sys.
Running the driver
I've been testing on Windows 7 via VM software, since it's less anal about unsigned drivers, and VMs because errors in drivers typically result in a bluescreen instead of error message or app crash. It's recommended you do the same, but not required.
-
Microsoft has been heading down the road to where you don't own your computer, they do. Windows Vista and newer do not normally allow unsigned drivers, or even self-signed drivers. To get them signed, you have to provide a ton of personal information and pay hundreds of dollars. One way around this is via the Advanced Boot Menu: You'll need to boot using the 'Disable driver signature enforcement' advanced boot option. Reboot and press F8 right when Windows starts loading. VMs like VirtualBox makes it nigh impossible to get an F8 keypress in, so instead, you can also open a command prompt as administrator, and enter the following:
bcdedit /set {globalsettings} advancedoptions true
Then restart. This will bring up the Advanced Boot Options menu, containing "Disable driver signature enforcement". You need to do this every boot for which you want to load unsigned drivers; and unfortunately it disables them globally. NOTE: When you start the driver, Windows will pop up a box saying a signature is required. But this doesn't mean the driver didn't load, as the log will show. -
Create a folder for the project, and put TBHWldDrv.sys and HelloWorldDriverController.exe all in the same folder (and/or VBHWldDrv.sys if you're testing that too). The folder cannot be named TBHWldDrv.
-
Run HelloWorldDriverController.exe as administrator (it has a manifest option requiring this).
-
Click 'Load driver' (Connect is only for if it's already running, e.g. if it's been installed in the boot sequence, it isn't by default and isn't necessary). This will create a service for a kernel driver and start it.
-
If it successfully loads and connects (there's a log that will tell you), you can send the version command to get a response back from the driver.
-
When done, click Unload and delete. This will remove the service created to load the driver. Then Exit.
How it works
The first step is the DriverEntry function. Normally VB and tB exes have a hidden function that's the first to run (even before Sub Main), this is used to set up various things like COM. But a driver can't have any of that; it must enter through the DriverEntry function. You won't be able to use any APIs besides ones in ntoskrnl.exe
, the Windows kernel module. This is because there's a barrier between user mode and kernel mode, and you can't load user mode DLLs into a kernel mode driver.
In VB, this dramatically limited what you can do, because virtually everything relies on the msvbvm60.dll runtime. But twinBASIC doesn't rely on an external runtime; all of the VB runtime stuff is built right into the exe. It also provides an option to put API declares in the Import Address Table, in VB they're late-bound and called using runtime APIs and only added to the IAT if they're in a TLB. This makes programming drivers quite a bit easier, because for instance you can use VarPtr
directly; in VB you'd need to use something like InterlockedExchange
to copy the pointer into a new variable.
There are some limitations. You still can't use strings or arrays (besides 1D arrays inside UDTs) because these are managed with APIs behind the scenes, so this project uses The_trick's BinaryString method of putting strings into the 1D UDT arrays that don't use SAFEARRAY and thus don't need APIs. But generally, there's a hell of a lot more twinBASIC lets you do.
In the DriverEntry function we do 4 things: Initialize our string types, create the DEVICE_OBJECT
that descibes our driver to the system, create a symbolic link that allows the driver to communicate with user mode apps via CreateFile
, and set up the function table for the IRP major functions. For most of these, we just implement default handlers that pass on IRPs (I/O request packets) to the next driver in the stack. The one we're primarily interested in for this demo is IRP_MJ_DEVICE_CONTROL
: If you've ever used the DeviceIoControl API, this is where those commands are going, to device drivers.
Public Function DriverEntry(ByRef DriverObject As DRIVER_OBJECT, ByRef RegistryPath As UNICODE_STRING) As Long
InitDebugStrings
DbgPrint VarPtr(dbgsEntry)
InitUnicodeStrings
InitFuncs
Dim ntStatus As Long
ntStatus = IoCreateDevice(DriverObject, 0&, DeviceName, FILE_DEVICE_UNKNOWN, 0&, False, Device)
If NT_SUCCESS(ntStatus) Then
ntStatus = IoCreateSymbolicLink(DeviceLink, DeviceName)
If Not NT_SUCCESS(ntStatus) Then
IoDeleteDevice Device
DriverEntry = ntStatus
Exit Function
End If
Dim i As Long
For i = 0 To IRP_MJ_MAXIMUM_FUNCTION
DriverObject.MajorFunction(i) = AddressOf OnOther
Next
DriverObject.MajorFunction(IRP_MJ_CREATE) = AddressOf OnCreate
DriverObject.MajorFunction(IRP_MJ_CLOSE) = AddressOf OnClose
DriverObject.MajorFunction(IRP_MJ_DEVICE_CONTROL) = AddressOf OnDeviceControl
DriverObject.DriverUnload = AddressOf OnUnload
End If
DriverEntry = ntStatus
End Function
We define a custom command for our project using the CTL_CODE
macro, which can be implemented easily thanks to tB having << and >> bitshift operators. The command we're defining is used to send the driver a verification number to check to make sure everything is ok as input. The output is the HelloWorldVersion
UDT, a custom structure we use for the driver to pass it's version data to us to test that everything is working and we've successfully created a driver that takes commands and exchanges data with user mode applications.
In the driver controller, after starting the driver with the service APIs and connecting to it with CreateFile
, the command is sent:
Dim tVer As HelloWorldVersion
Dim lVerify As Long
Dim cbRet As Long
Dim result As Long
AppendLog "Sending IOCTL_HWRLD_VERSION to driver..."
result = DeviceIoControl(hDev, IOCTL_HWRLD_VERSION, lVerify, 4&, tVer, LenB(tVer), cbRet, ByVal 0&)
AppendLog "Result: ret=0x" & Hex$(result) & ",cbRead=" & cbRet & vbCrLf & "Version (Expecting 1.2.3.4)=" & tVer.Major & "." & tVer.Minor & "." & tVer.Build & "." & tVer.Revision
If we get the version numbers we expect, SUCCESS! Everything has worked.
In the driver, we receive that command:
Public Function OnDeviceControl(ByRef DriverObject As DRIVER_OBJECT, ByRef pIrp As IRP) As Long
DbgPrint VarPtr(dbgsDevIoEntry)
Dim lpStack As LongPtr
Dim ioStack As IO_STACK_LOCATION
Dim ntStatus As Long
pIrp.IoStatus.Information = 0
lpStack = IoGetCurrentIrpStackLocation(pIrp)
If lpStack Then
DbgPrint VarPtr(dbgsStackOk)
CopyMemory ioStack, ByVal lpStack, LenB(ioStack)
If (ioStack.DeviceIoControl.IoControlCode = IOCTL_HWRLD_VERSION) And (pIrp.AssociatedIrp <> 0) Then
DbgPrint VarPtr(dbgsCmdOk)
Dim tVer As HelloWorldVersion
Dim lpBuffer As LongPtr
Dim cbIn As Long, cbOut As Long
lpBuffer = pIrp.AssociatedIrp
cbIn = ioStack.DeviceIoControl.InputBufferLength
cbOut = ioStack.DeviceIoControl.OutputBufferLength
If (lpBuffer = 0&) Or (cbIn <> 4) Or (cbOut <> LenB(tVer)) Then
If (lpBuffer = 0&) Then
ntStatus = STATUS_BUFFER_ALL_ZEROS
DbgPrint VarPtr(dbgsNoBuffer)
ElseIf (cbOut <> LenB(tVer)) Then
ntStatus = STATUS_INVALID_BUFFER_SIZE
DbgPrint VarPtr(dbgsBadSize)
Else
ntStatus = STATUS_INVALID_DEVICE_REQUEST
DbgPrint VarPtr(dbgsNoVal)
End If
Else
DbgPrint VarPtr(dbgsCopyOut)
tVer.Major = 1
tVer.Minor = 2
tVer.Build = 3
tVer.Revision = 4
CopyMemory ByVal lpBuffer, tVer, cbOut
pIrp.IoStatus.Information = LenB(tVer)
End If
pIrp.IoStatus.StatusPointer = ntStatus
IoCompleteRequest pIrp, IO_NO_INCREMENT
OnDeviceControl = ntStatus
Exit Function
The current location on the stack has all the DeviceIoControl arguments from when we called that API, we check if everything is ok, if so, we copy the data to the i/o packet.
The last thing we handle is the Unload function, where we delete the symbolic link and device object:
Public Sub OnUnload(DriverObject As DRIVER_OBJECT)
If Device.Size = 0 Then Exit Sub
IoDeleteSymbolicLink DeviceLink
IoDeleteDevice ByVal DriverObject.DeviceObject
End Sub
This project shows the use of the DbgPrint function so we can more easily debug our driver like a normal VB/tB project instead of the extraordinarily difficult WinDbg's kernel debugger. It's a CDecl function, but fortunately tB supports that natively so we don't have to worry about the workarounds VB requires for that. It has a ...
argument that maps to a ParamArray; but right now tB manages those behind the scenes with APIs, so can't use them here... but those are all optional, we can supply just the string, then use DebugView to see the output. Note that you must open DebugView (as administrator) after clicking load driver, or it won't attach properly.
Self-signed kernel mode drivers
According to Geoff Chappell, it's possible to use a bunch of undocumented functionality to load self-signed drivers under Windows 10/11 x64, with SecureBoot enabled, without disabling signature checks. Have a read of his article on it if you're interested in trying it. This is not for the faint of heart however. It's extremely complicated. You'll need the Windows SDK installed for the signing tools, and Windows 10/11 Enterprise or, oddly, Education editions, to compile the policy files (though you may be able to use the ones he provides).
Requirements
twinBASIC Beta 95 or newer (v0.15.95) This will build for x86 or x64. You cannot use the x86 version of the driver on x64; though you could probably use the controller. I've only tested this on Windows 10+.
Thanks
This project reuses many definitions and techniques from o.g. BASIC driver developer The_trick. The rest of it is heavily based on Geoff Chappell's SelfSign driver from the article above. I'm brand new to driver development myself, and had never attempted it before. If I can follow those projects and do it, so can you. :)