<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mr.Cupcake</title>
    <description>The latest articles on DEV Community by Mr.Cupcake (@mrcupcake_78bc58aca0fb73).</description>
    <link>https://dev.to/mrcupcake_78bc58aca0fb73</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4217829%2F26d33cf0-21a0-41ab-b434-7a9c0555ce9c.png</url>
      <title>DEV Community: Mr.Cupcake</title>
      <link>https://dev.to/mrcupcake_78bc58aca0fb73</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrcupcake_78bc58aca0fb73"/>
    <language>en</language>
    <item>
      <title>I built a Windows driver vuln hunter in a day — and learned why heuristics lie</title>
      <dc:creator>Mr.Cupcake</dc:creator>
      <pubDate>Sat, 31 Oct 2026 02:58:23 +0000</pubDate>
      <link>https://dev.to/mrcupcake_78bc58aca0fb73/i-built-a-windows-driver-vuln-hunter-in-a-day-and-learned-why-heuristics-lie-ip</link>
      <guid>https://dev.to/mrcupcake_78bc58aca0fb73/i-built-a-windows-driver-vuln-hunter-in-a-day-and-learned-why-heuristics-lie-ip</guid>
      <description>&lt;p&gt;A few weeks ago I was fuzzing Windows kernel drivers and realized something&lt;br&gt;
uncomfortable: the &lt;em&gt;interesting&lt;/em&gt; bugs aren't the crashes you find by throwing&lt;br&gt;
bytes at an IOCTL. They're the ones that were &lt;strong&gt;designed in&lt;/strong&gt; — drivers that&lt;br&gt;
expose arbitrary kernel memory read/write to anyone who can open a device. The&lt;br&gt;
problem is spotting those by hand, across dozens of signed &lt;code&gt;.sys&lt;/code&gt; files, is&lt;br&gt;
slow.&lt;/p&gt;

&lt;p&gt;So I gave myself a constraint: build a &lt;strong&gt;static&lt;/strong&gt; driver vulnerability hunter in&lt;br&gt;
&lt;strong&gt;one day&lt;/strong&gt;, with no driver ever loaded (my analysis box is a cheap VPS that&lt;br&gt;
Defender quietly quarantines every BYOVD binary it sees). Here's what came out.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the tool does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DrvSniper&lt;/code&gt; takes a &lt;code&gt;.sys&lt;/code&gt; (local path or URL) and, without executing&lt;br&gt;
anything, does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Imports + strings.&lt;/strong&gt; It parses the PE import table &lt;em&gt;and&lt;/em&gt; scans every
ascii string for dangerous APIs. The string scan matters because a lot of
drivers resolve risky routines dynamically with
&lt;code&gt;MmGetSystemRoutineAddress&lt;/code&gt; — those never show up in the import table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IOCTL extraction.&lt;/strong&gt; It walks the &lt;code&gt;.text&lt;/code&gt; section with capstone and
collects &lt;code&gt;cmp reg, imm&lt;/code&gt; values that survive the IOCTL bit layout, then
flags any &lt;code&gt;METHOD_NEITHER&lt;/code&gt; code that has no &lt;code&gt;ProbeForRead&lt;/code&gt;/&lt;code&gt;ProbeForWrite&lt;/code&gt;
anywhere in the binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring.&lt;/strong&gt; Each dangerous primitive has a weight; a missing probe on a
&lt;code&gt;NEITHER&lt;/code&gt; IOCTL adds a bonus. The driver gets CRITICAL / HIGH / MEDIUM /
LOW.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Crucially, the driver is fetched and analyzed &lt;strong&gt;in memory only&lt;/strong&gt;, so Defender&lt;br&gt;
never sees a dropped &lt;code&gt;.sys&lt;/code&gt; on disk. That's what makes this safe to run on a&lt;br&gt;
normal Windows host.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo 1: Blackbone → CRITICAL
&lt;/h2&gt;

&lt;p&gt;Pointed at &lt;code&gt;BlackBoneDrv10.sys&lt;/code&gt;, the tool immediately lit up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[3] MmCopyVirtualMemory              arbitrary cross-process memory R/W
[3] MmMapLockedPagesSpecifyCache     maps arbitrary physical pages via MDL
[2] KeStackAttachProcess             attaches to an arbitrary process context
[2] PsLookupProcessByProcessId       resolves EPROCESS by PID
=&amp;gt; severity: CRITICAL (score 10)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That maps exactly to what manual RE shows: the &lt;code&gt;BBCopyMemory&lt;/code&gt; handler for&lt;br&gt;
IOCTL &lt;code&gt;0x803&lt;/code&gt; calls &lt;code&gt;MmCopyVirtualMemory(..., KernelMode, ...)&lt;/code&gt; on a&lt;br&gt;
user-supplied PID and address with &lt;strong&gt;no validation&lt;/strong&gt; — arbitrary R/W over any&lt;br&gt;
process, including &lt;code&gt;System&lt;/code&gt; (PID 4) and &lt;code&gt;lsass&lt;/code&gt;. Combined with a device object&lt;br&gt;
created without &lt;code&gt;FILE_DEVICE_SECURE_OPEN&lt;/code&gt;, any user process can open it.&lt;br&gt;
Vulnerable by design, and the tool found it in under a second.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo 2: WinDivert → LOW (the lesson)
&lt;/h2&gt;

&lt;p&gt;Then I ran it on &lt;code&gt;WinDivert64.sys&lt;/code&gt;, a legit networking driver I'd previously&lt;br&gt;
reversed and found clean. The &lt;em&gt;first&lt;/em&gt; version of my scorer screamed &lt;strong&gt;HIGH&lt;/strong&gt; —&lt;br&gt;
because WinDivert imports &lt;code&gt;MmMapLockedPagesSpecifyCache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's a false positive, and it's the interesting part. &lt;code&gt;MmMapLockedPagesSpecifyCache&lt;/code&gt;&lt;br&gt;
is used safely by thousands of clean drivers to map &lt;em&gt;their own&lt;/em&gt; buffers. My&lt;br&gt;
first instinct — "list every scary API and add up the points" — was wrong.&lt;br&gt;
The fix was &lt;strong&gt;weighting&lt;/strong&gt;: &lt;code&gt;MmCopyVirtualMemory&lt;/code&gt; is almost always an&lt;br&gt;
intentional, user-exposed primitive (weight 3), while&lt;br&gt;
&lt;code&gt;MmMapLockedPagesSpecifyCache&lt;/code&gt; is ambiguous and routinely safe (weight 1).&lt;/p&gt;

&lt;p&gt;After the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BlackBoneDrv10.sys  CRITICAL (10)
WinDivert64.sys     LOW      (2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hunter that flags the clean driver is worse than useless — it trains you to&lt;br&gt;
ignore it. Weighting signals instead of counting them is what made the tool&lt;br&gt;
trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is, and what it isn't
&lt;/h2&gt;

&lt;p&gt;This is &lt;strong&gt;triage&lt;/strong&gt;, not proof. A CRITICAL score means "look here first", not&lt;br&gt;
"this is exploitable". It doesn't model data flow, so it can't tell you&lt;br&gt;
whether a user-supplied address actually reaches a primitive — that's still&lt;br&gt;
manual RE. And &lt;code&gt;cmp&lt;/code&gt;-based IOCTL recovery misses dispatch via hash tables or&lt;br&gt;
lookup switches.&lt;/p&gt;

&lt;p&gt;But as a first-pass filter over a corpus of signed drivers, it does exactly&lt;br&gt;
what I wanted: it points me at the drivers worth my time and stays quiet on the&lt;br&gt;
ones that aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The code is small and dependency-light (&lt;code&gt;pefile&lt;/code&gt;, &lt;code&gt;capstone&lt;/code&gt;). Point it at any&lt;br&gt;
&lt;code&gt;.sys&lt;/code&gt; and see what it says — then go prove or disprove the score the old&lt;br&gt;
fashioned way.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next post: turning the CRITICAL hits into a real, documented finding — the&lt;br&gt;
gap between "this driver is dangerous" and "here is the exact IOCTL and&lt;br&gt;
struct that proves it."&lt;/em&gt;&lt;/p&gt;

</description>
      <category>windows</category>
      <category>kernel</category>
      <category>drivers</category>
      <category>security</category>
    </item>
    <item>
      <title>Comparison tracing made my fuzzer worse</title>
      <dc:creator>Mr.Cupcake</dc:creator>
      <pubDate>Fri, 30 Oct 2026 19:53:44 +0000</pubDate>
      <link>https://dev.to/mrcupcake_78bc58aca0fb73/comparison-tracing-made-my-fuzzer-worse-lo7</link>
      <guid>https://dev.to/mrcupcake_78bc58aca0fb73/comparison-tracing-made-my-fuzzer-worse-lo7</guid>
      <description>&lt;p&gt;Source and reproducible run: &lt;a href="https://gitlab.com/mscupcakegkill/wrfuzz" rel="noopener noreferrer"&gt;https://gitlab.com/mscupcakegkill/wrfuzz&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Comparison tracing made my fuzzer worse
&lt;/h1&gt;

&lt;p&gt;I built a coverage-guided fuzzer from scratch, without libFuzzer or AFL, to find out what each&lt;br&gt;
component actually contributes. Then I added comparison tracing — the mechanism libFuzzer is&lt;br&gt;
best known for — and the fuzzer stopped finding bugs completely.&lt;/p&gt;

&lt;p&gt;Not "found fewer". Zero, in 200,000 executions, with &lt;em&gt;lower&lt;/em&gt; coverage than before I added it.&lt;/p&gt;

&lt;p&gt;This is the story of why, because it is not the story you get from reading libFuzzer's source.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The fuzzer is about 300 lines of C: sanitizer coverage for feedback, a corpus of inputs that each&lt;br&gt;
reach somewhere new, and seven mutation operators applied in sequence. The target is a toy parser&lt;br&gt;
for &lt;code&gt;"WRM" &amp;lt;len&amp;gt; &amp;lt;payload&amp;gt;&lt;/code&gt;, where a payload starting with &lt;code&gt;ABCD&lt;/code&gt; counts as a bug.&lt;/p&gt;

&lt;p&gt;The seed is &lt;code&gt;WRM\x04wxyz&lt;/code&gt; — the first three bytes right, everything after wrong. To find the bug&lt;br&gt;
the fuzzer has to discover &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;D&lt;/code&gt; with no prior knowledge of what it is looking for.&lt;/p&gt;

&lt;p&gt;With coverage feedback alone it took &lt;strong&gt;124,573 executions&lt;/strong&gt;. That is bad. Random guessing would&lt;br&gt;
need 256⁴ ≈ 4.3 billion, so coverage bought four orders of magnitude, but it should have bought&lt;br&gt;
more. The fuzzer had no idea what the program was comparing, so it had to stumble onto each byte&lt;br&gt;
by luck.&lt;/p&gt;

&lt;p&gt;Comparison tracing is supposed to fix exactly this. Intercept every comparison, feed the compared&lt;br&gt;
values back into mutation, and the fuzzer stops guessing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The obvious implementation
&lt;/h2&gt;

&lt;p&gt;One pool of 4,096 slots. Every comparison stores both of its operands. Mutation picks a random&lt;br&gt;
value from the pool and writes it at a random offset in the input.&lt;/p&gt;

&lt;p&gt;This is what I wrote first, and it is roughly what the description sounds like.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;no bug found in 200,000 executions&lt;/strong&gt;, and coverage dropped from 12 edges to 10.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dumping the pool
&lt;/h2&gt;

&lt;p&gt;The fuzzer has a &lt;code&gt;DUMP_CMP=1&lt;/code&gt; mode that prints what comparison tracing collected. I expected to&lt;br&gt;
see the characters the parser was checking for. Here is what came out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;w=1 :  8 values    &amp;lt;- 'W', 'R', 'M', 'A', 'B' are in here
w=8 : 97 values    &amp;lt;- noise from size_t comparisons
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight useful values against ninety-seven useless ones. The program compares &lt;code&gt;size_t&lt;/code&gt; values far&lt;br&gt;
more often than it compares single bytes — lengths, buffer sizes, loop counters — and every one&lt;br&gt;
of those comparisons deposits an 8-byte value that is meaningless as input.&lt;/p&gt;

&lt;p&gt;With a 97-to-8 ratio in a single pool, the values that mattered came up about 8% of the time.&lt;br&gt;
Multiply that by the chance of landing at the correct offset and the answer is: almost never.&lt;/p&gt;

&lt;p&gt;The tracing was working. It was faithfully recording what the program compared. The problem was&lt;br&gt;
that I had assumed all comparisons are equally interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Three changes, and the third one is the one I did not expect to matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Split the pool by width.&lt;/strong&gt; Four pools, one per operand width (1, 2, 4, 8 bytes), and choose&lt;br&gt;
the width &lt;em&gt;before&lt;/em&gt; choosing the value, weighted toward narrow. A 1-byte comparison is far more&lt;br&gt;
likely to be about input than an 8-byte one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Store one side for &lt;code&gt;const_cmp&lt;/code&gt;.&lt;/strong&gt; When the program compares against a constant, the constant&lt;br&gt;
is the thing worth writing into the input. Plain &lt;code&gt;cmp&lt;/code&gt; still stores both sides, because there is&lt;br&gt;
no way to tell which one came from the input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Favour the newest seed.&lt;/strong&gt; Half the time, mutate from the entry most recently added to the&lt;br&gt;
corpus. That is the one that reached furthest; picking uniformly throws recent progress away.&lt;/p&gt;

&lt;p&gt;Changes 1 and 2 took 124,573 down to &lt;strong&gt;2,673&lt;/strong&gt;. Change 3 took it to &lt;strong&gt;2,309&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A 54x improvement over no tracing at all, at the cost of one edge of peak coverage. The fuzzer&lt;br&gt;
now finds the bug early and spends its time there instead of exploring elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Comparison tracing does not produce equally useful data.&lt;/strong&gt; The literature describes it as&lt;br&gt;
"feed compared values back into mutation", which is accurate and also useless as an&lt;br&gt;
implementation guide. A width-8 value from a &lt;code&gt;size_t&lt;/code&gt; comparison and a width-1 value from a&lt;br&gt;
character check are not the same kind of hint, and treating them as one pool is the difference&lt;br&gt;
between 200,000 executions with no result and 2,309 with one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A negative result is worth more than a lucky number.&lt;/strong&gt; If I had written the naive version,&lt;br&gt;
found nothing, and concluded the target was safe, I would have been wrong for the same reason&lt;br&gt;
most fuzzer users are wrong: the fuzzer never reached the code. Dumping the pool took ten&lt;br&gt;
minutes and turned a dead end into the only interesting thing in the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The measurements are the project.&lt;/strong&gt; The fuzzer is 300 lines. The interesting part is knowing&lt;br&gt;
that each line of it is worth what I claim, and being able to show the run where it was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://gitlab.com/mscupcakegkill/wrfuzz
&lt;span class="nb"&gt;cd &lt;/span&gt;wrfuzz &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./fuzz 200000
&lt;span class="nv"&gt;DUMP_CMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./fuzz 3000     &lt;span class="c"&gt;# see what comparison tracing collected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The RNG seed is fixed, so a shorter run is a prefix of a longer one, and the numbers above come&lt;br&gt;
out of the same binary. Windows Server 2019, clang 17.0.6, 4 vCPU.&lt;/p&gt;

&lt;p&gt;The fuzzer catches faults in-process with structured exception handling now, but it still has no&lt;br&gt;
fork-per-exec and no persistent mode, so it is not ready for real software. That is the next&lt;br&gt;
thing, and it is in the README's future work alongside the limitation that matters more: this&lt;br&gt;
implementation records &lt;em&gt;what&lt;/em&gt; was compared but not &lt;em&gt;where&lt;/em&gt; in the input it lives, which is what&lt;br&gt;
AFL++'s Redqueen does and what separates an AFL++-class fuzzer from a libFuzzer-class one.&lt;/p&gt;

</description>
      <category>fuzzing</category>
      <category>security</category>
      <category>c</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
