Last month I got a frantic message from a friend on a Windows security team. They had a SIEM alert firing on a system that, on paper, should have been fully patched against an old kernel driver bug. The PoC they were testing against (a known LPE in cldflt.sys, the Cloud Files Mini Filter) was supposed to be dead since 2020. It wasn't.
This happens more than people admit. A patch lands, an update rolls back during a feature upgrade, a hotfix gets superseded by something that quietly reintroduces old code, or the original fix was incomplete to begin with. Suddenly that CVE you closed out two years ago is alive again — and your detection rules forgot it existed.
Here's how I approach this problem now, after getting bitten by it on two separate engagements.
The root cause: patches are not as permanent as we pretend
When we talk about "patching" a vulnerability, we usually mean one of three things:
- A file on disk was replaced with a newer version
- A configuration value was changed to disable a feature
- A code path was guarded with a new check
All three can regress. File replacement gets undone by in-place OS upgrades, system restores, or driver rollbacks. Config values get reset by Group Policy refreshes. Code path guards can be bypassed if the patch only fixed one of several reachable entry points — which is exactly the failure mode behind a lot of "variant" CVEs. The recent MiniPlasma PoC that surfaced on GitHub Trending claims the original cldflt.sys fix is either reversible or was never complete. Whether that's universally true or environment-specific, the lesson is the same: don't trust that a CVE is closed just because Patch Tuesday said so.
This applies way beyond Windows. I've seen the same pattern with glibc rollbacks on Debian after an unrelated apt operation, and with a Node.js fix that got undone by a package-lock.json that pinned the vulnerable minor version.
Step 1: pin a known-good fingerprint for the fixed binary
The first thing I do when I close out a vulnerability is record what "patched" actually looks like on disk. For a driver or a system binary, that means at minimum: file version, file hash, and the digital signature timestamp.
Here's a small PowerShell snippet I keep in a repo for exactly this:
# Capture a fingerprint for a sensitive driver post-patch
$path = 'C:\Windows\System32\drivers\cldflt.sys'
$info = Get-Item $path | Select-Object `
@{N='Path'; E={$_.FullName}}, `
@{N='Version'; E={$_.VersionInfo.FileVersion}}, `
@{N='Hash'; E={(Get-FileHash $_.FullName -Algorithm SHA256).Hash}}, `
@{N='Signed'; E={(Get-AuthenticodeSignature $_.FullName).Status}}
# Persist to a baseline you can diff later
$info | ConvertTo-Json | Out-File -Encoding utf8 .\baseline-cldflt.json
The trick is treating that JSON file as a contract. Commit it. Sign it. Whatever your shop does for build artifacts, do that here. Two weeks later, run the same script and diff. If the hash changed and you don't have a change ticket explaining it, something rolled back.
Step 2: actually exercise the vulnerable code path
File hashes are necessary but not sufficient. The binary on disk can be identical and the behavior can still be wrong — for example, if a feature flag toggled on a previously disabled, unsafe code path.
So for the vulnerabilities I care most about, I write a tiny regression check that exercises the patched path safely. Not the full exploit — just a benign trigger that proves the guard is in place.
For a kernel driver IOCTL bug, that might look like sending the offending IOCTL with malformed input and asserting that the driver returns STATUS_INVALID_PARAMETER rather than crashing or silently accepting it. A sketch in Python with ctypes:
import ctypes
from ctypes import wintypes
# Open a handle to the driver's user-mode interface
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
# These are illustrative — real values depend on the driver
GENERIC_READ = 0x80000000
OPEN_EXISTING = 3
handle = kernel32.CreateFileW(
r'\\.\SomeDeviceName', # the symlink the driver exposes
GENERIC_READ,
0, None, OPEN_EXISTING, 0, None,
)
if handle == wintypes.HANDLE(-1).value:
raise OSError(ctypes.get_last_error())
# Send a deliberately malformed buffer the patch is supposed to reject
bad_input = b'\x00' * 4 # too small on purpose
out_buf = ctypes.create_string_buffer(64)
returned = wintypes.DWORD(0)
ok = kernel32.DeviceIoControl(
handle, 0xDEADBEEF, # the IOCTL code under test
bad_input, len(bad_input),
out_buf, ctypes.sizeof(out_buf),
ctypes.byref(returned), None,
)
# A patched driver should refuse this. Unpatched, it may misbehave.
assert not ok, 'Driver accepted malformed input — patch may be missing'
Obvious caveats: only run this in an isolated test VM, only against software you own, and only when you actually understand the failure mode. The point isn't to weaponize the bug — it's to confirm the guard hasn't disappeared.
Step 3: wire it into your monitoring loop
A one-time check is just a snapshot. The value is in running it on a schedule and alerting on drift. The minimum viable version is a scheduled task that re-fingerprints the file and compares against the baseline.
A reasonable cadence I've landed on:
- Hash + version diff: every boot, plus daily
- Behavioral regression check: weekly, in a canary VM that mirrors prod
- Full re-audit: after every cumulative update, feature upgrade, or driver install
If you're on Linux, auditd with a watch on the file plus a systemd timer running a hash comparison gets you most of the way there. On Windows, Sysmon's FileCreate events on driver paths catch a lot, and you can layer on Windows Defender Application Control rules so unauthorized driver versions can't load at all.
Prevention: assume regressions, design for them
A few habits that have saved me real pain:
-
Treat patch status as state, not as history. "We patched this in March" is meaningless. "This host's
cldflt.sysis currently version X, last verified at timestamp Y" is meaningful. - Keep a per-CVE regression test. Even a five-line script is better than nothing. It documents what the fix was supposed to do.
-
Subscribe to issue trackers for the things you patched. When someone publishes a new PoC against an old CVE — like what's happening with the
cldflt.syswriteups right now — you want to hear about it the same day, not next quarter. - Don't trust feature upgrades. Major OS upgrades have a long and honest history of replacing patched system files with older versions. Re-baseline after every one.
- Document the why. When you write the regression check, leave a comment explaining what the bug was and what the check proves. Six months from now, that comment is the only thing standing between you and someone deleting the test because "it doesn't seem to do anything."
The uncomfortable truth is that a CVE number is just a label. What actually protects you is the specific code or configuration that closes the bug — and that artifact has to be monitored like any other piece of infrastructure. Otherwise you're just hoping nothing rolled back, and hope is a terrible detection strategy.
Top comments (0)