msrpc.sys — MES type-pickling refactor and toolchain rebuild
KB5082142
1. Overview
- Unpatched Binary:
msrpc_unpatched.sys - Patched Binary:
msrpc_patched.sys - Overall Similarity Score:
0.6907 - Diff Statistics:
- Matched Functions: 1000
- Changed Functions: 628
- Identical Functions: 372
- Unmatched Functions (Unpatched/Patched): 0 / 0
Verdict: No security-relevant change is delivered in the NDR MES (Marshalling-Enabled Serialization) type-pickling functions. The apparent difference in NdrMesTypeDecode, NdrMesTypeDecode2, and NdrMesTypeAlignSize is a refactor: validation that the unpatched build performs inside helper functions (NdrpCommonTypeHeaderUnmarshall, NdrpCommonTypeHeaderSize) is inlined into the callers in the patched build, and the format-string dispatch changes from a single indirect call to direct calls for a few common handlers. The same validation and the same raised exceptions exist in BOTH builds. The rest of the 628 changed functions is dominated by a general toolchain/library rebuild (WIL feature-staging helpers, integer-overflow helper renames, memset intrinsic variants, Control Flow Guard dispatch thunks), not a targeted memory-safety fix.
2. Change Summary
Finding: MES type-header validation is inlined, not added
- Severity: None (no security-relevant change)
- Nature: Refactor / inlining + module rebuild
- Affected Functions:
NdrMesTypeDecode(unpatched0x1C0025E30, patched0x18003A3E0),NdrMesTypeDecode2(unpatched0x1C0026000, patched0x180039ED0),NdrMesTypeAlignSize(unpatched0x1C0034830, patched0x180039670)
What actually changed:
In the unpatched build, NdrMesTypeDecode validates the MES handle and format-string header by calling the helper NdrpCommonTypeHeaderUnmarshall (0x1C0025F68). That helper contains, in order:
- cmp dword ptr [rcx+190h], 1 / jnz → RpcRaiseException(0x723) — handle-state check
- when [rcx+1A1h] != 0: cmp [rcx+1C8h], 8 / jb → RpcRaiseException(0xC000000D) — minimum buffer-size check for non-fixed-buffer handles
- test dword ptr [rcx+1A0h], 40000h / jz → NdrpReadPicklingBuffer, then cmp byte ptr [rdx], 1 / jnz → RpcRaiseException(0x724) — format-string version check
In the patched build these same three checks, with the same offsets (+0x190, +0x1C8, +0x1A0) and the same exception codes (0x723, 0xC000000D, 0x724), are inlined directly into NdrMesTypeDecode (0x18003A430–0x18003A65D). The helper NdrpCommonTypeHeaderUnmarshall still exists in the patched build (0x18003A834) and is still called from the patched NdrMesTypeDecode2. No new check is introduced; the guard set is identical across both builds.
The only literal constant that differs is the size cap on the handle field at +0x4C:
- Unpatched: cmp dword ptr [rdi+4Ch], 0A0000h / ja → ExRaiseStatus(0x725)
- Patched: cmp dword ptr [rdi+4Ch], 0A000Ch / ja → ExRaiseStatus(0x725)
Because the comparison raises when the field is strictly greater than the constant, and the patched constant (0xA000C) is 12 larger than the unpatched constant (0xA0000), the patched build permits a slightly larger value than the unpatched build. This is a marginal relaxation of a size cap, not a security hardening, and there is no demonstrable memory-safety consequence in either direction.
The format-string dispatch also changed shape. The unpatched NdrMesTypeDecode special-cases only NdrPointerUnmarshall and otherwise performs a single guarded indirect call (call rax). The patched build additionally special-cases NdrNonEncapsulatedUnionUnmarshall, NdrSimpleStructUnmarshall, and NdrComplexStructUnmarshall with direct calls (_guard_dispatch_icall). The dispatch table lookup and the set of reachable handlers are unchanged; this is a Control Flow Guard / call-site devirtualization refactor, not a change in what is parsed or how bounds are enforced.
NdrMesTypeAlignSize follows the identical pattern: the unpatched build calls NdrpCommonTypeHeaderSize (0x1C00347AC), which already contains cmp dword ptr [rcx+190h], 0 / jnz → RpcRaiseException(0x723) and the [rcx+1A0h] & 0x80000 size accounting; the patched build (0x180039670) inlines that same logic. Same checks, same exception, same size cap relaxation.
NdrMesTypeDecode2 was changed by inlining the body of NdrMesTypeDecode into it (it no longer delegates via a subroutine call) and applying the same dispatch devirtualization. It still validates the handle (NdrpValidateMesHandle), applies the same +0x4C cap, and calls NdrpCommonTypeHeaderUnmarshall. No pre-validation was added; the unpatched build already validated through the callee.
3. Code Comparison
The validation the two builds enforce is the same; only its placement (helper vs. inlined) differs.
// === UNPATCHED helper NdrpCommonTypeHeaderUnmarshall (0x1C0025F68),
// called by NdrMesTypeDecode (0x1C0025E30) ===
void NdrpCommonTypeHeaderUnmarshall(MIDL_ES_MESSAGE* h)
{
if (h->state_190 != 1) RpcRaiseException(0x723);
if (h->nonfixed_1a1 && h->bufsize_1c8 < 8) RpcRaiseException(0xC000000D);
if (!(h->flags_1a0 & 0x40000)) {
NdrpReadPicklingBuffer(h, 8);
if (*(uint8_t*)h->buf_8 != 1) RpcRaiseException(0x724); // version
}
// ...
}
// === PATCHED NdrMesTypeDecode (0x18003A3E0) — same checks, now inlined ===
void NdrMesTypeDecode(MIDL_ES_MESSAGE* h, PFORMAT_STRING fmt, void* out)
{
NdrpValidateMesHandle(h);
if (out == NULL) ExRaiseStatus(0xC0030009);
if (h->size_4c > 0xa000c) ExRaiseStatus(0x725); // unpatched: 0xa0000
if (h->state_190 != 1) ExRaiseStatus(0x723);
if (h->nonfixed_1a1 && h->bufsize_1c8 < 8) ExRaiseStatus(0xC000000D);
if (!(h->flags_1a0 & 0x40000)) {
NdrpReadPicklingBuffer(h, 8);
if (*(uint8_t*)h->buf_8 != 1) ExRaiseStatus(0x724);
}
// ... format-string dispatch (same table, direct calls for common handlers)
}
4. Assembly Evidence
Unpatched NdrMesTypeDecode (0x1C0025E30) — validation path
00000001C0025E45 call NdrpValidateMesHandle
00000001C0025E4A cmp [rsp+38h+ppMemory], 0
00000001C0025E50 jz loc_1C003773E ; -> ExRaiseStatus(0xC0030009)
00000001C0025E56 cmp dword ptr [rdi+4Ch], 0A0000h
00000001C0025E5D ja loc_1C0037750 ; -> ExRaiseStatus(0x725)
00000001C0025E85 call NdrpCommonTypeHeaderUnmarshall ; state/bufsize/version checks
00000001C0025EEF movzx ecx, byte ptr [rsi] ; read format-string byte
00000001C0025F10 mov rax, ds:rva off_1C0004E90[r8+rcx*8] ; dispatch table
Unpatched NdrpCommonTypeHeaderUnmarshall (0x1C0025F68) — the checks already present
00000001C0025F6E cmp dword ptr [rcx+190h], 1
00000001C0025F78 jnz loc_1C0037866 ; -> RpcRaiseException(0x723)
00000001C0025F7E cmp byte ptr [rcx+1A1h], 0
00000001C0025F8A jz short loc_1C0025F98
00000001C0025F8C cmp [rcx+1C8h], edx ; edx = 8
00000001C0025F92 jb loc_1C0037871 ; -> RpcRaiseException(0xC000000D)
00000001C0025F98 test dword ptr [rcx+1A0h], 40000h
00000001C0025FA2 jz loc_1C003787C ; -> version-byte check -> 0x724
Patched NdrMesTypeDecode (0x18003A3E0) — same checks, inlined
000000018003A409 cmp dword ptr [rdi+4Ch], 0A000Ch
000000018003A410 ja loc_18003A608 ; -> ExRaiseStatus(0x725)
000000018003A430 cmp dword ptr [rbx+190h], 1
000000018003A437 jnz loc_18003A61A ; -> ExRaiseStatus(0x723)
000000018003A43D cmp byte ptr [rbx+1A1h], 0
000000018003A444 jz short loc_18003A453
000000018003A446 cmp dword ptr [rbx+1C8h], 8
000000018003A44D jb loc_18003A62C ; -> ExRaiseStatus(0xC000000D)
000000018003A453 test dword ptr [rbx+1A0h], 40000h
000000018003A45D jz loc_18003A63E ; -> version-byte check -> 0x724
The offsets (+0x190, +0x1A1, +0x1C8, +0x1A0) and exception codes (0x723, 0xC000000D, 0x724, 0x725) are byte-for-byte identical between the two builds; only the placement (helper call vs. inlined) and the size-cap constant (0xA0000 vs. 0xA000C) differ.
5. Trigger Conditions
No vulnerability is present, so there is no trigger. Both builds reject the same malformed handle states (wrong operation state, undersized non-fixed buffer, wrong format-string version) with the same exceptions before any type-specific unmarshalling routine runs. A handle whose state byte at +0x190 is not 1, whose non-fixed buffer at +0x1C8 is smaller than 8, or whose format-string version byte is not 1, is rejected in the unpatched build exactly as it is in the patched build.
6. Exploit Primitive & Development Notes
None. There is no reachable out-of-bounds read or write introduced or removed by this diff in the MES type-pickling path. The claimed type-confusion / OOB primitive would require the validation to be absent in the unpatched build; it is present. No exploit primitive is demonstrable.
7. Debugger Notes
Not applicable — there is no vulnerable state to reproduce. For reference, the validation both builds enforce can be observed at:
- Unpatched: helper NdrpCommonTypeHeaderUnmarshall at 0x1C0025F68 (checks at 0x1C0025F6E, 0x1C0025F8C, 0x1C0025F98).
- Patched: inlined into NdrMesTypeDecode at 0x18003A430, 0x18003A446, 0x18003A453.
Handle offsets referenced by both builds: +0x190 handle state (must be 1), +0x1A0 flags (bit 0x40000 gates the version check), +0x1A1 non-fixed-buffer flag, +0x1C8 non-fixed buffer size (must be ≥ 8), +0x4C size field (capped).
8. Changed Functions — Full Triage
- NdrMesTypeDecode: [Not security] Helper validation (
NdrpCommonTypeHeaderUnmarshall) inlined; format-string dispatch devirtualized to direct calls forNdrNonEncapsulatedUnionUnmarshall/NdrSimpleStructUnmarshall/NdrComplexStructUnmarshall; size cap0xA0000→0xA000C(marginal relaxation). Same guard set and exceptions in both builds. - NdrMesTypeDecode2: [Not security] Now inlines the
NdrMesTypeDecodebody instead of delegating via a subroutine call; still validates the handle and callsNdrpCommonTypeHeaderUnmarshall. No new pre-validation. - NdrMesTypeDecode3: [Not security] Wrapper; reaches the same decode path. No new validation.
- NdrMesTypeAlignSize: [Not security] Inlines the
NdrpCommonTypeHeaderSizelogic (state check+0x190→0x723and+0x1A0 & 0x80000size accounting are present in the unpatched helper too); same size cap relaxation. - NdrMesTypeEncode2: [Not security] Setup-then-delegate to
NdrMesTypeEncode; size arithmetic lives in the same downstream helpers in both builds. No delivered overflow fix demonstrable in this function. - Register-allocation / rebuild churn only:
NdrMesTypeAlignSize2,NdrMesTypeAlignSize3,NdrMesTypeFree2,MesBufferHandleReset, andNdr64AsyncClientCalldiffer by register allocation, stack-slot layout, and relocated call targets. No security-relevant logic change. - Module-wide rebuild: The bulk of the 628 changed functions reflects a general toolchain/library rebuild — WIL feature-staging helpers (
wil_details_*,Feature_*__private_IsEnabled*), integer-overflow helper renames (UIntAdd/ULongMult/ULongLongMult→ULongAdd/MultiplyWithOverflowCheck), memset intrinsic variants (__memset_spec_*), and Control Flow Guard dispatch thunks (_guard_dispatch_icall). These are servicing/rebuild churn, not a targeted memory-safety fix.
9. Unmatched Functions
- Added (patched only): rebuild-introduced helpers and thunks (e.g.
_guard_dispatch_icall,__memset_spec_*,MultiplyWithOverflowCheck, various LRPC/EEInfo helpers). None represents a security fix to the MES pickling path. - Removed (unpatched only): superseded helpers (e.g.
UIntAdd,ULongMult,ULongLongMult, severalwil_details_*andFeature_*staging helpers). Consistent with a toolchain/library rebuild.
10. Confidence & Caveats
- Confidence Level: High. The three checks the diff appears to "add" (
0x723,0xC000000D,0x724) are present in the unpatched build insideNdrpCommonTypeHeaderUnmarshall(0x1C0025F68) andNdrpCommonTypeHeaderSize(0x1C00347AC), at the same offsets and with the same exception codes; the patched build merely inlines them. The only literal constant change (0xA0000→0xA000C) relaxes rather than tightens the cap. - Caveat: This is a large rebuild (628 changed functions). The audit confirms the MES type-pickling functions carry no delivered security fix and that the surrounding churn matches a toolchain/library rebuild signature. A function-by-function review of all NDR marshalling routines was not exhaustively performed; no security-relevant change was identified in the functions this report covers.