1. Overview

  • Unpatched Binary: mup_unpatched.sys
  • Patched Binary: mup_patched.sys
  • Overall Similarity: 97.89%
  • Diff Statistics: 315 matched functions, 313 identical by content. The one functional change is in MupiPackUncHardeningPrefixTableForFsctl. Separately, DriverEntry drops its WIL feature-staging init/uninit calls and the associated wil_details_* helper routines are removed from the patched image (staging-library teardown, not security-relevant).
  • Verdict: The change in MupiPackUncHardeningPrefixTableForFsctl (unpatched 0x1C001AD4C, patched 0x1C001AA1C) adjusts the running offset that is reported back as IRP->IoStatus.Information on the STATUS_BUFFER_OVERFLOW and integer-overflow exit paths of the UNC provider enumeration loop. Because the caller zero-fills the entire output buffer before population in both builds, and because the reported length can only under-report on the unpatched path, there is no information disclosure and no out-of-bounds access. This is a length-reporting correctness change with no demonstrable security impact.

2. Change Summary

  • Severity: None (informational).
  • Nature: Reported output-length (IoStatus.Information) rounding/robustness on error exit paths.
  • Affected Function: MupiPackUncHardeningPrefixTableForFsctl (unpatched 0x1C001AD4C, patched 0x1C001AA1C).

What the function does

MupiPackUncHardeningPrefixTableForFsctl walks the MUP Unicode prefix table of registered UNC providers (RtlNextUnicodePrefix over PrefixTable) and copies each provider's record into a METHOD_BUFFERED output buffer via the per-entry helper MupiPackUncHardeningPrefixTableEntryForFsctl (unpatched 0x1C001ACA4, patched 0x1C001A974). A running byte offset is kept in edi/rdi. On return, that offset is written to *arg4, which the caller stores into IRP->IoStatus.Information.

The actual code change

There are two behavioural differences between the builds, both on non-success exit paths:

  1. STATUS_BUFFER_OVERFLOW (0x80000005) top-of-loop path. When the aligned start of the next provider record is >= the buffer size and at least one prior record was written, the function returns STATUS_BUFFER_OVERFLOW. The unpatched build jumps straight to the exit and returns edi unchanged (the end offset of the last written record, an unaligned value). The patched build first executes mov edi, esi to report the 16-byte-aligned offset instead.

  2. (rdi+0xF) integer-overflow path. If adding the 16-byte alignment slack wraps the 32-bit offset, the function returns STATUS_INVALID_BUFFER_SIZE (0xC0000095). The unpatched build leaves edi stale on this specific path; the patched build sets edi = 0xFFFFFFFF first.

Why this is not a security issue

  • The output buffer is fully zeroed before population, in both builds. The caller MupFsctlGetUncHardeningPrefixTable executes memset(SystemBuffer, 0, OutputBufferLength) (unpatched 0x1C0019AE9, patched 0x1C00197B9) prior to calling the packer. No uninitialized pool data is ever present in the returned buffer, so no uninitialized-memory disclosure is possible regardless of the reported length.
  • The reported length can only under-report on the unpatched path, never over-read. On the STATUS_BUFFER_OVERFLOW path the unpatched value is the end offset of the last successfully written record, which is strictly < OutputBufferLength. METHOD_BUFFERED completion copies min(IoStatus.Information, OutputBufferLength) bytes from the OutputBufferLength-sized system buffer, so there is no out-of-bounds read in either build. The patched value is the aligned offset, which is >= the buffer size on this branch and is clamped down by the I/O Manager to OutputBufferLength.
  • The integer-overflow path's return code is discarded by the caller. The caller only copies the offset into IoStatus.Information when the result is >= 0, == 0x80000005, or == 0xC0000023 (unpatched 0x1C0019AFD0x1C0019B13). The STATUS_INVALID_BUFFER_SIZE (0xC0000095) result falls through the jnz and the length is not written, so the difference on that path is not observable through this caller.

Net effect: on the buffer-overflow path the value the user sees in bytesReturned differs by up to the 16-byte alignment padding between builds. No data is leaked and no memory is corrupted.

Reachability

  1. A user-mode process obtains a handle to \Device\Mup (directly or via a UNC path).
  2. It issues FSCTL 0x100038 (device 0x10 FILE_DEVICE_MULTI_UNC_PROVIDER, function 0x0E, METHOD_BUFFERED) via NtDeviceIoControlFile/DeviceIoControl.
  3. The IRP reaches MupFsControl (0x1C000DE40), whose FSCTL switch dispatches case 0x100038 to MupFsctlGetUncHardeningPrefixTable (unpatched 0x1C0019ABC, patched 0x1C001978C).
  4. That routine zero-fills the system buffer, calls MupiPackUncHardeningPrefixTableForFsctl, and writes the returned offset to IRP->IoStatus.Information (IRP+0x38).

3. Pseudocode Diff

// UNPATCHED MupiPackUncHardeningPrefixTableForFsctl @ 0x1C001AD4C
int pack(void* unused, uint32_t buf_size, void* sys_buf, uint32_t* out_length) {
    uint32_t off = 0;                       // edi
    while ( (entry = RtlNextUnicodePrefix(PrefixTable)) != NULL ) {
        uint32_t aligned = (off + 0xF) & 0xFFFFFFF0;
        if ((off + 0xF) < off) goto ovf;    // wrap: exit WITHOUT setting off = -1
        if (aligned >= buf_size) {
            if (prior_success) {            // STATUS_BUFFER_OVERFLOW: exit with off UNCHANGED
                status = STATUS_BUFFER_OVERFLOW;
                goto exit;                  // off still = end of last record (unaligned)
            }
            // else: measure-only path (null ptr, remaining = 0)
        } else {
            off = aligned;                  // committed before packing this record
            // ... pack record, off += record_bytes ...
        }
    }
exit:
    *out_length = off;
    return status;
ovf:
    status = STATUS_INVALID_BUFFER_SIZE;    // off left stale
    goto exit;
}

// PATCHED MupiPackUncHardeningPrefixTableForFsctl @ 0x1C001AA1C
int pack(void* unused, uint32_t buf_size, void* sys_buf, uint32_t* out_length) {
    uint32_t off = 0;                       // edi
    while ( (entry = RtlNextUnicodePrefix(PrefixTable)) != NULL ) {
        uint32_t aligned = (off + 0xF) & 0xFFFFFFF0;   // esi
        if ((off + 0xF) < off) { off = 0xFFFFFFFF; goto ovf; }   // set off = -1 on wrap
        if (aligned >= buf_size) {
            if (prior_success) {
                status = STATUS_BUFFER_OVERFLOW;
                off = aligned;              // report aligned offset before exit
                goto exit;
            }
        } else {
            // uses rbp as the write pointer; off committed after packing
        }
    }
exit:
    *out_length = off;
    return status;
ovf:
    status = STATUS_INVALID_BUFFER_SIZE;
    goto exit;
}

4. Assembly Analysis

Unpatched exit paths (MupiPackUncHardeningPrefixTableForFsctl @ 0x1C001AD4C)

; top of loop
0x1C001ADD2  lea     eax, [rdi+0Fh]          ; aligned candidate = off + 15
0x1C001ADD5  cmp     eax, edi                ; wrap check
0x1C001ADD7  jb      loc_1C001AE8F           ; wrap -> STATUS_INVALID_BUFFER_SIZE (off NOT set to -1)
0x1C001ADDD  and     eax, 0FFFFFFF0h         ; align to 16
0x1C001ADE0  cmp     eax, r12d               ; aligned vs buffer size
0x1C001ADE3  jb      short loc_1C001ADF7     ; fits -> process
0x1C001ADE5  test    bpl, bpl                ; prior success?
0x1C001ADE8  jnz     loc_1C001AE7E           ; yes -> STATUS_BUFFER_OVERFLOW

; process path (eager commit of off)
0x1C001ADF7  mov     esi, eax
0x1C001ADFC  add     rsi, [rsp+58h+arg_10]
0x1C001AE01  mov     edi, eax                ; off = aligned (committed here)
0x1C001AE03  sub     edx, eax

; STATUS_BUFFER_OVERFLOW exit
0x1C001AE7E  mov     ebx, 80000005h
0x1C001AE83  jmp     short loc_1C001AE94     ; off (edi) left unchanged from prior iteration

; integer-overflow exit
0x1C001AE8F  mov     ebx, 0C0000095h         ; STATUS_INVALID_BUFFER_SIZE (off not forced to -1 on wrap-in path)

; final write
0x1C001AEC6  mov     [r13+0], edi            ; *arg4 = off

Patched exit paths (MupiPackUncHardeningPrefixTableForFsctl @ 0x1C001AA1C)

; top of loop
0x1C001AAA3  lea     esi, [rdi+0Fh]
0x1C001AAA6  cmp     esi, edi
0x1C001AAA8  jb      loc_1C001AB59           ; wrap -> handler that forces off = -1
0x1C001AAAE  and     esi, 0FFFFFFF0h
0x1C001AAB1  cmp     esi, r12d
0x1C001AAB4  jb      short loc_1C001AAC8
0x1C001AAB6  test    r15b, r15b
0x1C001AAB9  jnz     loc_1C001AB4B           ; STATUS_BUFFER_OVERFLOW

; process path (uses rbp for the write pointer, off deferred)
0x1C001AAC8  mov     ebp, esi
0x1C001AACD  add     rbp, [rsp+58h+arg_10]
0x1C001AAD2  sub     edx, esi

; STATUS_BUFFER_OVERFLOW exit
0x1C001AB4B  mov     ebx, 80000005h
0x1C001AB50  mov     edi, esi                ; off = aligned offset
0x1C001AB52  jmp     short loc_1C001AB63

; integer-overflow exit
0x1C001AB59  mov     edi, 0FFFFFFFFh         ; off = -1
0x1C001AB5E  mov     ebx, 0C0000095h

; final write
0x1C001AB9A  mov     [rsi], edi              ; *arg4 = off

5. Trigger Conditions

  1. Environment: the system has at least 2 registered UNC providers so more than one prefix-table record exists.
  2. Handle: open \Device\Mup directly or via a UNC path; standard user privileges suffice for the FSCTL dispatch (case 0x100038 in MupFsControl performs no RequestorMode gate).
  3. Request: DeviceIoControl/NtDeviceIoControlFile with FSCTL 0x100038 and an OutputBufferLength large enough for the first record but too small for the aligned second record.
  4. Effect: the function writes the first record, returns STATUS_BUFFER_OVERFLOW (0x80000005), and reports a length that differs between builds by up to the 16-byte alignment padding (unpatched: end offset of the last record; patched: the aligned offset).
  5. Observation: bytesReturned (IoStatus.Information) is an unaligned value on the unpatched build and a 16-byte-aligned value on the patched build. The bytes themselves are identical (zero-filled tail in both builds).

6. Impact Assessment

  • No information disclosure. The caller memsets the entire OutputBufferLength-sized system buffer to zero before population in both builds (unpatched 0x1C0019AE9, patched 0x1C00197B9). Any bytes beyond the written records are zero, so nothing from adjacent pool memory is returned regardless of the reported length.
  • No out-of-bounds read. METHOD_BUFFERED completion copies min(IoStatus.Information, OutputBufferLength) bytes from the system buffer, which is exactly OutputBufferLength in size. The unpatched stale value on the STATUS_BUFFER_OVERFLOW path is < OutputBufferLength; the patched aligned value is clamped down to OutputBufferLength. Neither reads past the allocation.
  • No memory corruption. The affected register is only used as the reported byte count; it does not drive any write.
  • Discarded path. The integer-overflow (0xC0000095) length difference is never propagated, because the caller only writes IoStatus.Information for results >= 0, 0x80000005, or 0xC0000023.

The change is a defensively-tidied length-reporting path; it does not deliver a security fix.

7. Verification Notes

The following breakpoints let a researcher confirm the length-reporting difference on a live system (there is no exploitable primitive to observe, only the reported byte count).

bp mup_unpatched!MupiPackUncHardeningPrefixTableForFsctl
bp 0x1C001AE7E   ; STATUS_BUFFER_OVERFLOW exit; edi holds the (unaligned) reported length
bp 0x1C001AEC6   ; mov [r13], edi -> *arg4, later stored into IRP->IoStatus.Information
  • At 0x1C001AE7E the unpatched edi equals the end offset of the last written record (unaligned).
  • At the equivalent patched exit 0x1C001AB50 (mov edi, esi), edi equals the 16-byte-aligned offset.
  • At 0x1C001AEC6 (unpatched) / 0x1C001AB9A (patched) the value is written to *arg4; MupFsctlGetUncHardeningPrefixTable stores it into IRP->IoStatus.Information (IRP+0x38).

User-mode probe against the unpatched target:

HANDLE hMup = CreateFileW(L"\\\\.\\Mup", GENERIC_READ | GENERIC_WRITE,
                          FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
BYTE outBuffer[64] = {0};
DWORD bytesReturned = 0;
BOOL ok = DeviceIoControl(hMup, 0x100038, NULL, 0, outBuffer, sizeof(outBuffer), &bytesReturned, NULL);
// On the STATUS_BUFFER_OVERFLOW case, bytesReturned is unaligned (unpatched) vs 16-byte-aligned (patched).
// The returned bytes are identical (zero-filled tail) in both builds.
printf("ok=%d, bytesReturned=0x%x\n", ok, bytesReturned);

8. Changed Functions — Full Triage

  • DriverEntry (0x1C0015884)
  • Change Type: WIL feature-staging teardown + WPP relocation. Not security-relevant.
  • Notes: The patched build removes the wil_InitializeFeatureStaging and wil_UninitializeFeatureStaging calls, and the associated wil_details_* helper routines are dropped from the image. The remaining differences are WPP trace-GUID symbol references and address relocations from the image-layout shift. No change to MUP request handling.
  • MupiPackUncHardeningPrefixTableForFsctl (unpatched 0x1C001AD4C, patched 0x1C001AA1C, similarity 0.9226)
  • Change Type: Length-reporting robustness. Not security-relevant.
  • Notes: On the STATUS_BUFFER_OVERFLOW and (rdi+0xF) integer-overflow exit paths, the patched build sets the reported offset (edi) to the aligned value / 0xFFFFFFFF rather than leaving it stale. The output buffer is zero-filled by the caller in both builds and the METHOD_BUFFERED copy is clamped to OutputBufferLength, so no disclosure or out-of-bounds access results from either behaviour.
  • Cosmetic-only changes (MupGetIoStatus, MupPostAcquireForSectionSynchronisation, MupStateMachine, MupiCreateProviderSymbolicLink, MupiSetProviderFlags, RfsCskvLexerStreamAdvance, WppClassicProviderCallback, the WPP_SF_* tracing stubs, memset, memmove, __memset_spec_*)
  • Change Type: Cosmetic / relocation.
  • Notes: Differences are limited to relocated branch targets, data-symbol references, and jump-table addresses caused by the image-layout shift. No semantic logic changes.

9. Added / Removed Functions

The patched image removes the WIL feature-staging helper routines that are no longer called after DriverEntry drops its staging init/uninit: wil_InitializeFeatureStaging, wil_UninitializeFeatureStaging, wil_details_BuildFeatureStateCacheFromQueryResults, wil_details_EvaluateFeatureDependencies, wil_details_EvaluateFeatureDependencies_GetCachedFeatureEnabledState, wil_details_EvaluateFeatureDependencies_ReevaluateCachedFeatureEnabledState, wil_details_FeatureDescriptors_SkipPadding, wil_details_PopulateInitialConfiguredFeatureStates, wil_details_ReevaluateOnFeatureConfigurationChange, wil_details_UpdateFeatureConfiguredStates. This is feature-staging-library teardown and is not security-relevant. No new functions were added.

10. Confidence & Caveats

  • Confidence: High. Both build-differences in MupiPackUncHardeningPrefixTableForFsctl are present at the stated addresses. The caller's memset of the full output buffer, the min(Information, OutputBufferLength) METHOD_BUFFERED clamp, and the caller's restriction of which result codes propagate IoStatus.Information are all confirmed in both builds and together eliminate any disclosure or out-of-bounds path.
  • Conclusion: The patch alters the reported IoStatus.Information byte count on error exit paths of the UNC provider enumeration. It is a correctness/robustness adjustment with no demonstrable security impact; it is not an information-disclosure or memory-safety fix.