Hello everyone,
In this post, I want to walk through a Windows kernel driver research session that started as a simple curiosity and slowly turned into something much more interesting.
The target was WinCDEmu’s kernel driver, BazisVirtualCDBus.sys.
WinCDEmu is a virtual CD/DVD mounting tool. On the surface, it looks like a normal utility: mount an ISO, get a virtual drive, use it like a physical CD-ROM. But underneath that simple user experience there is a kernel-mode driver responsible for handling virtual disk and SCSI-like operations.
That is exactly the kind of place I like to look at.
Not because every driver is vulnerable, and not because every old driver is automatically dangerous. But because Windows kernel drivers often expose complex interfaces to user mode, and sometimes those interfaces trust input more than they should.
This research ended up showing:
- a low-privileged kernel memory disclosure,
- kernel pointer disclosure,
- disclosure of apparent stale or otherwise unintended kernel-side data,
- a READ(10)-based length mismatch,
- end-to-end validation that a low-privileged user-supplied kernel-space DataBuffer value reaches the internal copy helper unchanged,
- independent before-and-after confirmation of the helper’s copy semantics using the normal user-mode destination,
- a completed kernel-space write into the current process token privilege bitmaps,
- activation of SeTcbPrivilege, SeCreateTokenPrivilege, SeSystemProfilePrivilege, and other token privileges,
- a completed local privilege escalation chain to NT AUTHORITY\SYSTEM through a temporary service,
- and a separate local denial-of-service issue through IOCTL 0x001B0013.
In the isolated lab, a low-privileged process used the vulnerable SPTD copy-back path to overwrite its own token privilege bitmaps, enabled SeTcbPrivilege and other powerful privileges, and launched a temporary service that returned an NT AUTHORITY\SYSTEM shell.
Why I looked at this driver
I have been spending time building a workflow for Windows kernel driver research. The idea is simple: take real drivers, map their attack surface, identify IOCTL handlers, then validate suspicious paths with WinDbg and controlled PoCs.
WinCDEmu was interesting for a few reasons:
- it installs a kernel driver,
- it exposes virtual CD/DVD devices,
- it handles SCSI pass-through style requests,
- and it is the kind of software that may remain installed for years.
Virtual device drivers are especially interesting because they often sit between user-mode tools and kernel-mode storage paths. That means they may parse structures, copy buffers, emulate hardware behavior, and translate requests.
That is where bugs like to hide.
Target component
The driver I analyzed was:
- Driver: BazisVirtualCDBus.sys
- Product: WinCDEmu
- Role: Virtual CD/DVD bus driver
- Relevant IOCTL path: IOCTL_SCSI_PASS_THROUGH_DIRECT / 0x0004D014
- Test environment: Windows 11 x64
- WinCDEmu version: 4.1
- Architecture: x64

FileVersion and ProductVersion from Get-Item on BazisVirtualCDBus.sys confirming driver version 4.1.

BazisVirtualCDBus.sys module metadata and image path from the Windows 11 test VM.

Authenticode signature information for BazisVirtualCDBus.sys.
The driver was signed and loaded normally on the test VM. This is important because the research was performed against a real installed driver, not a modified test binary.
Test context
All interactions with the vulnerable device, including the disclosure tests and the kernel write request, were performed from a low-privileged local process. Administrative access and WinDbg were used separately for lab instrumentation, address validation, and retrieval of the token address supplied to the current proof of concept.
This matters because a kernel bug that requires administrator access is still important, but the impact is very different. In this case, the vulnerable path could be reached from a normal user context through a WinCDEmu-mounted virtual drive and was ultimately used to modify security-sensitive token state.

Low-privileged test context and WinCDEmu-mounted virtual CD/DVD devices.
The test was performed from a low-privileged user context against WinCDEmu-mounted virtual CD/DVD devices.
First step: finding the interesting IOCTL path
The important IOCTL path was:
0x0004D014
This corresponds to the SCSI pass-through direct path. At runtime, WinDbg showed execution reaching:
BazisVirtualCDBus+0x16cb2
immediately before a call to an internal copy helper:
BazisVirtualCDBus+0x17a40
That copy site became the center of the entire analysis.

Runtime breakpoint at the SCSI pass-through copy-back site immediately before the internal copy helper is called.
This breakpoint confirmed the runtime location of the copy-back operation. The driver is stopped at BazisVirtualCDBus+0x16cb2, directly before calling the internal helper at BazisVirtualCDBus+0x17a40.
Root cause: trusting the embedded DataBuffer pointer
The root cause is not simply “bad memcpy”.
The actual issue is that the driver trusts fields inside the SCSI_PASS_THROUGH_DIRECT structure.
In the SCSI pass-through handler, the driver loads:
- DataBuffer from [rdx+0x18]
- DataTransferLength from [rdx+0x0C]
- DataIn from [rdx+0x08]


The handler reads DataBuffer, DataTransferLength, and DataIn directly from the SCSI_PASS_THROUGH_DIRECT request structure.
This shows the first part of the root cause: the embedded DataBuffer pointer, transfer length, and direction flag are read directly from the SPTD request structure.
Later, in the copy-back path, those values are passed directly into the copy helper:
- rcx = DataBuffer
- rdx = kernel transfer buffer
- r8 = DataTransferLength

The handler passes DataTransferLength, the kernel transfer buffer, and DataBuffer directly into the internal copy helper.
The important part is this: DataBuffer is a secondary pointer embedded inside a request structure. The outer request may be handled normally, but the embedded pointer still needs strict validation before the driver writes to it.
That validation is the weak point here.
The copy helper
The helper at:
BazisVirtualCDBus+0x17a40
behaves like an optimized memmove-style routine.
It uses rcx as the destination, rdx as the source, and r8 as the length. Inside the helper, there are direct writes to [rcx].

The important detail is that rcx is used as the destination pointer. The helper performs raw writes to [rcx] and no user/kernel address validation is visible inside this helper.
Examples include:
- mov byte ptr [rcx], al
- mov word ptr [rcx], ax
- mov dword ptr [rcx], eax
- mov qword ptr [rcx], rax
I did not observe user/kernel address validation inside this helper. The helper itself behaves like a raw copy routine; any validation would have needed to happen before this call.
Finding 1 — Kernel memory disclosure via INQUIRY
The first confirmed issue was an information disclosure using a crafted INQUIRY request.
The idea was simple:
- CDB allocation length: normal INQUIRY response length
- DataTransferLength: larger than the real response
If the driver copied only the actual response length, the rest of the user buffer would remain filled with my marker bytes.
But that is not what happened. The returned buffer contained:
- a valid INQUIRY response,
- followed by apparent stale or otherwise unintended kernel-side bytes,
- sometimes including kernel-pointer-like values.
The valid response included:
Bazis WinCDEmu 0001
After that, data continued past the expected response.

The valid INQUIRY response ends around 0x24 bytes, but the returned buffer continues with non-marker data. Because the user buffer was pre-filled with 0x41 marker bytes, the non-0x41 tail shows that additional data was copied back by the driver. This indicates that the driver copied more data than the actual response required.
Validating the INQUIRY leak in WinDbg
The user-mode PoC output is useful, but I also wanted to see the driver state at the copy site.
At the breakpoint, the source buffer contained the normal INQUIRY response, while r8 contained the larger requested transfer length.
That means the driver was preparing to copy more bytes than the real response required.

WinDbg view of the INQUIRY copy-back path showing the kernel source buffer and requested copy length before the internal copy helper is called.
At the copy-back site, r8 contains 0x100 even though the normal INQUIRY response is much shorter. The source buffer pointed to by rdx contains the expected Bazis / WinCDEmu response followed by additional kernel-side bytes.
Finding 2 — Kernel pointer disclosure
The leaked tail was not just random-looking garbage. Some returned values looked like canonical kernel virtual addresses.

The leaked INQUIRY tail contains kernel-pointer-like values returned to the low-privileged user-mode process.
To validate this, I took one of the leaked values and checked it in WinDbg. The address resolved to a valid kernel virtual address with a writable kernel PTE and fell inside an allocated CMkb pool block.

WinDbg validation showing that one of the leaked pointer-like values resolves to a valid kernel pool region.
This matters because kernel pointer disclosure can weaken KASLR assumptions and provide useful information for further local kernel exploitation research.The pointer disclosure is independently significant because it weakens address-randomization assumptions and may support reliable kernel object targeting. The LPE proof used a token address supplied and validated separately in WinDbg; it did not automate token discovery from the leaked values.
Finding 3 — READ(10) mismatch disclosure
After confirming the INQUIRY leak, I wanted to know whether this was only an INQUIRY-specific bug or a more general copy-back problem.
So I created a small ISO containing a controlled marker at a known LBA:
WHATCTRL_LBA_0021_BAZIS_RESEARCH_MARKER_
Then I sent a READ(10) request:
- LBA: 21
- Blocks: 1
- Expected transfer: 0x800 bytes
- DataTransferLength: 0x1000 bytes
The result was exactly what I was looking for.

Safe user-mode READ(10) mismatch PoC showing attacker-controlled ISO data followed by copied tail bytes beyond the expected READ(10) response.
The READ(10) command requested one 0x800-byte block from the ISO image, but DataTransferLength was set to 0x1000. The first 0x800 bytes contain the controlled marker. After offset 0x800, the original 0x41 marker bytes were overwritten, showing that the copy-back length followed DataTransferLength instead of the actual one-block READ(10) response size.
The first 0x800 bytes contained my controlled ISO marker. The region after offset 0x800 no longer contained the original 0x41 marker bytes, showing that the returned data extended beyond the valid one-block READ(10) response. A separate adjacent-sector control test was then used to determine whether this tail came from the backing ISO or from kernel-resident memory.
Ruling out adjacent-sector data
One possible explanation was that DataTransferLength = 0x1000 caused the storage path to return both LBA 21 and the adjacent LBA 22 sector. To test that possibility, I read LBA 22 directly from the backing ISO image. LBA 22 begins at file offset 0xB000, and all 0x800 bytes in that sector were zero-filled.
The control read produced the following complete-sector result:
- [Raw ISO control] LBA 22 offset: 0x0000B000
- [Raw ISO control] Sector length: 0x800 bytes
- Non-zero bytes in complete LBA 22 sector: 0
This textual control output is important because the screenshot only shows the beginning of the sector; the full 0x800-byte sector check is what rules out adjacent-sector data.
In a subsequent repeat run, the user buffer did not contain a zero-filled second sector. The entire region from offset 0x800 through 0xFFF was overwritten with structured data containing numerous non-zero values and canonical kernel-pointer-like values. The first unexpected byte appeared exactly at +0x800, the last appeared at +0xFFF, and the output contained 108 values matching the canonical form of Windows kernel virtual addresses.
Observed repeat-run metrics:
- Changed bytes after the expected end: 2048
- First tail change: +0x800
- Last tail change: +0xFFF
- Kernel-pointer-like values: 108


Repeat-run READ(10) output showing that the entire second 0x800-byte region was overwritten and contained numerous canonical kernel-pointer-like values.

Direct read of the backing ISO image showing that LBA 22, located at file offset 0xB000, is entirely zero-filled and does not match the unexpected READ(10) tail.
Because the raw contents of ISO LBA 22 were entirely zero and did not match the returned tail, the additional data did not originate from the adjacent ISO sector. The result is consistent with disclosure of kernel-resident contents beyond the valid READ(10) response.
The changed-byte count in the first 0x800-byte region was lower than 0x800 because the destination was pre-filled with 0x41 and the controlled ASCII marker itself contains the letter ‘A’, which is also 0x41. That counter measures differences from the marker value; it is not the actual transfer length. That shows the issue is broader than INQUIRY.
The tail after the READ(10) response
The interesting part starts after offset 0x800.
That is where the real one-block READ(10) response should end. The returned user buffer nevertheless continued past that boundary. In the repeat run described above, the entire second 0x800-byte region was overwritten with structured data containing numerous canonical kernel-pointer-like values, while a direct read of ISO LBA 22 showed that the adjacent sector was completely zero-filled.

Boundary view showing that the READ(10) response should end at offset 0x800, but the returned buffer continues past that boundary.
Finding 4 — WHERE / WHAT / SIZE at the copy boundary
At this point, the information disclosure was already confirmed.
But I wanted to understand whether the same path could become more dangerous.
The copy helper takes three important values:
- rcx = destination
- rdx = source
- r8 = length
So the question became:
Can I influence WHERE, WHAT, and SIZE at the same copy boundary?
At the copy-back breakpoint, WinDbg showed the three values clearly: rcx contained the supplied DataBuffer pointer, rdx pointed to controlled READ(10) data from the mounted ISO, r8 contained the supplied DataTransferLength.

WinDbg breakpoint showing WHERE, WHAT, and SIZE at the copy boundary before the internal copy helper is called.
At the copy-back boundary, rcx holds the destination pointer, rdx points to the kernel transfer buffer containing controlled READ(10) data from the mounted ISO, and r8 holds the requested DataTransferLength.
This establishes the complete WHERE / WHAT / SIZE boundary: the destination, source, and length are all influenced at the helper call. The token-overwrite test allowed the helper to complete against a live kernel object and confirmed that this boundary provides an actual kernel-space write primitive.
The register state shown here captures the setup for the write primitive. The completed token write and resulting privilege escalation are documented in Finding 6.
Mapping the registers back to the SPTD request
The register view is not enough by itself. I also wanted to show that these values came from the SCSI pass-through request structure.
At the initial SPTD parsing point, the structure dump showed:
- DataIn = 1
- DataTransferLength = 0x1000
- DataBuffer = supplied destination pointer
- CDB[0] = 0x28 / READ(10)

WinDbg dump of the SCSI_PASS_THROUGH_DIRECT request showing DataIn, DataTransferLength, DataBuffer, and READ(10) CDB fields.
This connects the later register state back to the original request. The destination pointer comes from the embedded DataBuffer field, the copy length comes from DataTransferLength, and the CDB identifies the command as READ(10).
Proving WHAT control
For the source side, the debugger showed that rdx pointed to a buffer containing my controlled ISO marker:
WHATCTRL_LBA_0021_BAZIS_RESEARCH_MARKER_

WinDbg source-buffer dump showing controlled READ(10) data from the mounted ISO image.
At the copy-back boundary, rdx points to the kernel transfer buffer. Dumping that buffer shows the controlled WHATCTRL marker read from the mounted ISO image, confirming source-side control for the copy operation.
Important validation boundary
Debugger-assisted destination-register changes were excluded from the exploitability evidence because altering a register at a breakpoint can create a no-op or otherwise distort the real request flow. All security-impact claims below rely on values supplied through the user-mode SPTD request itself.
The pointer-validation test therefore supplied a kernel virtual address through the SPTD DataBuffer field from a low-privileged user-mode request and traced that value to the internal copy helper without register manipulation.
Finding 5 — Kernel-Space DataBuffer Pointer Flow
Finding 5 examines whether the driver validates the DataBuffer pointer before using it as the copy destination.
End-to-end pointer-flow validation
To test this without debugger manipulation, I supplied a kernel virtual address directly as DataBuffer in the SPTD request from a low-privileged user-mode process. The target address (ffffd7080cd7ebe0) came from the kernel address range exposed by the disclosure and was checked separately in WinDbg before use.
I placed a single breakpoint at the copy helper entry (BazisVirtualCDBus+0x17a40) and ran the modified PoC. When the breakpoint fired:
1: kd> r rcx, rdx, r8 rcx=ffffd7080cd7ebe0 ← supplied kernel VA, unmodified rdx=ffffd7080e77e000 ← kernel transfer buffer r8 =0000000000001000 ← DataTransferLength
1: kd> k BazisVirtualCDBus+0x17a40
BazisVirtualCDBus+0x16cb7
BazisVirtualCDBus+0x1928
BazisVirtualCDBus+0x14069
nt!PspSystemThreadStartup+0x5a
nt!KiStartSystemThread+0x34
rcx held the kernel VA exactly as supplied. In the traced path, I did not observe an interposing call to MmProbeAndLockPages, ProbeForWrite, or IoAllocateMdl before the supplied DataBuffer value reached the helper. Execution was intentionally paused at helper entry for this specific pointer-flow test. In the separate impact test documented in Finding 6, the request was allowed to complete and the kernel-space write was confirmed against the current process token.
This shows that, in the tested request path, no user/kernel address validation was observed before the supplied DataBuffer value reached the internal copy helper as its destination argument.

Confirming the helper’s copy semantics
To independently confirm the behavior of the internal helper, I repeated the READ(10) test using the normal user-mode DataBuffer destination. Breakpoints were placed immediately before the helper call at BazisVirtualCDBus+0x16cb2 and immediately after it returned at BazisVirtualCDBus+0x16cb7.
Before the call, I saved the original destination address in a WinDbg pseudo-register. The destination contained only 0x41 marker bytes, while the kernel transfer buffer referenced by rdx contained the attacker-controlled WHATCTRL_LBA_0021_BAZIS_RESEARCH marker.
At helper entry:
- rcx = 00000189911b7108 destination
- rdx = ffffbb8805443000 kernel transfer buffer
- r8 = 0000000000001000 copy length
The original destination address was preserved so that the same location could be inspected after the helper returned.

Before-and-after WinDbg validation of the internal copy helper. The destination initially contains only 0x41 marker bytes, while the kernel source buffer contains the controlled READ(10) data. After the helper returns, the original destination contains the controlled marker.
After the helper returned, the original destination address contained the controlled WHATCTRL_LBA_0021_BAZIS_RESEARCH marker. The helper had advanced rcx from 00000189911b7108 to 00000189911b8108, a difference of exactly 0x1000 bytes, while r8 had reached zero.
This confirms the helper’s effective copy semantics: it consumes the length in r8 and copies from the kernel transfer buffer referenced by rdx to the destination referenced by rcx.
This normal user-mode destination test independently confirms the helper’s copy semantics. Combined with the live token overwrite in Finding 6, it shows that the same request-controlled primitive can modify kernel security state in practice.
Finding 6 — Token Privilege Bitmap Overwrite and Local Privilege Escalation
The pointer-flow and helper-semantics tests showed that the driver accepted a request-controlled destination and performed the copy. To validate meaningful security impact against a live kernel object, I targeted the current process token in the test environment.
Targeting the current process token
The write-only PoC accepted the current process token base address as a command-line argument and calculated targetVA = Token + 0x40. On the tested Windows 11 build 26100 system, this offset corresponded to the beginning of the token privilege bitmap structure. The offset and target address were checked in WinDbg before the test; they should be treated as build-specific rather than universal constants.
A controlled ISO was prepared so that LBA 21 began with 24 consecutive 0xFF bytes. The PoC then submitted a SCSI_PASS_THROUGH_DIRECT READ(10) request with the following security-relevant fields:
targetVA = Token + 0x40
DataIn = 1
DataTransferLength = 0x18
DataBuffer = targetVA
CDB[0] = 0x28 // READ(10)
LBA = 21
TransferBlocks = 1
The 0x18-byte copy length was deliberate. The token privilege state is represented by three consecutive 64-bit bitmaps: Present, Enabled, and EnabledByDefault. When the vulnerable copy-back path used the embedded kernel address as its destination, the first 24 bytes from the controlled READ(10) source were written over those bitmaps.
The write completed without debugger-assisted register modification. Before exploitation, the low-privileged process exposed only the normal limited privilege set. After the write, whoami /priv showed powerful privileges including SeTcbPrivilege, SeCreateTokenPrivilege, and SeSystemProfilePrivilege in the Enabled state.
.\lpe-exploit.exe \\.\Y: <CURRENT_PROCESS_TOKEN_ADDRESS>
whoami /priv

Before-and-after token privilege state. The low-privileged process initially has a restricted token; after the kernel write, multiple high-impact privileges are present and enabled.
Chaining SeTcbPrivilege to NT AUTHORITY\SYSTEM
Enabling token privileges demonstrated a useful semantic change. To confirm end-to-end SYSTEM impact, I compiled the TcbElevation proof of concept from mSameerMalik’s SeTcbPrivilege_escalation repository. The project uses SeTcbPrivilege together with SSPI manipulation to obtain privileged Service Control Manager access and execute a command through a temporary Windows service.
Reference implementation:
https://github.com/mSameerMalik/SeTcbPrivilege_escalation
I built a service-compatible executable named rev-2.exe for the SYSTEM execution proof. The program registers a ServiceMain entry point, starts a PowerShell reverse shell as the service process, and keeps the service running while the child process remains alive. In the isolated lab, the payload connected to 192.168.44.133 on TCP port 4444.
After the token privilege overwrite, the temporary service was launched with a randomized name:

Windows-side execution of the SeTcbPrivilege privilege-escalation chain using a randomized temporary service name and the custom service payload.
A listener was already running on the attacker-controlled Ubuntu VM.
The listener received a connection from the Windows test VM. Running whoami inside the returned shell produced nt authority\system, completing the privilege escalation chain from the starting low-privileged account.

SYSTEM proof of impact: the reverse shell created through the temporary service executes as NT AUTHORITY\SYSTEM.
Scope and reliability of the demonstrated chain
The demonstrated chain is a completed LPE proof of impact: the vulnerable IOCTL path performs an unvalidated kernel-space write that overwrites the current process token privilege bitmaps, enables SeTcbPrivilege, and allows the process to obtain an NT AUTHORITY\SYSTEM shell through a temporary service.
The current implementation has two build-specific dependencies. The Token + 0x40 offset was validated on Windows 11 build 26100 (24H2) and should be confirmed per target build. The token kernel address was retrieved using NtQuerySystemInformation(SystemExtendedHandleInformation) from a local administrator account; non-elevated processes receive a NULL object pointer due to KASLR protections. A fully low-privileged chain would require an independent kernel address disclosure primitive — a candidate already present in this driver, as documented in Finding 2.
Finding 7 — Local DoS via IOCTL 0x001B0013
During the research, I also found a separate local denial-of-service issue.
A low-privileged user was able to open the mounted virtual CD-ROM device and send IOCTL 0x001B0013 with no input or output buffer.
The request reached the driver from user mode and eventually crashed inside BazisVirtualCDBus.
The crash dump showed that execution stopped at BazisVirtualCDBus+0x154f5. At the time of the crash, rdx contained 0x001B0013, rax was NULL, and the faulting instruction attempted to read from [rax+0x18].

Crash dump showing a NULL pointer dereference in BazisVirtualCDBus after IOCTL 0x001B0013.

Windows bugcheck screen showing BazisVirtualCDBus.sys as the failed driver.
This crash path is separate from the privilege-escalation chain, but it shows that a low-privileged user can also crash the system through an insufficiently validated IOCTL path. CVE-2011-5202 was publicly described as a local system crash triggered through the unmount command in batchmnt.exe. I did not establish whether that historical issue ultimately reached the same internal handler, so the relationship between the two crash paths remains unresolved.
Conclusion
This driver audit demonstrates why secondary pointers in kernel interfaces are dangerous.
The vulnerable pattern was:
- The outer request structure is accepted.
- The embedded DataBuffer pointer is trusted.
- DataTransferLength is trusted.
- Kernel data is copied using those request-controlled values.
That pattern produced kernel memory disclosure, kernel pointer disclosure, READ(10)-based copied-tail exposure, control over the copy destination/source/length boundary, a live token privilege bitmap overwrite, activation of SeTcbPrivilege and other high-impact privileges, a completed NT AUTHORITY\SYSTEM shell chain, and a separate local denial-of-service condition.
The PoC still has reliability work remaining: the token address is supplied externally and the token offset is build-specific. Even with those limitations, the security impact is demonstrated. A low-privileged caller can reach the vulnerable driver path and cause attacker-controlled data to be written to a supplied kernel address. In the demonstrated lab chain, the supplied address targeted the current process token, although that address was obtained separately rather than discovered automatically by the low-privileged PoC.
The key lesson is simple:
When a kernel driver accepts a structure from user mode, validating the outer buffer is not enough.
Every embedded pointer and every length field must be treated as untrusted.
See you in the next article.
