Geoff Chappell - Software Analyst
The MMWSL is the Memory Manager’s modelling of a Working Set List. Every working set, as modelled by an MMSUPPORT, has one: it’s pointed to from the latter’s VmWorkingSetList member.
Windows was born with the notion of working sets for the management of virtual address space. All known versions have the MMWSL structure, but the 1607 edition of Windows 10 renames it as MMWSL_FULL. This comprises new MMWSL_INSTANCE and MMWSL_SHARED structures which each have a different selection of the old MMWSL members.
Some measure of the MMWSL structure’s importance in memory management is that it’s one of the few structures (in contrast to mere arrays of small types) that Windows builds at a preset address. Through much of the history of Windows, every process has its working set’s MMWSL at the same address:
Version | Address (x86) | Address (PAE) | Address (x64) |
---|---|---|---|
3.10 to 3.50 | 0xC0482000 | ||
3.51 to 5.0 | 0xC0502000 | 0xC0882000 | |
5.1 to early 5.2 | 0xC0503000 0xC0503800 |
0xC0883000 0xC0883800 |
|
late 5.2 | 0xC0502000 0xC0502800 |
0xC0881000 0xC0881800 |
0xFFFFF700`00081000 |
6.0 | 0xC0401000 | 0xC0801000 | |
6.1 to 6.2 | 0xC0402000 | 0xC0802000 | 0xFFFFF700`01080000 |
6.3 to 1511 | 0xC0802000 | 0xFFFFF580`10804000 |
The early 32-bit builds, both with and without support for Physical Address Extension (PAE), access the current process’s MMWSL through an internal variable named MmWorkingSetList which is initialised to this fixed address and is thereafter left alone. Versions 5.1 and 5.2 can vary this variable’s initialisation by 0x00000800 when a virtual bias applies, as for the /3GB boot option. Starting with version 6.1, the variable is discarded in favour of hard-coding the address. All known 64-bit kernels that have the structure hard-code the address.
Whether the predictability of this address for the MMWSL was ever exploitable is not known, but the 1703 release of Windows 10 does away with it.
As an internal structure with little, if any, visibility outside the kernel, the MMWSL varies greatly between versions and even between builds:
Version | Size (x86) | Size (PAE) | Size (x64) |
---|---|---|---|
3.10 | 0x0868 | ||
3.50 | 0x0468 | ||
3.51 to 4.0 | 0x0470 | ||
early 5.0 (before SP3) | 0x06A0 | 0x0D00 | |
late 5.0 (starting with SP3) | 0x06A8 | 0x0D08 | |
5.1 | 0x069C | 0x0CFC | |
5.2 | 0x0698 | 0x0CF8 | 0x80 |
early 6.0 (before SP1) | 0x06A8 | 0x0D08 | 0x0488 |
late 6.0 (starting with SP1) | 0x06B8 | 0x0D18 | 0x0498 |
6.1 | 0x06A8 | 0x0D08 | 0x0488 |
6.2 | 0x0D9C | 0x0530 | |
6.3 | 0x0DBC | 0x0260 | |
10.0 | 0x0E20 | 0x0300 | |
1511 | 0x0E48 | 0x0350 |
The preceding sizes, and the names, offsets and types in the tables that follow, are from Microsoft’s symbol files for the kernel, starting with Windows 2000 SP3. What little information is given below for earlier versions—even for early builds of version 5.0—is at best tentative. What’s known of Microsoft’s names and types for these versions is something of a guess. Where use of a member corresponds closely with that of a version for which Microsoft’s symbols are available, it seems reasonable to suppose continuity. Some use, however, has no correspondence, the code having changed too much. Even where the use hasn’t changed, tracking it down exhaustively would be difficult, if not impossible, even with source code—and I haven’t yet tried to track it down exhaustively.
For many of the integral members, the value is a 0-based index into an array of MMWSLE structures. This index was widened from 32 bits to 64 for 64-bit Windows 8.1. Microsoft almost certainly defines a type for this index. The KDEXTS.DLL debugger extension confirms with high confidence that Microsoft’s name for this type is WSLE_NUMBER. For the table below, WSLE_NUMBER is ordinarily a ULONG but is a ULONG_PTR in 64-bit Windows version 6.3 and higher.
Offset (x86) | Offset (x64) | Definition | Versions | Remarks |
---|---|---|---|---|
0x00 (3.10 to 5.1) |
ULONG Quota; |
3.10 to 5.1 | ||
0x04 (3.10 to 5.1); 0x00 |
0x00 |
WSLE_NUMBER FirstFree; |
3.10 to 1511 | next at 0x00 in MMWSL_SHARED |
0x08 (3.10 to 5.1); 0x04 |
0x04 (5.2 to 6.2); 0x08 |
WSLE_NUMBER FirstDynamic; |
3.10 to 1511 | next at 0x04 and 0x08 in MMWSL_SHARED |
0x0C (3.10 to 5.1); 0x08 |
0x08 (5.2 to 6.2); 0x10 |
WSLE_NUMBER LastEntry; |
3.10 to 1511 | next at 0x08 and 0x10 in MMWSL_SHARED |
0x10 (3.10 to 5.1); 0x0C |
0x0C (5.2 to 6.2); 0x18 |
WSLE_NUMBER NextSlot; |
3.10 to 1511 | next at 0x00 in MMWSL_INSTANCE |
0x14 (3.10 to 5.1); 0x10 (5.2 to 6.1) |
0x10 (5.2 to 6.1) |
MMWSLE *Wsle; |
3.10 to 6.1 | next at 0x9C and 0xB0 |
0x14 (6.0 to 6.1) | 0x18 (6.0 to 6.1) |
PVOID LowestPagableAddress; |
6.0 to 6.1 | next at 0x2C and 0x30 |
0x18 (3.10 to 5.0) |
ULONG NumberOfCommittedPageTables; |
3.10 to 5.0 | next at 0x28 | |
0x1C (3.10 to 3.50) | unaccounted | 3.10 to 3.50 | ||
0x20 (3.10 to 3.50); 0x1C (3.51 to 5.0); 0x18 (5.1); 0x14 (5.2); 0x18 (6.0 to 6.1); 0x10 |
0x18 (5.2); 0x20 (6.0 to 6.1); 0x10 (6.2); 0x20 |
WSLE_NUMBER LastInitializedWsle; |
3.10 to 1511 | next at 0x0C and 0x18 in MMWSL_SHARED |
0x1C (6.0) | 0x24 (6.0) |
WSLE_NUMBER NextEstimationSlot; |
6.0 only | |
0x20 (6.0); 0x1C (6.1); 0x14 |
0x28 (6.0); 0x24 (6.1); 0x14 (6.2); 0x28 |
WSLE_NUMBER NextAgingSlot; |
6.0 to 1511 | next at 0x04 and 0x08 in MMWSL_INSTANCE |
0x18 | 0x18 (6.2); 0x30 |
WSLE_NUMBER NextAccessClearingSlot; |
6.2 to 1511 | next at 0x08 and 0x10 in MMWSL_INSTANCE |
0x1C | 0x1C (6.2); 0x38 |
ULONG LastAccessClearingRemainder; |
6.2 to 1511 | next at 0x0C and 0x18 in MMWSL_INSTANCE |
0x20 | 0x20 (6.2); 0x3C |
ULONG LastAgingRemainder; |
6.2 to 1511 | next at 0x10 and 0x1C in MMWSL_INSTANCE |
0x24 | 0x24 (6.2); 0x40 |
ULONG WsleSize; |
6.2 to 1511 | next at 0x10 and 0x20 in MMWSL_SHARED |
0x24 (6.0) | 0x2C (6.0) |
ULONG EstimatedAvailable; |
6.0 only | |
0x28 (6.0) | 0x30 (6.0) |
ULONG GrowthSinceLastEstimate; |
6.0 only | |
0x2C (6.0); 0x20 (6.1) |
0x34 (6.0); 0x28 (6.1) |
ULONG NumberOfCommittedPageTables; |
6.0 to 6.1 | previously at 0x24 and 0x2C; next at 0x00 in MI_USER_VA_INFO |
0x30 (6.0); 0x24 (6.1) |
0x38 (6.0); 0x2C (6.1) |
ULONG VadBitMapHint; |
6.0 to 6.1 | previously at 0x34 and 0x44; next at 0x08 in MI_USER_VA_INFO |
0x20 (5.0); 0x1C (5.1); 0x18 (5.2); 0x34 (6.0); 0x28 |
0x1C (5.2); 0x3C (6.0); 0x30 (6.1); 0x28 (6.2); 0x48 |
WSLE_NUMBER NonDirectCount; |
5.0 to 1511 | next at 0x14 and 0x28 in MMWSL_SHARED |
0x24 (5.0); 0x20 (5.1); 0x1C (5.2) |
0x20 (5.2) |
MMWSLE_HASH *HashTable; |
5.0 to 5.2 | |
0x28 (5.0); 0x24 (5.1); 0x20 (5.2) |
0x28 (5.2) |
ULONG HashTableSize; |
5.0 to 5.2 | |
0x24 (3.10 to 3.50); 0x2C (3.51 to 5.0) |
KEVENT *ImageMappingPteEvent; |
3.10 to early 5.0 | ||
KEVENT ImageMappingPteEvent; |
late 5.0 only | |||
0x28 (5.1); 0x24 (5.2) |
0x2C (5.2) |
ULONG NumberOfCommittedPageTables; |
5.1 to 5.2 | previously at 0x18; next at 0x2C and 0x34 |
0x38 (late 6.0); 0x2C (6.1) |
0x40 (late 6.0); 0x34 (6.1) |
ULONG LastVadBit; |
late 6.0 to 6.1 | |
0x3C (late 6.0); 0x30 (6.1) |
0x44 (late 6.0); 0x38 (6.1) |
ULONG MaximumLastVadBit; |
late 6.0 to 6.1 | next at 0x1C in MI_USER_VA_INFO |
0x40 (late 6.0); 0x34 (6.1) |
0x48 (late 6.0); 0x3C (6.1) |
ULONG LastAllocationSizeHint; |
late 6.0 to 6.1 | next at 0x0C in MI_USER_VA_INFO |
0x44 (late 6.0); 0x38 (6.1) |
0x4C (late 6.0); 0x40 (6.1) |
ULONG LastAllocationSize; |
late 6.0 to 6.1 | next at 0x10 in MI_USER_VA_INFO |
0x2C | 0x30 (6.2); 0x50 |
PVOID LowestPagableAddress; |
6.2 to 1511 | previously at 0x14 and 0x18; next at 0x18 and 0x30 in MMWSL_SHARED |
0x38 (early 6.0); 0x48 (late 6.0); 0x3C (6.1); 0x30 |
0x40 (early 6.0); 0x50 (late 6.0); 0x48 (6.1); 0x38 (6.2); 0x58 |
MMWSLE_NONDIRECT_HASH *NonDirectHash; |
6.0 to 1511 | next at 0x1C and 0x38 in MMWSL_SHARED |
0x30 (early 5.0); 0x3C (late 5.0); 0x2C (5.1); 0x28 (5.2); 0x3C (early 6.0); 0x4C (late 6.0); 0x40 (6.1); 0x34 |
0x30 (5.2); 0x48 (early 6.0); 0x58 (late 6.0); 0x50 (6.1); 0x40 (6.2); 0x60 |
PVOID HashTableStart; |
5.0 to 5.2 | |
MMWSLE_HASH *HashTableStart; |
6.0 to 1511 | next at 0x20 and 0x40 in MMWSL_SHARED | ||
0x34 (early 5.0); 0x40 (late 5.0); 0x30 (5.1); 0x2C (5.2); 0x40 (early 6.0); 0x50 (late 6.0); 0x44 (6.1); 0x38 |
0x38 (5.2); 0x50 (early 6.0); 0x60 (late 6.0); 0x58 (6.1); 0x48 (6.2); 0x68 |
PVOID HighestPermittedHashAddress; |
5.0 to 5.2 | |
MMWSLE_HASH *HighestPermittedHashAddress; |
6.0 to 1511 | next at 0x24 and 0x48 in MMWSL_SHARED | ||
0x44 (late 5.0); 0x34 (5.1); 0x30 (5.2) |
0x40 (5.2) |
ULONG NumberOfImageWaiters; |
late 5.0 to 5.2 | |
0x38 (5.1); 0x34 (5.2) |
0x44 (5.2) |
ULONG VadBitMapHint; |
5.1 to 5.2 | next at 0x30 and 0x38 |
0x44 (early 6.0); 0x54 (late 6.0) |
0x48 (5.2); 0x58 (early 6.0); 0x68 (late 6.0) |
PVOID HighestUserAddress; |
6.0 only (x86); 5.2 to 6.0 (x64) |
The x86 builds of versions before version 6.2 end with arrays whose sizes vary with how many page tables can be needed for the whole of the user-mode address space. Description is eased by hypothesising MAX_USER_PAGE_TABLES as a symbolic name for this number:
Offset (x86) | Offset (PAE) | Definition | Versions | Remarks |
---|---|---|---|---|
0x28 (3.10 to 3.50); 0x30 (3.51 to 4.0); 0x40 (early 5.0); 0x48 (late 5.0); 0x3C (5.1); 0x38 (5.2); 0x48 (early 6.0); 0x58 (late 6.0); 0x48 (6.1) |
0x40 (early 5.0); 0x48 (late 5.0); 0x3C (5.1); 0x38 (5.2); 0x48 (early 6.0); 0x58 (late 6.0); 0x48 (6.1) |
USHORT UsedPageTableEntries [0x0400]; |
3.10 only | |
USHORT UsedPageTableEntries [MAX_USER_PAGE_TABLES]; |
3.50 to 6.1 | next at 0x3C in MI_USER_VA_INFO | ||
0x0828 (3.10); 0x0428 (3.50); 0x0430 (3.51 to 4.0); 0x0640 (early 5.0); 0x0648 (late 5.0); 0x063C (5.1); 0x0638 (5.2); 0x0648 (early 6.0); 0x0658 (late 6.0); 0x0648 (6.1) |
0x0C40 (early 5.0); 0x0C48 (late 5.0); 0x0C3C (5.1); 0x0C38 (5.2); 0x0C48 (early 6.0); 0x0C58 (late 6.0); 0x0C48 (6.1) |
ULONG CommittedPageTables [MAX_USER_PAGE_TABLES / 0x20]; |
3.10 to 6.1 | next at 0x0C3C in MI_USER_VA_INFO; last member in 3.10 to 6.1 (x86) |
The x64 builds of versions before 6.2 end very differently. The one member in common with the x86 builds is CommittedPageTables, but it is a pointer, not an array.
Offset (x64) | Definition | Versions | Remarks |
---|---|---|---|
0x50 (5.2); 0x60 (early 6.0); 0x70 (late 6.0); 0x60 (6.1) |
ULONG MaximumUserPageTablePages; |
5.2 to 6.1 | |
0x54 (5.2); 0x64 (early 6.0); 0x74 (late 6.0); 0x64 (6.1) |
ULONG MaximumUserPageDirectoryPages; |
5.2 to 6.1 | |
0x58 (5.2); 0x68 (early 6.0); 0x78 (late 6.0); 0x68 (6.1) |
ULONG *CommittedPageTables; |
5.2 to 6.1 | |
0x60 (5.2); 0x70 (early 6.0); 0x80 (late 6.0); 0x70 (6.1) |
ULONG NumberOfCommittedPageDirectories; |
5.2 to 6.1 | |
0x68 (5.2); 0x78 (early 6.0); 0x88 (late 6.0); 0x78 (6.1) |
ULONG *CommittedPageDirectories; |
5.2 only | |
ULONGLONG CommittedPageDirectories [0x80]; |
6.0 to 6.1 | next at 0x70 in MI_USER_VA_INFO | |
0x70 (5.2); 0x0478 (early 6.0); 0x0488 (late 6.0); 0x0478 (6.1) |
ULONG NumberOfCommittedPageDirectoryParents; |
5.2 to 6.1 | |
0x78 (5.2); 0x0480 (early 6.0); 0x0490 (late 6.0); 0x0480 (6.1) |
ULONGLONG CommittedPageDirectoryParents [1]; |
5.2 to 6.1 | next at 0x0470 in MI_USER_VA_INFO; last member in 5.2 to 6.1 (x64) |
Offset (x86) | Offset (x64) | Definition | Versions | Remarks |
---|---|---|---|---|
0x3C | 0x50 (6.2); 0x70 |
ULONG ActiveWsleCounts [8]; |
6.2 only | |
ULONG_PTR ActiveWsleCounts [8]; |
6.3 only | |||
ULONG_PTR ActiveWsleCounts [0x10]; |
10.0 to 1511 | next at 0x14 and 0x20 in MMWSL_INSTANCE | ||
0x5C (6.2 to 6.3); 0x7C |
0x70 (6.2); 0xB0 (6.3); 0xF0 |
MI_ACTIVE_WSLE ActiveWsles [8]; |
6.2 only | |
MI_ACTIVE_WSLE_LISTHEAD ActiveWsles [8]; |
6.3 only | |||
MI_ACTIVE_WSLE_LISTHEAD ActiveWsles [0x10]; |
10.0 to 1511 | next at 0x54 and 0xA0 in MMWSL_INSTANCE | ||
0x9C (6.2 to 6.3); 0xFC |
0xB0 (6.2); 0x0130 (6.3); 0x01F0 |
MMWSLE *Wsle; |
6.2 to 1511 | previously at 0x10; next at 0x30 and 0x50 in MMWSL_SHARED |
0xA0 (6.2 to 6.3); 0x0100 |
0xB8 (6.2); 0x0138 (6.3); 0x01F8 |
MI_USER_VA_INFO UserVaInfo; |
6.2 to 1511 |