Hello everyone,
I recently spent some time looking at ElbyCDIO.sys, the kernel driver installed by Virtual CloneDrive / Elaborate Bytes.
My goal was not to throw random IOCTLs at the driver and hope for a crash. I wanted to treat it like a small driver research project: understand the exposed device, map the IOCTL handler, reverse the interesting cases, and then validate the suspicious paths in a VM.
The target I looked at was:
- Driver: ElbyCDIO.sys
- Version: 6.1.5.0
- Description: ElbyCD Windows x64 I/O driver
- Device: \Device\ElbyCDIO
- User-mode path: \\.\ElbyCDIO
- SHA256: 899D410F591B6B56BC3C1BE655072BF7920CB2F347A1BEB027DCD29994AA3AB9
The first thing I usually do with a Windows kernel driver is boring but important: find the device object, the symbolic link, and the dispatch routines.
If a driver exposes a device to user mode, the next questions are simple:
- Can a low-privileged user open the device?
- Which dispatch routines are reachable?
- Which IOCTLs touch user buffers, handles, or lower devices?
In Ghidra, the initialization routine showed the driver creating the device and symbolic link for ElbyCDIO. More importantly, it registered a device-control dispatch routine. That dispatch routine later became the main map for the rest of the research.
The IOCTL dispatch function was:
- FUN_000134e0

ElbyCDIO creates the \Device\ElbyCDIO device object, exposes it through \DosDevices\ElbyCDIO, and registers FUN_000134e0 as the device-control dispatch routine.
At this point, my rough plan was:
- Confirm the user-mode device path.
- Check whether a standard user can open \.\ElbyCDIO.
- Reverse the IOCTL dispatch function.
- Identify cases that touch user buffers, handles, or lower devices.
- Validate only the promising candidates inside a VM.
Mapping the IOCTL handler
After confirming the exposed device and the dispatch routine, I moved into FUN_000134e0.
This function was the main switch-like IOCTL dispatcher. It pulled values from the current IRP stack location, checked buffer lengths, and branched based on the IOCTL code.
The important values were:
- InputBufferLength
- OutputBufferLength
- IoControlCode
- Type3InputBuffer
- UserBuffer
One detail stood out early: the driver was using METHOD_NEITHER style IOCTLs in multiple places. That means the driver often deals directly with user-mode pointers.
So the main questions became:
- Does it correctly probe user buffers?
- Does it ever write to a buffer that was only probed for read?
- Does it return kernel pointers back to user mode?
- Does it open or proxy access to lower device objects?
The IOCTL handler had a short generic probing block. If the IOCTL method was METHOD_NEITHER, it called:
- ProbeForWrite(UserBuffer, OutputBufferLength, 1)
- ProbeForRead(Type3InputBuffer, InputBufferLength, 1)
At first glance, this looks reasonable. But generic probing is only safe if the later IOCTL-specific code uses the buffers consistently.
For example, if the input buffer is only probed for read, the driver should not later write into that same input buffer.
The first interesting path was shared by two IOCTLs:
- 0x22E007
- 0x22E08B
Both required an input buffer of 0x136 bytes and an output buffer of at least 8 bytes. If those checks passed, the handler called:
- FUN_00012b10
At this point, I did not know exactly what FUN_00012b10 was doing. But the shape was interesting enough: a large user-controlled input structure was being passed into a helper function, and the IOCTL handler had only probed that input buffer for read.
So I followed the call.

The main IOCTL dispatcher checks METHOD_NEITHER buffers and routes 0x22E007 / 0x22E08B into FUN_00012b10.
Bug 1 — Local DoS through input-buffer write
Inside FUN_00012b10, the previous probing logic immediately became suspicious.
The helper writes directly into the input buffer:
*(byte *)(UserOpenRequest + 0x121) = 0;
This is the important part: the buffer passed into this function is the same Type3InputBuffer that the IOCTL handler previously probed only for read.
The flow looked like this:
0x22E007 / 0x22E08B
→ METHOD_NEITHER input buffer
→ ProbeForRead(Type3InputBuffer, 0x136, 1)
→ FUN_00012b10(Type3InputBuffer, UserBuffer)
→ Type3InputBuffer[0x121] = 0
That is a classic mismatch. The driver treats a user input buffer as read-only during probing, but later writes into it.

FUN_00012b10 writes a null byte into the caller-controlled input buffer at offset 0x121.
I first validated this with a writable user buffer. I filled the input buffer with 0x41 bytes, sent the IOCTL, and checked whether byte 0x121 changed.
It did. The driver changed:
Input[0x121] = 0x41
into:
Input[0x121] = 0x00
So the write was real.
The next question was whether this could be turned into a crash. Since the driver only probes the input buffer for read, a read-only user page should pass ProbeForRead, but the later write should fault when the driver executes:
input[0x121] = 0;
To verify that, I allocated a user-mode input buffer, filled it with data, changed the page protection to read-only, and then called 0x22E007 with:
- InputBufferLength = 0x136
- OutputBufferLength = 8
The VM crashed with SYSTEM_SERVICE_EXCEPTION.
The crash dump confirmed that the fault happened inside ElbyCDIO.sys, not somewhere unrelated. More importantly, the crash landed on the exact write instruction:
mov byte ptr [rcx+121h], sil
The driver was trying to write a single byte into the caller-controlled input buffer at offset 0x121. A buffer that should have been handled safely was being written to in a way that was easy to break by changing its page protection.
That turned the path from “maybe suspicious” into a real, reproducible local denial-of-service bug.


WinDbg confirms the crash inside ElbyCDIO.sys at the instruction writing to RCX + 0x121.
The root cause was simple:
The driver probes the METHOD_NEITHER input buffer for read, but later writes into the same user-controlled input buffer.
Bug 2 — Kernel context pointer disclosure through 0x22E03F
After confirming the local crash, I continued walking the IOCTL handler instead of stopping at the first bug.
The next case that stood out was:
- 0x22E03F
This IOCTL accepted a user-supplied handle and returned 8 bytes back to the caller.
The dispatcher side was simple:
- InputBufferLength >= 8
- OutputBufferLength >= 8
If both checks passed, the handler called:
- FUN_00013230
At first, this looked like a normal “create context” operation. The user gives the driver a handle, the driver resolves it, stores some internal state, and returns something back.
But the important question was:
What exactly is returned to user mode?

IOCTL 0x22E03F accepts an 8-byte input handle and returns an 8-byte value produced by FUN_00013230.
Following FUN_00013230 made the behavior clear.
The driver allocates a kernel pool object and clears 0x78 bytes:
- ExAllocatePool(…)
- memset(context, 0, 0x78)
Then it calls ObReferenceObjectByHandle on the user-supplied handle. This resolves a user-mode file handle into a kernel FileObject.
This specific handle resolution uses UserMode for the access mode, so this part is not an access-check bypass by itself. The user still needs to provide a handle they are allowed to open.
After that, the driver stores internal state in the newly allocated context, including the referenced FileObject and the related lower DeviceObject. Then it links the context into an internal list protected by a mutex.
So far, this looks like a normal driver context-management pattern.
The last step is the problem:
*Output = context;
Instead of returning a small opaque ID, index, or randomly generated handle, the driver returns the raw kernel pool address of the context object back to user mode.
That turns 0x22E03F into a kernel pointer disclosure.

FUN_00013230 allocates a kernel context object, links it into the driver’s internal list, and returns the raw context pointer back to user mode.
I validated this from user mode with a simple test:
- Open \.\ElbyCDIO.
- Open a normal temporary file.
- Send the file handle to IOCTL 0x22E03F.
- Read the 8-byte output value.
- Free the returned context with IOCTL 0x22E043.
The returned value looked like this:
0xFFFFAF0E…
That is consistent with a kernel-space pointer range on x64 Windows. More importantly, the same value was later accepted by other IOCTLs as a context token.
So the returned token was not a harmless random ID. It was the actual address of a kernel context object allocated by the driver.
The confirmed behavior was:
Low-privileged user
→ IOCTL 0x22E03F
→ kernel pool pointer disclosure
This does not give direct privilege escalation by itself. But it is still a useful information disclosure primitive, especially when chained with memory corruption or use-after-free style bugs.

Dynamic validation of IOCTL 0x22E03F. The driver returns an 8-byte kernel-looking context pointer and later accepts the same value through IOCTL 0x22E043 as a context token.
Bug 3 — ZwCreateFile access-control bypass through 0x22E007
After the crash and the pointer leak, I went back to FUN_00012b10.
At first, this function looked like a helper that takes a user-supplied path, does some basic filtering, and opens something on behalf of the caller.
The input structure was not documented, but by looking at the offsets passed into ZwCreateFile, the structure became clear enough:
- +0x000 ASCII NT object path
- +0x122 DesiredAccess
- +0x126 ShareAccess
- +0x12A CreateDisposition
- +0x12E CreateOptions
- +0x132 FileAttributes
The important part was the ZwCreateFile call.
The driver was taking both the object path and the requested access mask from the user-controlled input buffer. Then it called ZwCreateFile from kernel mode.
That immediately raised a question:
Does the driver force the access check to be performed as the original user?
In Windows kernel code, this matters a lot. If a driver opens an object on behalf of a user, it should not accidentally use its own kernel privilege to bypass the security descriptor of the target object.
In this function, the OBJECT_ATTRIBUTES structure was initialized with:
- Attributes = 0x40
That is OBJ_CASE_INSENSITIVE.
What I did not see was:
OBJ_FORCE_ACCESS_CHECK = 0x400
That was the important missing piece.
So the situation looked like this:
User supplies:
- NT object path
- DesiredAccess
- ShareAccess
- CreateDisposition
- CreateOptions
- FileAttributes
Driver performs:
- ZwCreateFile(…) from kernel mode
- without OBJ_FORCE_ACCESS_CHECK
That means the driver may open protected device objects for a low-privileged caller, even if the same caller would get STATUS_ACCESS_DENIED when opening the object directly.
At this point, I had a new hypothesis:
ElbyCDIO may act as a privileged open proxy.

FUN_00012b10 builds an OBJECT_ATTRIBUTES structure with Attributes = 0x40 and calls ZwCreateFile using user-controlled access and create parameters from the input buffer.
Testing the privileged open hypothesis
To test this properly, I needed a baseline.
If a low-privileged user could already open the target objects directly, then ElbyCDIO returning a handle would not be very interesting.
So I first tested the direct path from the same low-privileged user context:
- \Device\RaidPort0
- \Device\RaidPort1
I tried to open both with NtCreateFile and FILE_ALL_ACCESS.
The result was exactly what I expected:
- \Device\RaidPort0 + FILE_ALL_ACCESS → STATUS_ACCESS_DENIED
- \Device\RaidPort1 + FILE_ALL_ACCESS → STATUS_ACCESS_DENIED
This confirmed that the target device objects were protected from the current low-privileged user context.

Direct NtCreateFile from a low-privileged process fails with STATUS_ACCESS_DENIED when trying to open \Device\RaidPort0 and \Device\RaidPort1 with FILE_ALL_ACCESS.
Then I tested the same targets through ElbyCDIO’s 0x22E007 path.
This time, the result changed completely.
The same low-privileged process was able to obtain valid handles to the same \Device\RaidPort* objects. I also queried the returned handles with NtQueryObject, because I wanted to confirm what access was actually granted.
The result showed:
- RequestedAccess = 0x001F01FF
- GrantedAccess = 0x001F01FF
That is FILE_ALL_ACCESS.
So the comparison became very clear:
Direct NtCreateFile:
- \Device\RaidPort0 + FILE_ALL_ACCESS → STATUS_ACCESS_DENIED
- \Device\RaidPort1 + FILE_ALL_ACCESS → STATUS_ACCESS_DENIED
Via ElbyCDIO 0x22E007:
- \Device\RaidPort0 + FILE_ALL_ACCESS → SUCCESS, GrantedAccess = 0x001F01FF
- \Device\RaidPort1 + FILE_ALL_ACCESS → SUCCESS, GrantedAccess = 0x001F01FF
At that point, this was no longer just a crash bug or a small information leak. The driver was acting as a privileged open proxy.
A low-privileged user could ask ElbyCDIO to open protected kernel device objects, and the driver would return a fully granted handle back to user mode.
The root cause was the combination of:
- User-controlled NT object path
- User-controlled DesiredAccess
- Kernel-mode ZwCreateFile
- Missing OBJ_FORCE_ACCESS_CHECK
That is a classic confused-deputy style issue.

The same low-privileged user can obtain valid \Device\RaidPort* handles through ElbyCDIO. NtQueryObject confirms that the returned handles were granted with FILE_ALL_ACCESS.
The returned handle is usable
Getting a handle back was already interesting, but I also wanted to make sure the returned handle was actually usable.
So I sent a few safe read/query-style IOCTLs to the returned RaidPort handle. I avoided destructive or write-like operations and only used queries such as SCSI inquiry, capabilities, and address checks.
The result confirmed that the handle was not just a meaningless value returned by the driver. It was a real handle into the lower storage stack.
For example, using the handle returned for \Device\RaidPort1, IOCTL_SCSI_GET_INQUIRY_DATA completed successfully and returned identifiable device information:
NVMe VMware Virtual N1.3
That made the impact much clearer.
The bug was not only:
“the driver returns a handle”
It was:
“a low-privileged user can obtain a protected RaidPort handle through ElbyCDIO
and use that handle to query the underlying storage stack”
This is why I consider the ZwCreateFile path the strongest issue in this driver. The local crash is easy to demonstrate, and the kernel pointer leak is useful information disclosure, but the privileged open behavior crosses an access-control boundary.

The returned RaidPort handle is usable. A low-privileged process can send safe read/query IOCTLs through the handle and receive storage stack information.
Behavior 4 — Lower-device IOCTL proxy through 0x22E04F
After confirming that the returned RaidPort handle was real and usable, I kept following the context-token based IOCTLs.
One case stood out:
- 0x22E04F
This IOCTL takes a small user-controlled structure:
- +0x00 Context token
- +0x08 Lower IOCTL code
- +0x0C Input buffer pointer
- +0x14 Input buffer length
- +0x18 Output buffer pointer
- +0x20 Output buffer length
The first field is the context token returned by 0x22E03F. As shown earlier, that token is actually a kernel context pointer.
The driver validates the token against its internal list. If the token is accepted, the driver takes the stored lower DeviceObject from the context and builds a new device-control request for that device.
The important call is:
IoBuildDeviceIoControlRequest(…)
The lower IOCTL code, input buffer, input length, output buffer, and output length all come from the user-controlled proxy structure.
The simplified flow looks like this:
User supplies context token
- ElbyCDIO validates token
- ElbyCDIO reads DeviceObject from the context
- User supplies lower IOCTL code and buffers
- ElbyCDIO builds an IRP
- ElbyCDIO sends it to the lower device with IofCallDriver
This makes 0x22E04F a lower-device IOCTL proxy.
By itself, this does not automatically mean privilege escalation. The impact depends on what lower device is targeted and which IOCTLs are reachable. I did not want to overclaim this as SYSTEM or code execution.
But combined with the previous bug, the behavior is still important:
Low-privileged user
- obtains protected RaidPort handle through ElbyCDIO
- creates an Elby context for that handle
- sends selected lower-device IOCTLs through ElbyCDIO
In my validation, I only used safe read/query-style IOCTLs. The goal was not to perform destructive storage operations, but to confirm that the proxy path really reached the lower storage stack.

IOCTL 0x22E04F validates a context token, builds a lower-device IOCTL request with user-controlled parameters, and forwards it with IofCallDriver.
Confirming the proxy behavior
The static view made the design pretty clear, but I still wanted to validate the behavior dynamically.
The test flow was:
- 0x22E007 → open lower device and get a handle
- 0x22E03F → create an Elby context from that handle
- 0x22E04F → use the context to send IOCTLs to the lower device
- 0x22E043 → free the context
For the RaidPort1 path, I first used ElbyCDIO to open the protected device object through 0x22E007. Then I created a context from the returned handle using 0x22E03F.
The driver returned another kernel-looking context token, which was then accepted by 0x22E04F.
Using that token, I sent a safe query IOCTL through ElbyCDIO’s lower-device proxy path:
IOCTL_SCSI_GET_INQUIRY_DATA
The call completed successfully and the proxied output contained recognizable storage information:
NVMe VMware Virtual N1.3
This confirmed that 0x22E04F was not just a dead or unused code path. It could actually forward a lower-device IOCTL using the context created from a handle returned by ElbyCDIO.
Again, I kept this validation intentionally read-only. I did not test destructive storage operations, write-like SCSI operations, or anything that could damage the VM disk. The purpose was only to prove reachability and behavior.
The final chain looked like this:
Low-privileged user
- ElbyCDIO opens \Device\RaidPort1 with FILE_ALL_ACCESS
- ElbyCDIO creates a context token from that handle
- ElbyCDIO forwards a safe lower-device IOCTL through 0x22E04F
- storage stack information is returned to user mode
This does not prove privilege escalation by itself, but it does show a meaningful access-control problem. A low-privileged user reaches a protected lower storage device through a driver-controlled path that should have enforced stricter boundaries.

Dynamic validation of the 0x22E04F lower-device proxy. ElbyCDIO accepts the context token and forwards a safe query IOCTL to the lower storage device.
Conclusion
This research started as a simple IOCTL review, but ElbyCDIO turned out to expose several interesting behaviors.
The first issue was easy to validate: a local user could trigger a system crash by reaching a path where the driver wrote into a user-supplied input buffer. The crash dump confirmed that the fault happened inside ElbyCDIO.sys, directly on the instruction writing to the caller-controlled buffer.
The second issue was an information disclosure. IOCTL 0x22E03F returned a raw kernel context pointer to user mode and then accepted the same value later as a context token. That kind of kernel pointer disclosure is not code execution by itself, but it is still a useful primitive and should not be exposed to low-privileged callers.
The strongest finding was the ZwCreateFile behavior behind 0x22E007.
A low-privileged user could not directly open \Device\RaidPort0 or \Device\RaidPort1 with FILE_ALL_ACCESS. Direct NtCreateFile returned STATUS_ACCESS_DENIED, which was the expected baseline.
But through ElbyCDIO, the same user could request the same path and the same access mask, and the driver returned valid handles with:
GrantedAccess = 0x001F01FF
That changed the impact from a simple driver bug into an access-control boundary issue.
The returned handle was also usable. Safe read/query IOCTLs returned real storage stack information, and the 0x22E04F path confirmed that ElbyCDIO could also forward lower-device IOCTLs through its own context-token mechanism.
I am not claiming full privilege escalation here. I did not prove arbitrary kernel read/write, token manipulation, or code execution.
But I do think the confirmed behavior is meaningful:
Low-privileged user
- local system crash
- kernel context pointer disclosure
- protected RaidPort handle through ElbyCDIO
- lower-device query/proxy behavior
For me, the main lesson from this driver is that small design decisions in kernel code can combine into a much larger attack surface.
A raw pointer used as a token may look convenient.
A helper around ZwCreateFile may look harmless.
A lower-device IOCTL proxy may look like normal driver plumbing.
But when those pieces are reachable from low-privileged user mode, they become security-relevant very quickly.
That is why driver hunting is not only about finding the one obvious memory corruption bug. Sometimes the more interesting issue is how the driver connects user mode to privileged kernel objects, and whether it accidentally becomes a confused deputy in the process.
See you in the next article.

Electrical Engineering students at Telkom University Surabaya study advanced security vulnerability analysis of the Windows operating system kernel driver (**ElbyCDIO.sys v6.1.5.0**) based on research by Okan Kurtuluş. This study covers IOCTL handler mapping (*METHOD_NEITHER*), a Local DoS vulnerability caused by a *ProbeForRead* check mismatch, a kernel pointer leak via IOCTL *0x22E03F*, and an access control bypass flaw (*confused-deputy*) in the *ZwCreateFile* function—which fails to utilize *OBJ_FORCE_ACCESS_CHECK* when opening protected kernel objects. A deep understanding of operating system architecture, Windows kernel security, reverse engineering techniques, and software vulnerability mitigation equips graduates to design, analyze, and evaluate computing and network systems in a secure, reliable, efficient, and professional manner.