Kernel driver / BYOVD
In one sentence
Kernel driver analysis is the Malwagon module for Windows kernel-mode PE images: it takes a driver file, or a link the analysis host fetches the bytes from, and returns a static report of the IOCTL dispatch surface, the privileged hardware access in the code, the signing and mitigation state, an ATT&CK mapping, generated Sigma rules and a verdict.
- Input
- Any Windows PE image. Upload it, or give a link and the analysis host fetches the bytes. The gate is the MZ magic, so a driver delivered as
.dator under a random GUID is typed by content. - Ceilings
- 128 MB at the upload form. The module parses to a smaller ceiling of its own; a file between the two is still accepted, and the document names the size and the ceiling; nothing is quietly truncated to fit.
- Layers
- Static is the whole scan. Dynamic is a disabled field, not an unticked one, so a hand-crafted POST does not enable it. The derived-data AI layer is optional and off.
- Calibration
- Every import, rule and hardware primitive the module weighs is measured against how common it is across a corpus of real signed drivers, so an ordinary call is not read as a finding. The measured figure is printed beside the name it belongs to.
- Output
- Twelve named stages, four hashes including the Authenticode hash, an ATT&CK mapping in kill-chain order, generated Sigma rules as a downloadable bundle, and a verdict drawn from a vocabulary the rest of the platform does not have.
- Time
- Seconds. There is no snapshot to restore and no run to wait out; every document reports its own end-to-end duration in milliseconds.
Where this capability came from
This page asks which privileged primitive the binary holds, then how user mode is meant to reach it, and only after that whether the file looks malicious, and that order did not start here. It came in as a technology transfer from drivershield.io, and it decides what the module has an answer for: a correctly signed and currently trusted driver is still something it can characterise, and the point where a static read stops is something it states rather than guesses past. Everything that does the answering was built and measured here, from the corpus a finding is graded against to the scoring, the classification vocabulary and the report format, so a figure quoted on this page or printed in a driver document is a measurement from this build and not a number inherited with the approach.
What the module takes in, and what comes back refused
The submission gate is two bytes. A file whose first two bytes are not
MZ is refused at the form with the reason and with somewhere to go
instead, because for an archive, a script or a truncated blob there is no driver
report to be had, only an empty one. Everything past that gate is typed by
content: the extension decides nothing, which matters because a loader that
brings its own vulnerable driver rarely drops it under the name it was published
with.
A user-mode PE is deliberately accepted. Whether an image is kernel mode is a finding, and a finding belongs in the report: the decisive field is the optional header's subsystem, and when it is not the native subsystem the document says so in plain words and tells you the kernel-specific panels below it will be empty. Refusing that submission would teach the analyst nothing and cost them the round trip.
The submitted name is an input in its own right. It keys the cross-reference against publicly documented driver vulnerabilities, and it supplies the filename and version context the block-policy lookup needs. The name recorded on the submission wins over the stored sample's display name, because a sample row is shared by content and two analysts can submit the same bytes under two different names.
| Input | Bound | At the edge |
|---|---|---|
| Uploaded image | MZ, up to 128 MB | An empty file is refused at the form. Anything that is not a PE is refused with the reason: extract the driver from the archive and submit it, or send a script or document through the module built for it. |
| Source link | http / https | The host performs the download and the bytes then take the identical ingest path as an upload. The destination is validated at the form, again in the fetcher, and again on every redirect hop at the socket. The scan opens immediately and the credit is returned if the fetch fails. |
| Upload or link | exactly one | Both, or neither, is a validation error, not a silent preference for one of them. |
| Analysis ceiling | below the upload limit | A file the platform accepts but this module will not parse comes back with a document naming its size and the ceiling it exceeded. It is not truncated to fit and then reported on as though it were whole. |
| Machine type | i386, AMD64, ARM64, ARM | PE32 and PE32+ are both read and named. The layers that come out of a disassembly, meaning the compared control codes and the privileged instructions, run for x86 and x64 only and the report says which mode was used. |
| User-mode PE | Analysed | Accepted, parsed, and told what it actually is. The subsystem check is reported as a fact about the image; it decides nothing at the door. |
| Detonation options | None exist | Image, duration, network, work path, dumps, execution privilege and supporting files are all removed from this form as a set, taken from the shared option list as a set, so an option added there later does not quietly reappear on a module with no way to honour it. |
Does the driver ever get loaded or run?
No. Nothing is loaded, nothing is executed, no guest is opened and no snapshot is restored, so the driver does not become running kernel code on this platform. The form makes that structural: the dynamic layer is a disabled field, which ignores submitted data entirely and cleans to its initial value, and the view passes a literal false whatever arrives. An unticked box would have hidden the switch from an ordinary user while leaving it reachable to a crafted request.
The reason is an engineering one. Detonating a driver means loading kernel code in a guest, and doing that properly needs three things this platform has not built yet: a path that loads an unsigned or revoked image, a policy for the bugcheck that a genuinely vulnerable driver usually ends in, and a revert that survives the crash. An option nobody honours is a promise the report cannot keep, so the report's live panel states that kernel detonation is not yet available and shows the real queue of control codes that would be exercised. No field in it is filled with a plausible-looking value.
Parsed on the host, not run on it
The platform's line is drawn by what a tool does with the bytes, and how well the tool is confined does not move it: reading a structure is host work, and anything that would interpret, emulate or install goes into a virtual machine. This module sits entirely on the reading side of that line. The only outbound call it makes is a hash lookup, so the image itself is not sent off the machine that received it.
Being static has a consequence worth stating plainly, because it is the main practical difference an analyst feels. There is no restore, no arming and no run duration to wait out, so an answer arrives in seconds. Every document also carries its own measured end-to-end duration next to the stage list, so the figure you read is the one your own submission produced.
| Stage | What it settles |
|---|---|
| Hashes | MD5, SHA-1, SHA-256 and the Authenticode hash, the last one through the platform's single shared implementation, so the printed value and the corpus join key do not drift apart. |
| PE structure | Header, subsystem and bitness, sections with per-section entropy and flags, imports, exports and the version resource. |
| IOCTL extraction | Control codes the driver's own code compares, decoded field by field, plus the privileged instructions recovered from the same walk. |
| Signing | Embedded or catalog, the resolved leaf, the issuer chain, the validity window and whether the signature still covers these bytes. |
| Mitigations | Exploit mitigation posture, writable-and-executable sections, high-entropy code and packer markers. |
| Rule corpus | The kernel-driver rule set, separate from the platform's general file corpus, with each match tested for whether its evidence was only an import name. |
| Known-vulnerable corpus | Four identities of the image tried in strength order against a live feed of published vulnerable-driver hashes. |
| Name cross-reference | The submitted name looked up against a curated table of driver filenames with publicly documented vulnerabilities. A hint, and labelled as one. |
| Reputation | Multi-engine consensus, by hash only. "Not configured" and "not seen" are recorded as different answers. |
| ATT&CK | Techniques the contents justify, each carrying the sentence that names its evidence, grouped by tactic in kill-chain order. |
| Sigma | Detection rules generated from what was found, with stable ids, plus a single multi-document bundle to download. |
| Score | The composite, the verdict, which arithmetic produced the number, and which components were skipped because their layer had no answer. |
Which primitive does the driver hand to user mode?
A kernel driver is dangerous for one reason: it holds a capability that user mode does not have and it may be persuaded to use that capability on somebody else's behalf. So the first question is not whether the file looks malicious. It is which primitive the binary contains, and the binary declares that in two different places that have to be read two different ways.
Imports, read from the directory and graded against a base rate
The import table is parsed from the PE import directory, so "imports
MmMapIoSpace" means the loader will bind MmMapIoSpace.
A name lifted out of extracted strings would mean something much weaker, since
any mention anywhere in the file produces one and a call bound by ordinal
produces none at all. Ordinal-only imports are kept here under a synthetic name
so the count stays complete, they do not match the graded table, and the report
says so on the panel where it matters.
Every graded call is then weighed against how ordinary it is, and that ordinariness is measured: each name carries the share of a corpus of real signed drivers that also imports it, and a call that most drivers import is not treated as a finding. That grading is what keeps a module like this from calling every driver suspicious, because the call groups an ordinary driver trips are exactly the ones a naive reading scores. A common call is still shown, because an analyst reading a dispatch routine wants to see it; it is labelled as context and contributes nothing to the verdict.
| Kernel call | What it gives away, and what it takes to read it |
|---|---|
| ExAllocatePoolWithTag | Kernel pool allocation, the allocator almost every driver reaches for. It earns its place on the page because a reviewer working through a dispatch routine needs to see where the buffer that receives caller data came from: an allocation sized from a caller-supplied length is the point where a length check has to exist, and its absence there is the classic driver bug. |
| MmGetSystemRoutineAddress | Resolves a kernel routine by name at run time. It is how an ordinary driver stays portable across builds, and it is also how code reaches a routine that no call site in the image points at, so the report shows it with that ambiguity attached and with the resolved-pointer imports listed beside it. |
| ZwOpenKey | Registry access from kernel mode, below the level a user-mode monitor can see. What it means comes from what it sits next to: a driver reading its own service key is configuring itself, and one writing a service or run key is arranging to be loaded again. |
| IoCreateDevice | Creates the device object user mode opens. Half of the surface that makes any of the rest of this reachable. |
| IoCreateSymbolicLink | Gives that device a name user mode can type. The other half of the door: a device object nothing has named is reachable only by something that already knows how to find it. |
| ZwQuerySystemInformation | Enumerates loaded modules and handles, which is how the address of a kernel module is learned before anything is written to it. |
| MmGetPhysicalAddress | Translates a virtual address to a physical one, the companion call to a physical memory map: one turns an address into a physical page, the other hands that page to a caller. |
| MmMapIoSpace | Maps arbitrary physical memory into a caller's view. Reached through a dispatch routine that takes the address from the caller, this is an arbitrary read and write primitive over the whole machine, which is why the report reads it together with the transfer method of the codes the driver serves and with the physical-address translation call that usually accompanies it. |
| ZwTerminateProcess | Terminates an arbitrary process from kernel mode. This is the primitive behind tooling written to shut down endpoint agents, and it is also what a legitimate protection driver does, so the report reads it next to the rest of the image. |
| ObRegisterCallbacks | Registers an object-access callback, which can strip rights out of a handle before the requester sees it. Rare, and rare in a way that separates a security product from a driver imitating one. |
Calls are then grouped into the eight primitives a driver can actually hand over, and a group takes its weight from its least common member, because a group is only as surprising as the most surprising call in it. Each panel names what the group gives away in one sentence, separates the calls it weighed from the ones it carried for context, and prints the measured prevalence beside every name, so the reading can be checked against the corpus and need not be taken on trust. The distribution above the panels labels the calls it did not weigh, so a name carried for context is never read as a severity judgement about it.
The primitives no import table can show
Reading model-specific registers, writing an I/O port, moving a control
register and touching the hardware breakpoint registers are compiler intrinsics.
They compile straight to an opcode and do not appear in an import table at all,
so a reading built on import names has nothing to say about the primitive that
defines the best known cases of driver abuse. __readmsr,
__writemsr and READ_PORT_UCHAR are not imports of
anything; they are two-byte and three-byte instructions sitting in the middle of
a function.
They are read off the decoded instruction stream, in the same walk that recovers control codes. A read and a write are separated by operand position, so the two never collapse into one "register access" finding, and each site is recorded with its address so a reviewer can go and look at the function it sits in. Sites of one kind are kept up to a bound, because the count of them is what carries meaning and the thousandth address does not; the report prints how many were seen alongside the addresses it kept. That separation is the useful part: port I/O is what a hardware driver does for a living, a control register write is something else entirely, and reading them through one label would put the two on the same page as the same fact.
| Primitive | Instruction | What it gives whoever can reach it |
|---|---|---|
| Port I/O write | out | Writes an I/O port directly, which is what talking to hardware looks like from kernel mode. The site addresses are the point: a port number and a value that both arrive from a caller's buffer are a very different object from a fixed register written on a known chip, and only the function around the instruction tells you which one this is. |
| Port I/O read | in | Reads an I/O port directly. The string forms are recovered alongside the single-byte and single-word ones, so a loop that drains a port is seen as the loop it is, with the form named. |
| MSR read | rdmsr | Reads a model-specific register. Reading the register that holds the system-call entry point leaks where the kernel is loaded, which is what an exploit needs before any kernel address is worth writing to. |
| MSR write | wrmsr | Writes a model-specific register. Writing the system-call entry register sends every system call into code of the writer's choosing, which is a takeover of the kernel and not a bug in it. Which register is being written is not in the instruction itself, it is in a value loaded before it, so the report gives the site address and leaves the register to the reviewer who opens the function. |
| Control register read | mov from CRn | Leaks paging and protection state, which is the reconnaissance step before the matching write. |
| Debug register access | mov DRn | Touches the hardware breakpoint registers, the classic route to a kernel hook that leaves no patched bytes behind for an integrity check to find. |
| Control register write | mov to CRn | Clearing the write-protect bit in CR0 turns read-only kernel pages writable, CR3 swaps the page tables under a running system, and CR4 is where the supervisor execution and access protections live. The destination register is read out of the operand text, because a control register write only means something once you know which register it names. |
| Cache control | wbinvd, invd | Invalidates processor caches directly, which is how a change made through one view of memory is made visible through another. |
Some of those primitives never appear in the corpus at all, and the module is careful about what that licenses. An unobserved primitive is an absence of observation, and the code says so at the point where a clean sweep would otherwise read as a guarantee: a corpus drawn from one population can only report what that population does, and silence in it is a statement about the corpus before it is a statement about the file.
A primitive only counts if user mode can reach it
A driver that writes model-specific registers and gives user mode no way to name it is a hardware driver doing its job. Reachability is therefore read alongside every primitive: the panel says whether anything in the binary lets user mode address the driver at all, and a primitive with no door in front of it is described as exactly that. The test behind the answer is a cheap one and the report says so where it prints it. It asks whether the image names an interface user mode could open, which is a weaker question than whether a call graph reaches the primitive from the dispatch routine, and both questions have shapes of driver they read wrongly. The module records that limit on the panel it applies to.
The IOCTL surface, and how a control code decodes
A dispatch routine reads the control code out of the current stack location and compares it, arm by arm, against the codes it serves. That comparison is the signal. Scanning a file for every aligned dword that merely has the shape of a control code is a different thing entirely: run over a real signed driver it returns tens of thousands of candidates, almost all of them ordinary constants that happen to fit the bit layout. Keeping only the immediates the driver's own code compares leaves a couple of dozen, and those are checkable against the driver's own claim about itself, because a storage class driver comes back with codes whose device type decodes to mass storage.
code 0x81232403 device type 0x8123 (vendor-defined range) function code 0x900 transfer METHOD_NEITHER access FILE_ANY_ACCESS compared yes, at 0x140003C7A bytes in .text (executable) evidence compared at 0x140003C7A in an executable section
A worked example: one code, four fields
Take the code in the panel above, 0x81232403, invented for this
page. Four fields are packed into those 32
bits, and the report separates them because each one answers a different
question about the interface.
| Field | Value | What it tells you |
|---|---|---|
| Device type, bits 31-16 | 0x8123 | At or above 0x8000, so it is vendor-defined and belongs to no standard device class. If the driver presents itself as a storage or network device, this is a private interface of its own, sitting beside the class interface the operating system talks to, and that mismatch between the claimed role and the served surface is the first thing worth knowing about it. |
| Access rights, bits 15-14 | 0x0, FILE_ANY_ACCESS | The handle that carries this code needs no particular right on the device object. Anyone who can open the device can send it, which is why this field is read next to the device ACL and means little on its own. |
| Function code, bits 13-2 | 0x900 | Which operation is being asked for. Vendor function numbers usually cluster, so one isolated code high in the range describes a narrow purpose-built entry point, and the neighbouring numbers the driver does not serve are almost as informative as the one it does. |
| Transfer method, bits 1-0 | 0x3, METHOD_NEITHER | The driver is handed the caller's raw pointers. Every probe, every length check and every alignment check belongs to the driver from here on, so this field decides how much of the review is about the driver's own validation. |
Read together, the four fields say: a vendor-defined interface, reachable by anyone who can open the device, serving one narrow operation, handed raw user pointers. That combination is what the module grades, and no single field would have carried it. The evidence line matters as much as the decode, because the same four fields decoded out of a constant the driver never compares describe an interface that does not exist.
Immediates that survive the disassembly are still filtered for plausibility before they are reported, because driver code compares a great many 32-bit constants that are not control codes: status values, four-character magic constants, bit masks. The filter separates those out, and its predicate is not published here, since written down it would be a map of what the extractor drops.
| Method | How the buffer reaches the driver | What it means for a review |
|---|---|---|
| METHOD_BUFFERED | The I/O manager copies the caller's data into a system buffer and copies the result back. | The safest of the four. The driver never touches a user address, and the size is known to the operating system. |
| METHOD_IN_DIRECT | The caller's buffer is probed, locked and described by a memory descriptor list for reading. | The pages are pinned and validated before the driver sees them, but the driver still chooses how to map and how far to walk. |
| METHOD_OUT_DIRECT | The same, for writing back into the caller's buffer. | Same shape of risk, in the write direction. |
| METHOD_NEITHER | The driver is handed the caller's raw pointers and nothing else. | Graded higher Every probe, every length check and every alignment check is now the driver's own responsibility, and the absence of one of them is precisely the bug a vulnerable driver is reused for. |
A short set of control codes that are already public knowledge in bring-your-own-vulnerable-driver tooling is also searched for as raw bytes, because the prior for those is different: the code is public, so its presence is worth reporting even where this pass cannot prove the driver compares it. They cover arbitrary process termination, physical memory read and write, and model-specific register read and write. The list itself is not printed here, and the distinction the report draws matters more than the list: a public code found only inside a firmware blob is reported as exactly that, with the section it landed in named, and it is not counted as a served interface. Without that distinction an ordinary signed device driver that happens to embed the constant would read as a finding.
What the binary says about itself: signing, mitigations, and who can open the device
Signing, and three things it is careful not to claim
The certificate is parsed, and parsing is all that is claimed. There is no signature verification here, no chain walk and no revocation check, and the report says so in its own note, and does not leave the reader to assume the stronger thing. What the module does answer is who is named, when the certificate expires, and whether the embedded digest still covers the bytes that are in front of it.
Resolving the signer correctly is less obvious than it sounds. A countersigned signature puts every certificate needed to validate it into one unordered bag, including the timestamping authority's own chain, so taking the first certificate in the file reports a driver as signed by a timestamping service: true of a certificate in the file, and false about who signed the driver. The leaf is resolved instead by code-signing extended key usage and by being the certificate that issued nothing else in the bag, and the issuer chain is then walked by name, with an organisational-unit fallback for cross-certificates that carry no common name at all.
| Answer | What was computed | What it licenses you to say |
|---|---|---|
| Covers | The Authenticode PE hash, under SHA-256 and SHA-1, was found verbatim inside the certificate blob. | These bytes are the bytes that were signed. It is what an embedded signature looks like throughout the calibration corpus, which is exactly why the opposite answer carries so much weight. |
| Does not cover | The certificate is present and the digest it was made over is not the digest of this file. | Something was changed after signing. No driver in the benign corpus lands here, and this is the one state the check exists to catch. |
| Nothing to ask | There is no embedded certificate table. | The driver is catalog-signed, which a large part of the corpus is and which is the ordinary case. This is emphatically not "unsigned", and it is not printed as a broken signature. |
The Authenticode hash is treated as a first-class identity for the same reason. It is computed over the image with the certificate table excluded, so stripping the signature off a driver moves all three flat hashes and leaves that one alone. That evasion is a practical one, and it is why the known-vulnerable corpus is queried on four identities in strength order, with the report stating which one matched and exactly what claim it licenses. "This exact file" and "the same image, re-signed or stripped of its signature" are different sentences, and the page prints whichever one is true.
| Corpus | Matches on | Notes |
|---|---|---|
| Known-vulnerable drivers | SHA-256, SHA-1, MD5, Authenticode hash | A live feed of published vulnerable-driver identities; not every entry carries all four of them, so a miss on one identity is not a clean result. The report prints the size of the feed it actually read, together with the entry's category, published vulnerabilities and version details as enrichment. |
| Enforceable block policy | hash | The same identity a flat hash lookup uses, from a completely separate authority. |
| Enforceable block policy | signer certificate | Reaches a driver that was renamed and rebuilt, because the certificate did not change. Certificate hashes are computed under three algorithms and compared by length, because the policy implies the algorithm from the length of the value it carries. |
| Enforceable block policy | signer + publisher | Leaf and full-chain certificate sets are kept apart, because a rule with no narrowing term may only ever match the leaf. |
| Enforceable block policy | signer + filename and version range | Identifies a specific vulnerable build of a product, not a file. The filename and version range that produced the match are printed with it, so a reader can see which build the policy names and check it against the version resource in front of them. |
| Deliberately not matched | shared certificate authority | A signer rule whose only narrowing term is something this analysis cannot read out of the binary is skipped. Matching it on the shared root alone would sweep in a large share of legitimately signed drivers, and a match that broad is not a finding about this image. |
The block policy is an operator-supplied artifact. This platform does not ship it, does not fetch it and hard-codes no location for it, and when it is absent the lookup records that it had no answer, and a clean result is never printed for a question nobody asked. What the lookup answers is narrow and worth stating exactly: whether this image would be admitted under the code-integrity policy the operator supplied. The module makes no claim about how strong that policy is or what it would stop. Where both corpora speak, two corpora agreeing is treated as one fact and counted once.
The mitigation matrix, and what each gap costs
| Mitigation | When it is missing |
|---|---|
| Control Flow Guard | Indirect calls go unchecked, so a write primitive that reaches a function pointer becomes execution with nothing further to build. |
| ASLR | The image loads at a fixed base, so every gadget, every pointer and every writable global inside it has a known address before the exploit is written, and no leak is needed to find them. |
| DEP / NX | Data pages are executable, so a buffer the driver already copies into becomes somewhere to put code, and an overrun into it needs no further step to become execution. |
| Force Integrity | The loader is not told to require a valid signature for this image. |
| Embedded certificate | Its absence says little on its own, because a great many legitimate drivers carry their signature in a catalog that never travels with the file. |
| Certificate in date | On its own this is ordinary: signatures are countersigned with a trusted timestamp, so a driver signed years ago stays validly signed after its certificate lapses. It matters next to something else, because an expired certificate on a signed driver holding a live primitive is exactly what makes an old driver worth reusing. |
| Writable and executable section | A section that is both is a ready-made place to stage code, and it is reported per section with the section named, so a reviewer knows which one to open. |
| High-entropy code | Judged on executable sections only, so a compressed resource inside an ordinary signed driver is not called packed. |
Alongside those, the module lists the imports that reach a call target no compiler-emitted call site points at: a kernel routine resolved by name at run time and then invoked through the resolved pointer. That is reported as an observation about the image, with nothing attached about what any particular mitigation or policy would make of it.
Who can open the device object?
Reachability asks whether user mode can name the driver. The device ACL asks the other half of the question: whether an unprivileged caller can open it once it is named. A physical memory primitive behind a device only SYSTEM can open is a very different risk from the same primitive behind one any authenticated user can open. Security descriptors are recovered in both ASCII and UTF-16LE, split access-control entry by entry, and decoded into trustee, trustee privilege class and named rights, and the module separately reports which imports could actually bind a descriptor at all, because a descriptor string in a file with none of those calls present is a string and not an applied ACL.
Reported in full, and read as context
A declared ACL is bytes in a file, and bytes in a file are written by whoever produced the file, so the descriptor settles nothing on its own in either direction. A restrictive one is not evidence that a driver deserves the benefit of the doubt, and a binary that declares nothing has not thereby left its device open to the world, because the ACL a device object ends up with is commonly decided by the installation file or left to the default the operating system applies. The decoded descriptor is printed in full and labelled for what it is, because an analyst deciding whether a primitive is worth chasing wants to know who could reach it once it is named.
Classification first, then a number that says how it was made
The driver is classified before it is graded, because nearly every heuristic in a module like this quietly assumes a driver that owns its own dispatch table. Classification runs on four axes, each carrying its evidence and its measured prevalence: the framework the driver is built on, its functional role, whether it belongs to a platform class whose whole job is direct hardware access, and whether the binary is itself a framework or port library, or a client of one.
| Measured across the corpus | Driver owns its dispatch table | Framework owns it |
|---|---|---|
| Share of the corpus | The larger population | A substantial minority |
| Creates its own device object | Common, and its absence is informative | Rare, and absent by design |
| Creates a symbolic link | Common enough to be worth grading | Rare, for the same reason |
| At least one control code recovered | The usual outcome | Recovered less often, because the comparison sits in framework code |
Read the second row without the class and "no device interface named" looks like a finding, when for a substantial share of drivers it is close to the default. The class therefore emits its warnings onto the exact panels it makes unreliable, each one printed with the measurement behind it, and it changes no weight and no base rate anywhere: what an absence earns is an annotation on the panel where it would otherwise have read as a finding. Any softening a framework or port library earns is withdrawn the moment the signature no longer covers the bytes, because a port library's primitives are normally the ones it offers to its own clients, and that reading does not survive a binary that is no longer the one that was signed.
Functional role is read from the registration symbol a given kind of driver calls, then from an export, then from a linked library, in that order, and the report prints the corpus prevalence of the role it settles on. General purpose is the bulk of the population; network, storage, file system minifilter, network transport, audio, human interface, USB and display trail it, and the tail matters because a claimed role that does not fit the imports is itself worth reading.
The rule corpus, and the double count it refuses
The kernel-driver rule corpus is kept separate from the platform's general file-scanning corpus. It holds a small set of hand-written behaviour rules for the bring-your-own-vulnerable-driver case, and a much larger body of rules that identify a known-vulnerable driver by its version resource. API names are matched on word boundaries, and that detail is not cosmetic: a plain substring match binds the prefix of longer symbol names, which inflates how common a rule looks, and a rule that looks common is read as ordinary when it is nothing of the kind.
Every match is also tested positionally against the file offsets the import directory points at. A match whose entire evidence is the import name table is flagged as such and refused a second count, because the import layer has already graded exactly those names against a measured rate. One observation counted twice under two headings would read as two signals agreeing with each other, and it is not that.
A version resource can be copied into any file
A rule that identifies a known-vulnerable driver only by its embedded version strings cannot set a verdict on its own, because those strings are text that anything can carry. Such a match is reported in full, it moves the weighted score, and the score panel writes the reason out in words next to the number: so many rules identified a known-vulnerable driver by its version resource alone, with nothing to corroborate it. Saying that out loud is better than dropping the match, and much better than letting it set a verdict.
ATT&CK and generated Sigma
Seven kinds of Sigma rule are generated from what was actually found, for driver-load and process-creation log sources: a hash rule that survives a rename, a rule for a published exploit control code, rules for the process termination, register access and physical memory primitives, one for a process opening the device object this driver names, and one for the kernel service install that is how such a driver gets loaded in the first place. Rule ids are derived deterministically from the sample hash and the rule kind, so importing the same driver's rules twice does not give a SIEM two copies, and the YAML comes out of a serialiser, so a hostile filename does not restructure the document it lands in.
The number, and which arithmetic produced it
Two arithmetics run. One is a weighted average over nine components, renormalised across the components that had an answer, so a layer that did not run is removed from the divisor, and never counted as a zero that would quietly make a driver look cleaner than it is. The other is a floor driven by definitive, specific facts, and it exists because a driver whose only finding is that it exposes arbitrary physical memory through a published control code is fully malicious with one signal, and no average of nine components will ever say so. The report states which of the two produced the number, and when the floor wins it lists the facts that set it, in words, beside the score. An analyst who adds the components up and gets something else should be able to see why in one glance, and should not be left to distrust the page.
| Component | Question |
|---|---|
| Known-vulnerable corpus | Has this image, by any of its four identities, already been published as abusable? |
| IOCTL dispatch surface | Does it serve a published exploit code, or hand itself raw user pointers? |
| Dangerous kernel imports | Which primitives does the loader bind, and how rare are they in benign drivers? |
| Privileged hardware access | What does the code do that no import table could have shown? |
| Rule matches | What did the driver rule corpus say, minus anything whose only evidence was an import name? |
| Multi-engine consensus | What do other engines say about this hash, if a key is configured at all? |
| Name cross-reference | Does the submitted name match a driver with a publicly documented vulnerability? A hint, weighted as one. |
| Code-signing state | Embedded or catalog, in date or lapsed, and does the signature still cover these bytes? |
| Packing and entropy | Is the code hidden, judged on executable sections only? |
| Verdict | In driver terms |
|---|---|
| Clean | No abusable primitive found, or the primitives present are architectural for the class and there is no door to them. |
| Suspicious | Something is here that most benign drivers do not have, and it is not enough on its own to say more. |
| Vulnerable | A legitimately signed driver that holds an abusable primitive. This is a real and common outcome, it is the entire point of the bring-your-own-vulnerable-driver technique, and collapsing it into "suspicious" loses the distinction. |
| Malicious | Published as abusable, or holding a live published exploit code, or otherwise carrying a definitive fact the floor recognises. |
| Not a kernel driver | Not one of the four, and the accurate answer to a submission this module receives regularly: the image was analysed and it is not a native-subsystem binary. |
"Vulnerable" is the module's own word. The platform-wide verdict vocabulary has no place for it, so it is mapped conservatively to suspicious for the shared scorer and shown in full on the driver report, and the driver score acts as a floor on the scan's overall verdict: it can pull the scan up, never down. Alongside the verdict the module prints a behaviour classification, one of endpoint-agent killer, privilege escalation primitive, rootkit component, bring-your-own-vulnerable-driver candidate, or no abusable primitive found, with a confidence, a rationale and the method named plainly as a primitive heuristic, with no pretence that a model produced it. Where the framework owns the dispatch table, the "nothing found" branch says the absence is weak evidence and does not state a conclusion.
What this module does not do
A driver this module calls clean can still be malicious in a way only a running kernel would show, because nothing here runs. Everything below is printed in the report itself, at the panel it applies to, so none of it waits to be discovered halfway through an incident.
| Limit | What it costs you |
|---|---|
| No kernel detonation | There is no dynamic layer here, only a static one. Nothing is loaded, nothing is executed, and the live panel says so where a runtime panel would otherwise stand empty. |
| No signature verification, no chain walk, no revocation check | The certificate is parsed and the digest is checked against the file's own bytes. Whether the signature verifies and whether the chain is trusted are questions this module does not answer, and the report says so where a reader would otherwise assume the stronger thing. |
| A catalog-signed driver cannot be checked from the binary | Catalog signing is the ordinary case for a large part of the corpus, and the catalog is not in the file. The report names which of the two states applies and does not describe the second one as unsigned. |
| The name cross-reference is a name lookup | A name is not an identity. A renamed driver misses, and renaming is the normal case in an incident, so the report labels a match a hint and says what it would take to confirm one. Nothing rests on it alone: the hash, the certificate and the Authenticode identity are what the module actually relies on to recognise a known-vulnerable image. |
| Reachability is a cheap heuristic | It asks whether user mode can name the driver, which is a weaker question than whether a call graph reaches the primitive from the dispatch routine. There are shapes of driver the cheap test reads wrongly, and the panel carrying the answer carries the limit with it. |
| Framework-managed drivers are weaker ground | A substantial share of drivers hand their dispatch table to the framework, so control codes are recovered less often and the device-creation imports this module reads are usually absent by design. The affected panels carry the warning, and the answer really is weaker there. |
| The disassembly layers are x86 and x64 only | The compared control codes and the privileged instructions come out of a decoded instruction stream, and that decoding is implemented for x86 and x64. On another machine type the PE layers still run and the report says which of them produced the document. Reading a code path statically has bounds of its own as well: the walk runs to a budget, a large image can reach it, and the document says when it did, so a partial walk is never presented as a complete one. Ordinal-only imports are counted under a synthetic name and cannot be matched by name at all. |
| The device ACL is only what the binary declares | The ACL a device object ends up with is commonly decided by the installation file or left to the operating system's default, and neither of those is in the binary. A driver that declares nothing is recorded as no answer in either direction, and a descriptor that is there is read as context for the primitive behind it. |
| Corpus enrichment describes the corpus entry | The published vulnerability list and the loading notes attached to a corpus entry describe that entry. They are shown in full and read as context, because a description written about somebody else's file is a fact about that file and not a measurement of the bytes in front of the module. |
| The calibration corpus is one population | Real and signed, but drawn from a single Windows build, so it is one population. The module's own code names where that leaves it structurally blind, and it treats an unobserved primitive as no evidence, not as safety. |
| Layers are allowed to have no answer, and several often do | No reputation key means not configured, an absent block policy means not checked, and both drop out of the average, where counting them as clean would be the wrong answer. The score panel names the components that were skipped, and it does mean the number can be computed over fewer layers than the panel list suggests. |
| Nothing is unpacked | A packed or protected driver is flagged as such, its hidden code is not recovered, and every layer downstream of it sees less. |
| The generated rules cover less ground than the report does | Every rule names the finding it was generated from, so the bundle can be read back against the document it came from. Not every observation has a clean equivalent in a log source, and where there is none no rule is emitted, so the report is the complete statement of what was found and the bundle is the part of it a SIEM can hunt for. The rules are a starting point for detection engineering and are meant to be reviewed against the environment they are loaded into. |
The limit that matters most
This module cannot tell you that a vulnerable driver is exploitable. It can tell you which primitive the binary holds, how rare that primitive is among real signed drivers, whether the code compares a control code that reaches it, and whether there is a door for user mode to knock on. Turning that into a working exploit is a separate piece of work, and no static answer should be read as having done it.
Where this module hands off
Where this module sits
The general PE chain still runs underneath a driver submission: platform hashing, the general rule corpus, antivirus signatures, string extraction, secret detection, indicator extraction and similarity clustering all happen, and the driver document sits on top of them as its own report with its own vocabulary. Influence between the two runs in one direction only, by design: the driver score is a floor on the scan verdict, so it can only raise the scan.
The other half of the bring-your-own-vulnerable-driver problem is a detonation question, and it belongs to file analysis. A loader that drops a driver and installs it as a kernel service runs there in a full virtual machine under a hypervisor, on hardware Malwagon operates. That guest is restored from a live memory snapshot and not cleaned up after the previous run, and it writes to a disposable disk that is discarded with it. Driver loads, kernel callbacks and the syscall table are read out of a memory image captured at the hypervisor level; the packet capture and the screen recording are taken outside the guest as well. An in-guest component adds API-level detail on top of those three, as one source among several, so a sample that kills or unhooks it costs the report that detail while the outside sources keep recording. Dropped drivers are then cross-referenced against the same known-vulnerable identities this page describes. Submit the binary here to learn what it can do; detonate the loader there to see whether it did it.
This module has no guest and no in-guest component at all. It reads the image without running it, so every answer on this page is the product of parsing bytes. Two properties hold on both sides of the hand-off. The image is not sent off this host, because the only outbound call the module makes is a hash lookup, which is the no-upload invariant set out on the security page. And the analysis is deterministic: the same bytes and the same submitted name produce the same document every time, which is what makes a driver report something you can cite in a ticket and re-run a week later to the same document.