Case Study: MS17-010 (MS17-010)
The mock modules elsewhere in these guides show the shape of the SDK without touching a
real target. This page documents a pair of real modules — ms17-010-survey
and ms17-010-exploit — that fingerprint and exploit the MS17-010 SMBv1
vulnerability (CVE-2017-0143) in the Windows kernel driver srv.sys. It is the
reference example for a full, network-facing capability in Hovel: a survey that produces
facts, an exploit that consumes them, a real memory-corruption primitive, typed
capabilities for later payload steps, and the safety machinery that gates a
dangerous action.
Both modules live under modules/examples/python/ms17_010_survey/ and
modules/examples/python/ms17_010_exploit/. They are pure-Python and carry no third-party
dependencies — the SMB and DCE/RPC wire formats are assembled by hand with
struct — so they run straight from source through the daemon's interpreter,
exactly like the mock examples.
Scope and safety. The implementation targets Windows XP SP3 (x86),
srv.sys build 5.1.2600.5512 — an operating system that has been
end-of-life since 2014. Every numeric constant below is specific to that build. The
exploit corrupts remote kernel memory and can crash the target; it is intended only for
an authorized lab host you control. The module enforces this with the controls described
in Integration with Hovel.
What the example demonstrates
- A survey that feeds an exploit.
ms17-010-surveyreports whether a host is vulnerable and which named pipe to use;ms17-010-exploittakes those facts as configuration. - A genuine kernel read/write primitive built entirely from legitimate-looking SMB requests.
- Execution without kernel shellcode. The primitive is used to elevate the live SMB session to SYSTEM, after which ordinary administrative operations can create accounts, upload files, manage services, or run a command when those sub-capabilities are implemented.
- The dangerous-module path: the
dangeroustag, an explicit operator confirmation requirement, an audit record, and a clean restore step.
Background: SMB Transactions in one minute
SMBv1 lets a client send a request that is too large for one packet by splitting it into a
primary request followed by secondary writes. To track this, the server
allocates a transaction object in the kernel's paged pool with a data buffer
(here called InData) sized to the promised total. Each follow-up packet carries
three bookkeeping values:
TotalDataCount— how big the whole payload will be, which is how bigInDatawas allocated.DataCount— how many bytes this packet carries.DataDisplacement— where in the buffer to place them: the server copies toInData + DataDisplacement.
Transactions are reached over a named pipe opened on the IPC$ share.
On a vulnerable, default Windows XP install that pipe can be opened with an
anonymous (NULL) session — no credentials — which is what makes the whole chain
remote and unauthenticated. A short, fixed set of pipes is usable this way:
spoolss, browser, lsarpc.
The survey module
ms17-010-survey is a survey module: it gathers facts and changes
nothing. It performs a real SMBv1 handshake — negotiate, anonymous session setup, tree
connect to IPC$ — then probes the three candidate pipes and issues the
standard MS17-010 detection request (a PeekNamedPipe transaction against an
invalid file id). A vulnerable srv.sys answers that request with NT status
STATUS_INSUFF_SERVER_RESOURCES (0xC0000205); a patched one does
not. The verdict and the reachable pipes are returned as facts:
{
"host": "10.0.0.42",
"port": 445,
"smb_reachable": true,
"null_session": true,
"verdict": "vulnerable",
"detection_status": "0xc0000205",
"reachable_pipes": ["spoolss", "browser", "lsarpc"],
"recommended_pipe": "spoolss",
"suggested_target_profile": "XP_SP2SP3_X86"
}
A vulnerable result is also surfaced as a critical Finding and a
JSON Artifact. Because it is reconnaissance only, the survey carries no
dangerous tag and no confirmation gate. Its recommended_pipe and
suggested_target_profile are the inputs the exploit expects.
The exploitation primitive
The exploit builds an arbitrary kernel read/write from four observations about how
srv.sys handles transactions. Each step is driven by ordinary SMB packets.
1. The flaw: a shifted buffer, not a missing check
When a secondary write arrives, the server copies DataCount bytes to
InData + DataDisplacement, and it does bounds-check the destination
against TotalDataCount. The flaw is a type confusion between a raw
WriteAndX request and a Transaction: a raw WriteAndX
(WriteMode = write-raw-named-pipe) sent to an in-progress transaction's pipe
advances that transaction's InData pointer by the number of bytes
written, while leaving TotalDataCount unchanged. A following secondary write
with a large DataDisplacement still passes the bounds check — which is measured
against the unshifted total — but now lands past the real, shortened buffer, into
whatever sits next in the pool. The result is a controlled out-of-bounds kernel write.
2. Grooming: arrange two transactions side by side
A wild overflow just crashes the machine. To make it useful, the module first shapes the paged pool so that the buffer it overflows from is immediately followed by a second transaction it also controls. It does this by allocating a run of identically sized, deliberately incomplete transactions; uniform same-size allocations pack back-to-back, so two of them end up adjacent. One of the run reuses the open pipe's file id as its identifier, which fixes it at a predictable position relative to its neighbour.
3. Leak: turn a peek into a kernel address
Kernel addresses are not guessable, so before corrupting anything the module reads pool
memory back. By completing a transaction whose counts it has quietly enlarged, it gets the
server to return bytes from beyond the transaction's own buffer — the adjacent chunk —
which contains a transaction header. From that header it recovers genuine kernel pointers,
including the CONNECTION and SESSION objects for the current SMB
session and the addresses of the two groomed transactions.
4. Takeover: one overflow becomes read/write anywhere
The module then performs the shifted-buffer write to overwrite the neighbouring transaction's header — specifically the pointer to its data buffer and its count fields. After that, the neighbour is a re-aimable window into kernel memory:
- Write: point the victim's data pointer at any kernel address and send it a normal secondary write — the server copies your bytes there.
- Read: point it at any address and ask for its data back — the server returns the bytes at that address.
A pair of helper transactions keeps the victim valid between operations, giving a stable,
reusable read/write primitive. The relevant field offsets for this build are kept in one
place, offsets.py: the transaction is 0x98 bytes, with
InData at +0x44, TotalDataCount at +0x64,
and the identifier used for the takeover oracle at +0x7c.
5. From read/write to reusable access
With kernel read/write in hand there are two well-known routes to execution. One overwrites
a function pointer in the SMBv1 transaction dispatch table and supplies kernel shellcode.
This module takes the other, which needs no shellcode and is markedly more stable: it edits
the current SMB session's own security context. It clears the session's
"anonymous" flag and rewrites the token's group list to include the SYSTEM and
Administrators groups. The very same TCP connection is now an administrative session — which
is confirmed simply by tree-connecting to the C$ share, something an anonymous
session cannot do.
From an administrative session, remote operations become routine. The current
example can open the Service Control Manager over DCE/RPC, create a one-shot
service, and start it. The modular chain model generalizes that result into a
RemoteExecutionCapability with explicit sub-capabilities such as
exec_command, upload_file, and
service_control. The MS17-010 module must only report sub-capabilities it
actually implements and can keep valid.
The exploit module
ms17-010-exploit is an exploit module. Its class attributes declare
the metadata, the dangerous tag, the chain-level confirmation requirement, and
the per-target configuration; run performs the chain. The shape will be
familiar from the Python guide:
class MS17010ExploitModule(HovelModule):
name = "ms17-010-exploit"
module_type = "exploit"
summary = "MS17-010 SMBv1 srv.sys RCE — runs a command as SYSTEM."
tags = ("dangerous", "smb", "ms17-010", "exploit", "python")
global_config = (
Requirement("operator.confirmed_lab", "bool",
description="Operator confirmed this is an authorized lab target."),
)
target_config = (
Requirement("target.host", "host"),
Requirement("target.port", "port", default="445"),
Requirement("pipe", "enum", default="spoolss",
allowed=["spoolss", "browser", "lsarpc"]),
Requirement("command", "string", required=False, default="calc.exe"),
Requirement("cleanup", "bool", required=False, default="true"),
Requirement("target_profile", "string", required=False, default="XP_SP2SP3_X86"),
)
def run(self, ctx: Context) -> Result:
...
The package is split so that each concern is testable on its own:
offsets.py— the build-specific structure offsets and pool constants.smb.py— the hand-rolled SMBv1 client: session bring-up, the transaction primitives, rawWriteAndX, and aTransactNamedPipehelper for DCE/RPC.analysis.py— pure parsing of the leaked pool chunk and construction of the replacement token group list (no I/O, fully unit-tested).exploit.py— the chain itself: control, leak, the read/write primitive, elevation, command execution, and restore.svcctl.py— the minimal Service Control Manager client used to run the command.errors.py— a staged error enum so a failure reports exactly which phase stopped (pipe, leak, takeover, walk, payload, and so on).module.py— theHovelModulethat ties it together and maps the outcome onto aResult.
On success the module returns a critical finding, a JSON artifact
recording the recovered CONNECTION/SESSION addresses
and the access status, and capability outputs describing what later steps may
safely consume. On failure it returns the staged error identifier, so an
operator can see whether, say, the groom missed (re-throw) or the host
blue-screened (reset the lab).
Integration with Hovel
Module contract and runtime
Both modules are ordinary jsonrpc-stdio modules. They subclass
HovelModule, declare survey / exploit as
their module_type, and answer the common RPC methods through the
SDK's serve loop. In the chain-step model, the MS17-010 exploit also reports live
step contracts through step.describe and executes confirmed steps
through step.execute. Nothing about the framing changes for a real
module versus a mock; see the Wire Protocol
reference.
The dangerous-module path
The exploit participates in Hovel's safety model (see Safety and Scope) in three ways:
- It is tagged
dangerous, so a throw will not run it without--allow-dangerous. - It declares a chain-level requirement
operator.confirmed_laband refuses to proceed unless it is set — returning a cleanfailedresult, not an exception, when it is absent. - On an authorized throw it writes an audit log line recording the confirmed target, command, and profile before any packet is sent.
The runtime fact that it corrupts kernel memory is therefore never reachable by accident: a plan must exist, the operator must confirm the lab, and the dangerous flag must be present.
The survey-to-exploit handoff
The two modules are designed to chain. The survey's recommended_pipe becomes
the exploit's pipe input, and its suggested_target_profile becomes
target_profile. In a chain file the survey runs first; its facts are available
to the exploit's configuration, so an operator does not hand-pick a pipe — the touch chose
one that is anonymously reachable.
The MS17-010 to Squatter handoff
The MS17-010 exploit does not own Squatter sessions. In the representative Squatter chain, it
produces a connection-bound RemoteExecutionCapability. A separate
Windows credential step consumes that capability to create a temporary local SMB
credential with neutral random username and high-entropy random password. The
Squatter SMB installer consumes the remote execution capability, the SMB
credential, and a Squatter payload artifact to stage and start Squatter. It
returns an explicit installed-payload descriptor that Hovel validates against
the Squatter provider and persists in SQLite. The Squatter connector then uses
the SMB credential to connect to \\target\pipe\name and produce the
SessionRef.
The currently implemented legacy CLI bridge keeps that ownership boundary while
using module-level chain entries: when Squatter is present in an MS17-010 chain,
planning derives payload.transport and bind settings for the MS17-010
exploit. At execution time Hovel asks the Squatter provider to generate the payload
artifact and passes the resulting bytes to the exploit as payload.bytes_base64;
operators do not configure host-local payload paths. If the exploit reports a Squatter
installed-payload descriptor, Hovel records it in the payload inventory and immediately
asks the Squatter provider to reconnect, appending the resulting SessionRef
to the throw result. Without a Squatter bridge step, the MS17-010 exploit remains command
execution only and should not be expected to create a session.
ms17-010.exploit
-> RemoteExecutionCapability(connection_bound)
windows.credential.create_local_admin
-> CredentialCapability(protocol=smb), CleanupHandle
squatter.generate
-> PayloadArtifact
squatter.install_smb
-> PayloadInstance, SmbPipeEndpoint, CleanupHandle, InstalledPayloadDescriptor
squatter.connect_smb
-> SessionRef
This is intentionally a representative chain, not a bespoke MS17-010 product. The MS17-010 exploit establishes access; credential, install, installed payload tracking, connect, session, and cleanup behavior are owned by their respective modules and provider steps. It may report that it installed Squatter only by returning a typed installed-payload descriptor naming the Squatter provider and payload id; Hovel still validates that descriptor before persistence.
Configuration and registration
Each module ships as a package with a Python launch entry. Operators install release packages or link local package roots while developing:
hovel module install ./ms17-010-survey-1.0.0.tgz
hovel module install ./ms17-010-exploit-1.0.0.tgz
hovel module install --link "$PWD/modules/examples/python/ms17_010_survey"
hovel module install --link "$PWD/modules/examples/python/ms17_010_exploit"
A throw supplies the target configuration and the confirmation:
{
"target": "10.0.0.42",
"chainConfig": { "operator.confirmed_lab": "true" },
"targetConfig": { "target.host": "10.0.0.42", "pipe": "spoolss",
"command": "calc.exe", "cleanup": "true" }
}
Build and test
The modules are part of the standard gate. Each ships unit tests that exercise the pure
logic — the pool-chunk parser, the token-group builder, the packet builders, the wire
marshalling, the configuration schema, and the confirmation gate — without any network
access, so they run in CI offline. They are wired into the Bazel test suite and pass the
same task ci gate (lint, docs, build, and tests) as the rest of the
repository. The chain itself is exercised against a live lab host, not in CI.
Where to look
modules/examples/python/ms17_010_survey/— the survey module and its SMB touch.modules/examples/python/ms17_010_exploit/— the exploit module, primitive, and SCM client.- Python Modules — the SDK surface these are built on.
- Safety and Scope — the trust model the dangerous tag plugs into.
- Operations, Chains, Throws — how the survey and exploit are chained and thrown.