agrsm64.sys — Modem driver disabled by DriverEntry kill-switch; unbounded IOCTL_SERIAL_SET_QUEUE_SIZE pool allocation (CWE-789) fixed
KB5078752
1. Overview
| Item | Value |
|---|---|
| Unpatched binary | agrsm64_unpatched.sys |
| Patched binary | agrsm64_patched.sys |
| Overall similarity | 0.0026 (effectively unrelated — the patch is a kill-switch) |
| Matched functions | 7 |
| Changed functions | 5 |
| Identical functions | 2 |
| Unmatched (either side) | 0 / 0 |
Verdict: The vendor did not harden the individual handlers — they shipped a kill-switch that makes DriverEntry return STATUS_NOT_SUPPORTED (0xC0000424), so the entire driver (device creation, symlinks, and all IRP handlers) never loads. The unpatched driver registers a serial-class IRP_MJ_DEVICE_CONTROL dispatcher (SerialIoControl (sub_1297f8)), the standard Microsoft serial-framework handler, on the driver object. It creates a per-modem device \Device\LSIModem<N> (FILE_DEVICE_SERIAL_PORT, Exclusive=1, protected symlink \DosDevices\LSIModem<N>) and a control device \Device\LSISM_xface (FILE_DEVICE_SERIAL_PORT, Exclusive=0, no device extension, symlink \DosDevices\LSISM_xface created with IoCreateUnprotectedSymbolicLink). The one notable code-level item is IOCTL_SERIAL_SET_QUEUE_SIZE (0x1b0008), which takes the requested receive-buffer size straight from user input and allocates it from NonPagedPool with no upper bound (an excessive-allocation-size / memory-exhaustion issue, CWE-789). The driver code contains no IoCreateDeviceSecure/SDDL call, so the device DACLs are the I/O-manager defaults and cannot be determined from the binary; no world-accessible-device escalation is established here.
2. Vulnerability Summary
Finding 1 — IOCTL 0x1b0008: Unbounded (Size-Controlled) NonPagedPool Allocation
| Field | Value |
|---|---|
| Severity | Low |
| Class | Memory allocation with excessive size value (CWE-789) |
| Function | SerialIoControl (sub_1297f8) (IRP_MJ_DEVICE_CONTROL dispatcher) |
| IOCTL | IOCTL_SERIAL_SET_QUEUE_SIZE (0x1b0008) |
| Primitive | Memory-exhaustion DoS (large or failing NonPagedPool allocation) |
Root cause: The IOCTL_SERIAL_SET_QUEUE_SIZE handler reads the first DWORD of the user-supplied SystemBuffer (the requested receive-buffer size) and uses it directly as the NumberOfBytes argument to ExAllocatePoolWithTag(NonPagedPool, …, 'AGSM'). The only gate is a grow-only comparison against the current receive-buffer size stored at device-extension offset +0x140 (cmp eax, [rsi+0x140] at 0x129b1e): the buffer is (re)allocated only when the requested size exceeds the current one, and there is no upper bound. On allocation failure the handler returns STATUS_INSUFFICIENT_RESOURCES (0xC000009A). The resulting pool pointer is stashed in the current I/O stack location scratch field ([rbx+0x20]); the IRP is then queued via SerialStartOrQueue (sub_12c4b4) with the read start routine SerialStartRead (sub_12b5b8), which treats the buffer as the driver's receive FIFO. This is the standard Microsoft serial-framework IOCTL_SERIAL_SET_QUEUE_SIZE behaviour; the allocation is sized exactly to the request and its contents are later filled by received serial data, not by the IOCTL input, so there is no attacker-controlled out-of-bounds write here. The ceiling is a large or failing NonPagedPool allocation (memory-exhaustion / allocation-failure DoS).
The driver code contains no IoCreateDeviceSecure call or SDDL string, so the device DACLs are the I/O-manager defaults and are not determinable from the binary. The per-modem device \Device\LSIModem<N> is created Exclusive=1 with a protected symlink (IoCreateSymbolicLink, 0x591cb/0x5925b); the control device \Device\LSISM_xface is created Exclusive=0 with an unprotected symlink (IoCreateUnprotectedSymbolicLink, 0x12e301). An unprotected symlink governs deletion/replacement of the symlink object, not access to the device object, so it does not by itself establish unprivileged device access. The vendor's fix removes the entire driver.
Attack flow (IOCTL 0x1b0008):
- A process that can open the device calls
CreateFileWon the corresponding symlink (\\.\LSIModem<N>or\\.\LSISM_xface). - It calls
DeviceIoControl(h, 0x1b0008, &sizeDword, 8, …)(input must be ≥ 8 bytes so theinlen < 8check at0x129b11passes). - I/O Manager copies the input into
IRP->AssociatedIrp.SystemBuffer(METHOD_BUFFERED). - The driver's
MajorFunction[IRP_MJ_DEVICE_CONTROL]=SerialIoControl (sub_1297f8)runs. SerialIoControl (sub_1297f8)reads*SystemBufferintoeax(0x129b1c); if it exceeds[rsi+0x140]it callsExAllocatePoolWithTag(NonPagedPool, eax, 'AGSM')(0x129b35) with no upper bound.- Pool pointer stored at
[rbx+0x20];SerialStartOrQueue (sub_12c4b4)queues the IRP withSerialStartRead (sub_12b5b8). - A very large requested size drives a large or failing NonPagedPool allocation (memory pressure /
STATUS_INSUFFICIENT_RESOURCES). The buffer is sized to the request, so no overflow follows from this IOCTL alone.
Finding 2 — IOCTL 0x1b0044: Wait-Event Mask Validation
| Field | Value |
|---|---|
| Severity | Informational (validation present) |
| Class | IOCTL input validation, present and correct (CWE-20, negative finding) |
| Function | SerialIoControl (sub_1297f8) (IOCTL 0x1b0044 branch) |
| IOCTL | IOCTL_SERIAL_SET_WAIT_MASK (0x1b0044) |
| Primitive | None (validated event-mask store; no memory-safety impact) |
Root cause: The IOCTL_SERIAL_SET_WAIT_MASK handler validates the requested wait mask with (*input & 0xffffe000) == 0, rejecting any bit outside the low 13 bits (the defined SERIAL_EV_* event bits). Values that pass are handed to SerialStartMask (sub_12c6c0), queued via SerialStartOrQueue (sub_12c4b4), which stores the mask that later gates event-wait completions. The mask itself is not an address or register index; the only exposure is influencing which serial events satisfy a pending IOCTL_SERIAL_WAIT_ON_MASK.
Finding 3 — IOCTL 0x1b0064: SERIAL_HANDFLOW Structure Validation
| Field | Value |
|---|---|
| Severity | Informational (validation present) |
| Class | IOCTL input validation, present and correct (CWE-20, negative finding) |
| Function | SerialIoControl (sub_1297f8) (IOCTL 0x1b0064 branch) |
| IOCTL | IOCTL_SERIAL_SET_HANDFLOW (0x1b0064) |
| Primitive | None (validated flow-control settings applied under spinlock) |
Root cause: The IOCTL_SERIAL_SET_HANDFLOW handler reads the 16-byte SERIAL_HANDFLOW structure and validates each field: ControlHandShake against the reserved mask 0x7FFFFF84, FlowReplace against 0x7FFFFF20, rejects the illegal SERIAL_DTR/SERIAL_DTR_HANDSHAKE combination, and range-checks XonLimit and XoffLimit to be non-negative and no larger than the receive-buffer size at extension +0x140. Passing inputs are applied under the device spinlock via SerialSetHandFlow. This is the standard serial-framework validation; the values that survive it are legal flow-control settings.
3. Pseudocode Diff
GsDriverEntry (sub_12e334) — Before vs After
// ==================== UNPATCHED: GsDriverEntry (0x12e334) ====================
NTSTATUS GsDriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
__security_init_cookie(); // sets __security_cookie
return DriverEntry(DriverObject, RegistryPath); // call into full init (sub_12e008)
}
// DriverEntry (sub_12e008) does:
// DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SerialIoControl (sub_1297f8); // 0x12e1f3
// ... other Serial* IRP handlers, ModemLibDriverInit, SerialAddDevice ...
// IoCreateDevice(DriverObject, 0 /*DevExtSize*/, "\\Device\\LSISM_xface",
// 0x1b /*FILE_DEVICE_SERIAL_PORT*/, 0, FALSE /*Exclusive=0*/, &ctrlDev); // 0x12e2c3
// IoCreateUnprotectedSymbolicLink("\\DosDevices\\LSISM_xface", "\\Device\\LSISM_xface"); // 0x12e301
//
// Per-modem devices are created later in AddNewModem (sub_588f0):
// IoCreateDevice(DriverObject, 0x12be0 /*DevExtSize*/, "\\Device\\LSIModem<N>",
// 0x1b, 0, TRUE /*Exclusive=1*/, &modemDev); // 0x58a16
// IoCreateSymbolicLink("\\DosDevices\\LSIModem<N>", ...); // protected — 0x591cb / 0x5925b
// ==================== PATCHED: GsDriverEntry (0x1c0006020) ====================
NTSTATUS GsDriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
__security_init_cookie();
return DriverEntry(DriverObject, RegistryPath); // patched DriverEntry (0x1c0006008):
// return 0xC0000424; // STATUS_NOT_SUPPORTED — kill switch
}
SerialIoControl (sub_1297f8) — Reviewed IOCTL Paths (unpatched only)
// ============ IRP_MJ_DEVICE_CONTROL dispatcher ============
NTSTATUS SerialIoControl(PDEVICE_OBJECT dev, PIRP irp) // sub_1297f8
{
PIO_STACK_LOCATION sl = irp->Tail.Overlay.CurrentStackLocation;
ULONG code = sl->Parameters.DeviceIoControl.IoControlCode;
PVOID sysbuf = irp->AssociatedIrp.SystemBuffer;
ULONG inlen = sl->Parameters.DeviceIoControl.InputBufferLength;
switch (code) {
case IOCTL_SERIAL_SET_QUEUE_SIZE: { // 0x1b0008 — Finding 1
if (inlen < 8) return STATUS_BUFFER_TOO_SMALL;
SIZE_T n = (SIZE_T)*(uint32_t*)sysbuf; // requested receive-buffer size
if (n <= dev->Extension->BufferSize) break; // grow-only; +0x140
PVOID p = ExAllocatePoolWithTag(NonPagedPool, n, 'AGSM'); // no upper bound
sl->Parameters.DeviceIoControl.Type3InputBuffer = p; // scratch, +0x20
return SerialStartOrQueue(dev, irp, SerialStartRead); // sub_12c4b4 / sub_12b5b8
}
case IOCTL_SERIAL_SET_WAIT_MASK: { // 0x1b0044 — Finding 2
if (inlen < 4) return STATUS_BUFFER_TOO_SMALL;
uint32_t v = *(uint32_t*)sysbuf;
if (v & 0xFFFFE000) return STATUS_INVALID_PARAMETER; // reject bits >12
return SerialStartOrQueue(dev, irp, SerialStartMask); // sub_12c4b4 / sub_12c6c0
}
case IOCTL_SERIAL_SET_HANDFLOW: { // 0x1b0064 — Finding 3
if (inlen < 0x10) return STATUS_BUFFER_TOO_SMALL;
SERIAL_HANDFLOW *hf = sysbuf;
if (hf->ControlHandShake & 0x7FFFFF84) return STATUS_INVALID_PARAMETER;
if (hf->FlowReplace & 0x7FFFFF20) return STATUS_INVALID_PARAMETER;
if ((hf->ControlHandShake & 3) == 3) return STATUS_INVALID_PARAMETER;
if (hf->XonLimit < 0 || hf->XonLimit > dev->Extension->BufferSize)
return STATUS_INVALID_PARAMETER; // +0x140
if (hf->XoffLimit < 0 || hf->XoffLimit > dev->Extension->BufferSize)
return STATUS_INVALID_PARAMETER;
KeAcquireSpinLockRaiseToDpc(&dev->Extension->spin);
// SerialSetHandFlow applies the validated flow-control settings
break;
}
...
}
}
The patched binary contains no equivalent — the patched DriverEntry (0x1c0006008) returns STATUS_NOT_SUPPORTED before this code is ever wired up.
4. Assembly Analysis
SerialIoControl (sub_1297f8) — IOCTL Dispatch (unpatched)
; ============================================================
; agrsm64_unpatched!SerialIoControl — IRP_MJ_DEVICE_CONTROL
; ============================================================
; --- Preamble: rsi = dev ext, r12 = IRP, rbx = current IO stack location ---
; --- [r12+0x18] = AssociatedIrp.SystemBuffer ---
; >>>>>>>>> IOCTL 0x1b0008 — IOCTL_SERIAL_SET_QUEUE_SIZE <<<<<<<<<<<
0x129b11: cmp dword [rbx+0x10], 8 ; InputBufferLength < 8 ?
0x129b1a: jb loc_129b6d ; -> STATUS_BUFFER_TOO_SMALL (0xC0000023)
0x129b15: mov rax, [r12+0x18] ; rax = SystemBuffer
0x129b1c: mov eax, [rax] ; eax = requested size ← USER-CONTROLLED
0x129b1e: cmp eax, [rsi+0x140] ; vs current receive-buffer size
0x129b24: jbe loc_12a256 ; grow-only: skip alloc if size <= current
0x129b2f: mov r8d, 0x4d534741 ; Tag 'AGSM'
0x129b35: call ExAllocatePoolWithTag ; NonPagedPool, size — no upper bound
0x129b3b: mov [rbx+0x20], rax ; store ptr in IO stack location scratch (+0x20)
0x129b44: mov edi, 0xC000009A ; STATUS_INSUFFICIENT_RESOURCES on alloc failure
0x129b5c: lea rax, SerialStartRead ; start routine (sub_12b5b8)
0x12a28a: call SerialStartOrQueue ; sub_12c4b4 — queue IRP
; >>>>>>>>> IOCTL 0x1b0044 — IOCTL_SERIAL_SET_WAIT_MASK <<<<<<<<<<<
0x129bcd: cmp dword [rbx+0x10], 4 ; InputBufferLength < 4 ?
0x129bd7: test dword [rax], 0xFFFFE000 ; reject bits >12 (invalid SERIAL_EV_*)
0x129bdd: jnz loc_12a251 ; -> STATUS_INVALID_PARAMETER
0x129bf1: lea rax, SerialStartMask ; sub_12c6c0
; >>>>>>>>> IOCTL 0x1b0064 — IOCTL_SERIAL_SET_HANDFLOW <<<<<<<<<<<
0x129d85: cmp dword [rbx+0x10], 0x10 ; InputBufferLength < 16 ?
0x129d94: mov eax, [r14] ; ControlHandShake (r14 = SystemBuffer)
0x129d97: test eax, 0x7FFFFF84 ; reserved ControlHandShake bits
0x129da2: test dword [r14+4], 0x7FFFFF20 ; reserved FlowReplace bits
0x129db0: mov r13d, 3 ; r13b = 3
0x129db6: and eax, r13d
0x129db9: cmp al, r13b ; reject (ControlHandShake & 3) == 3 (SERIAL_DTR|SERIAL_DTR_HANDSHAKE)
0x129dd2: cmp [r14+8], eax ; XonLimit <= receive-buffer size ([rsi+140h])
0x129de6: cmp [r14+0xc], eax ; XoffLimit <= receive-buffer size
0x129e01: call KeAcquireSpinLockRaiseToDpc ; apply via SerialSetHandFlow
; ============================================================
; Patched GsDriverEntry (0x1c0006020) — entire driver is unreachable
; ============================================================
; call __security_init_cookie
; call DriverEntry ; 0x1c0006008: mov eax, 0xC0000424; retn
; retn ; STATUS_NOT_SUPPORTED
Side-by-side entry point (GsDriverEntry):
| Unpatched | Patched |
|---|---|
__security_init_cookie then jmp DriverEntry (sub_12e008) (full init) |
__security_init_cookie then call sub returning 0xC0000424 |
There is no per-function patch to the dispatcher; the dispatcher is prevented from existing in the patched build.
5. Trigger Conditions
To fire Finding 1 (IOCTL 0x1b0008 large NonPagedPool allocation):
- Driver loaded — verify
\Device\LSIModem0/\Device\LSISM_xfaceexist in WinObj, ordriverquery | findstr agrsm. - Open handle:
c HANDLE h = CreateFileW(L"\\\\.\\LSIModem0", // or L"\\\\.\\LSISM_xface" GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);Whether an unprivileged caller can open the device depends on the device DACL, which is the I/O-manager default (the driver sets no explicit security descriptor); it is not established by the binary.\Device\LSIModem<N>isExclusive=1, so a second opener is refused while a handle is held. - Compute IOCTL code:
c DWORD ioctl = CTL_CODE(FILE_DEVICE_SERIAL_PORT /*0x1b*/, 2, METHOD_BUFFERED, FILE_ANY_ACCESS); // == 0x1b0008 - Send crafted input:
c DWORD size = 0x100; // must exceed current receive-buffer size (ext +0x140) to force alloc BYTE payload[0x100] = { ... }; // controlled data for async completion DeviceIoControl(h, ioctl, payload, sizeof(payload), NULL, 0, &bytes, NULL);Input buffer must be ≥ 8 bytes so theinlen < 8check at0x129b1apasses; only the first DWORD (the size) is used. - Observable effect: a NonPagedPool allocation of the requested size is made (the receive FIFO). A very large size drives memory pressure or an allocation failure returning
STATUS_INSUFFICIENT_RESOURCES(0xC000009A). The buffer is sized exactly to the request and its contents come from received serial data, not from the IOCTL, so this IOCTL alone yields no out-of-bounds write.
To fire Finding 2 (IOCTL 0x1b0044 state write):
- Same handle acquisition.
DWORD code = CTL_CODE(0x1b, 0x11, METHOD_BUFFERED, FILE_ANY_ACCESS); // 0x1b0044DWORD value = 0x1FFF;(passes& 0xFFFFE000 == 0).- Send 4-byte input.
- The wait mask is updated; it only changes which serial events satisfy a subsequent
IOCTL_SERIAL_WAIT_ON_MASK. No memory-safety impact on its own.
To fire Finding 3 (IOCTL 0x1b0064 state corruption):
- Same handle.
DWORD code = CTL_CODE(0x1b, 0x19, METHOD_BUFFERED, FILE_ANY_ACCESS); // 0x1b0064- Construct 16-byte
SERIAL_HANDFLOW:c struct { uint32_t ControlHandShake, FlowReplace; int32_t XonLimit, XoffLimit; } in = { /* ControlHandShake */ 0x00000040, // (& 0x7FFFFF84) == 0 /* FlowReplace */ 0x00000000, // (& 0x7FFFFF20) == 0 /* XonLimit */ 0x00000010, // 0 <= XonLimit <= BufferSize (ext +0x140) /* XoffLimit */ 0x00000010, // 0 <= XoffLimit <= BufferSize }; - Send 16-byte input.
- The validated flow-control settings are applied under the device spinlock. Only in-range, legal handshake values are accepted; out-of-range limits are rejected with
STATUS_INVALID_PARAMETER.
6. Exploit Primitive & Development Notes
Primitive
The primitive reachable via IOCTL 0x1b0008 (IOCTL_SERIAL_SET_QUEUE_SIZE) is an attacker-controlled NonPagedPool allocation size:
- The attacker chooses the allocation size (any value greater than the current receive-buffer size, up to
0xFFFFFFFF); the buffer becomes the driver's receive FIFO. The IOCTL does not let the attacker choose the buffer's contents — the FIFO is filled by received serial data, not by the IOCTL input. - A very large size drives a large or failing
NonPagedPoolallocation (resource exhaustion / allocation-failure paths), rather than a controlled overflow. Turning this into pool corruption would require an independent overflow or size-mismatch bug in the FIFO handling that is not evidenced here.
IOCTL 0x1b0064 (IOCTL_SERIAL_SET_HANDFLOW) only applies validated, in-range flow-control settings; it is not a corruption primitive on its own.
Ceiling
The binary evidence supports only a memory-exhaustion / allocation-failure DoS ceiling for IOCTL 0x1b0008. There is no evidence in either build of an out-of-bounds write, use-after-free, or attacker-controlled buffer contents on this path, so no memory-corruption → code-execution primitive is established. The allocation and its grow-only gate match the stock Microsoft serial-framework IOCTL_SERIAL_SET_QUEUE_SIZE implementation.
7. Debugger PoC Playbook
Target: agrsm64_unpatched.sys loaded, with WinDbg/KD attached over net/serial.
Breakpoints
bp agrsm64_unpatched!SerialIoControl "Entry to IOCTL dispatcher; dump code"
bp agrsm64_unpatched!SerialIoControl+0x324 "0x129b1c — read of attacker-controlled size"
bp agrsm64_unpatched!SerialIoControl+0x33d "0x129b35 — ExAllocatePoolWithTag call site"
bp agrsm64_unpatched!SerialStartOrQueue "Queue routine that starts the IRP"
bp agrsm64_unpatched!AddNewModem "Device creation routine"
bp agrsm64_unpatched!AddNewModem+0x126 "0x58a16 — IoCreateDevice call (DeviceType 0x1b, Exclusive=1)"
bp agrsm64_unpatched!SerialIoControl+0x3df "0x129bd7 — IOCTL 0x1b0044 wait-mask check"
What to Inspect at Each BP
| BP | Register / Memory to inspect |
|---|---|
SerialIoControl (sub_1297f8) entry |
rcx = DeviceObject, rdx = IRP. dx @rdx+0xb8 to get current stack location, then dd <stackloc>+0x18 L1 for IoControlCode; dq @rdx+0x18 L1 for SystemBuffer pointer. |
+0x324 (0x129b1c) |
eax ← *SystemBuffer (attacker size). Confirm against your user input. |
+0x33d (0x129b35) |
Args to ExAllocatePoolWithTag: ecx = 0 (NonPagedPool), rdx = size, r8d = 'AGSM'. |
SerialStartOrQueue (sub_12c4b4) |
rcx = dev ext, rdx = IRP. The stored pool ptr is in the IO stack location scratch (+0x20). Trace into SerialStartRead (sub_12b5b8) to see how the receive buffer is used. |
AddNewModem (sub_588f0)+0x126 |
r9d = 0x1b (FILE_DEVICE_SERIAL_PORT DeviceType), edx = 0x12be0 (DevExtSize), [rsp+…Exclusive] = 1 (Exclusive). This device's symlink is created with IoCreateSymbolicLink (protected). IoCreateDevice takes no SecurityDescriptor; no IoCreateDeviceSecure/SDDL is used, so the device DACL is the I/O-manager default and is not visible in the binary. |
Key Offsets Cheat-Sheet
| Address | Meaning |
|---|---|
0x129864 |
IOCTL code dispatch (subtract/compare chain over 0x1b0004…0x1b0090) |
0x129b1c |
mov eax, [rax] — requested queue size from SystemBuffer |
0x129b1e |
cmp eax, [rsi+0x140] — grow-only vs current receive-buffer size |
0x129b35 |
ExAllocatePoolWithTag call (no upper bound) |
0x129bd7 |
test dword [rax], 0xFFFFE000 — IOCTL 0x1b0044 wait-mask check |
0x129d97 |
test eax, 0x7FFFFF84 — IOCTL 0x1b0064 ControlHandShake mask |
0x129dd2 |
cmp [r14+8], eax — IOCTL 0x1b0064 XonLimit <= BufferSize |
0x58a16 |
IoCreateDevice (DeviceType 0x1b, Exclusive=1; no SecurityDescriptor param) |
0x58dd0 |
DeviceObject->Flags |= DO_BUFFERED_IO |
0x12e301 |
IoCreateUnprotectedSymbolicLink call in DriverEntry (sub_12e008) |
Trigger Setup (User-Mode PoC Skeleton)
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#define FILE_DEVICE_SERIAL_PORT 0x1b
#define IOCTL_ALLOC CTL_CODE(FILE_DEVICE_SERIAL_PORT, 0x2, METHOD_BUFFERED, FILE_ANY_ACCESS) // 0x1b0008
#define IOCTL_WRITE CTL_CODE(FILE_DEVICE_SERIAL_PORT, 0x11, METHOD_BUFFERED, FILE_ANY_ACCESS) // 0x1b0044
#define IOCTL_STATE CTL_CODE(FILE_DEVICE_SERIAL_PORT, 0x19, METHOD_BUFFERED, FILE_ANY_ACCESS) // 0x1b0064
int main(void) {
HANDLE h = CreateFileW(L"\\\\.\\LSIModem0",
GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE) { printf("open fail %lu\n", GetLastError()); return 1; }
DWORD bytes;
// Fire Finding 1:
BYTE buf1[0x108] = {0};
*(DWORD*)buf1 = 0x100; // queue size (must exceed current receive-buffer size)
DeviceIoControl(h, IOCTL_ALLOC, buf1, sizeof(buf1), NULL, 0, &bytes, NULL);
// Fire Finding 2:
DWORD v = 0x1FFF;
DeviceIoControl(h, IOCTL_WRITE, &v, sizeof(v), NULL, 0, &bytes, NULL);
// Fire Finding 3 (SERIAL_HANDFLOW):
struct { DWORD ControlHandShake, FlowReplace; LONG XonLimit, XoffLimit; } s = { 0x40, 0, 0x10, 0x10 };
DeviceIoControl(h, IOCTL_STATE, &s, sizeof(s), NULL, 0, &bytes, NULL);
CloseHandle(h);
return 0;
}
Expected Observation
- Finding 1: an oversized queue-size request causes a correspondingly large
NonPagedPoolallocation (resource-exhaustion / potential allocation-failure DoS); the buffer becomes the receive FIFO. Watch thesizeargument atagrsm64!SerialIoControl+0x33d. - Finding 2: the wait mask is stored; no crash on its own. Confirm the new mask in the device extension.
- Finding 3: legal, in-range
SERIAL_HANDFLOWsettings are applied; out-of-range limits returnSTATUS_INVALID_PARAMETER. Observe the updated flow-control fields in the device extension.
Struct / Offset Notes
- Device extension size: 0x12be0 bytes.
+0x60,+0x68: modem line-control register shadows (set to0xFFFFin normal init).+0x140: current receive-buffer size — the grow-only threshold for IOCTL 0x1b0008 and the upper bound for the XonLimit/XoffLimit checks in IOCTL 0x1b0064.- Scratch used by dispatcher:
Type3InputBufferat+0x20of the current IO stack location. - Input length field in stack location:
Parameters.DeviceIoControl.InputBufferLengthat+0x10fromCurrentStackLocation.
8. Changed Functions — Full Triage
| Function | Sim | Change | Note |
|---|---|---|---|
GsDriverEntry (sub_12e334) |
0.2452 | Security-relevant | Unpatched calls the full DriverEntry (sub_12e008); patched DriverEntry (0x1c0006008) returns STATUS_NOT_SUPPORTED. The kill-switch. |
CSV92EXIntEnter (sub_843EC) |
0.8756 | Spurious match | The diff paired this unpatched interrupt-enter routine with a patched stub; no meaningful code change. |
__security_check_cookie (sub_E6FB0) |
0.9654 | Cosmetic | Security-cookie ROL/ROR check; only address deltas changed. |
sub_EC8CC |
0.4046 | Spurious match | Paired with the patched DriverEntry stub returning STATUS_NOT_SUPPORTED. |
sub_EF3B0 |
0.4046 | Spurious match | Paired with the patched __report_gsfailure (__fastfail(2)). |
Note on the matched-function table: the patched binary is a ~7 KB stub, so the diff's fuzzy matching pairs unpatched functions against the handful of remaining patched helpers (security cookie, fast-fail, the STATUS_NOT_SUPPORTED DriverEntry). Aside from the entry point, these pairings are artifacts of matching a full driver against a stub; the only real change is that DriverEntry short-circuits, so none of the modem/serial dispatch code is ever wired up.
9. Unmatched Functions
None reported by the diff. The patched image is a stub containing only the CRT/security helpers, the guard-icall thunks, GsDriverEntry, and a DriverEntry that returns STATUS_NOT_SUPPORTED. There is no added sanitizer for the IOCTL dispatcher — the dispatcher (and the device) simply do not exist in the patched build because the driver refuses to load.
10. Confidence & Caveats
Confidence: High that the patched build disables the driver wholesale (DriverEntry returns STATUS_NOT_SUPPORTED) and that the unpatched driver exposes a serial-class IOCTL dispatcher; the exploitability ceiling of the individual IOCTLs is a memory-exhaustion DoS, well below a memory-corruption primitive.
Rationale:
- The kill-switch patch is strong evidence the vendor chose to disable the driver wholesale rather than harden it.
- The device DACLs are not established by the binary: no IoCreateDeviceSecure call or SDDL string is present, so both devices receive the I/O-manager default security descriptor. IoCreateUnprotectedSymbolicLink (DriverEntry, 0x12e301) applies to the \DosDevices\LSISM_xface symlink object (deletion/replacement), not to device access; the \Device\LSIModem<N> device (IoCreateDevice at 0x58a16, DeviceType 0x1b, Exclusive=1) uses a protected symlink. No world-accessible-device escalation is demonstrated.
- The IOCTL handlers are the standard Microsoft serial-framework routines (SerialIoControl, SerialStartOrQueue, SerialStartRead, SerialStartMask, SerialSetHandFlow). The most notable exposure is the unbounded NonPagedPool allocation for the queue size at 0x129b1c–0x129b35 (IOCTL_SERIAL_SET_QUEUE_SIZE); the wait-mask and handflow paths are validated as in stock serial.sys.
Assumptions / Things to Verify Manually:
- Hardware presence —
agrsm64.systypically loads only if the corresponding Agere/LSI Soft Modem PCI device is enumerated. On a test VM without the hardware, you may need to force-load the driver (test-signing on) or use a fake PDO to exercise the dispatcher. Verify with!devnode 0 1and look for the modem PDO. - Device symlink index — the device may appear as
LSIModem0,LSIModem1, etc. Iterate indices inCreateFileWor read them fromHKLM\SYSTEM\CurrentControlSet\Services\AGRSM. - Async processing — the queued IRP runs in
SerialStartOrQueue (sub_12c4b4)/SerialStartRead (sub_12b5b8)on a worker thread or DPC. Any observable effect follows the queue/start, so delay after the IOCTL or drive receive traffic. - Call chain into
SerialStartRead (sub_12b5b8)is the start routine loaded before the tail call toSerialStartOrQueue; confirm with!stack/ubat the dispatcher return site. - Current value of dev ext
+0x140(receive-buffer size) — read it live withdd <devext>+0x140 L1to pick a larger value that forces the allocation in Finding 1.
Verify these in a snapshot VM with a kernel debugger attached before claiming a working exploit.