Geoff Chappell, Software Analyst
SKETCH OF HOW RESEARCH MIGHT CONTINUE AND RESULTS BE PRESENTED
The RTL_RVA_LIST structure is used internally by the kernel to support Control Flow Guard (CFG). Central to this feature is that each executable image can be built with a table of acceptable targets for indirect calls. Indeed, later versions of Windows 10 can have as many as three such tables, each for slightly different sorts of call (or jump). All these tables are reached through an IMAGE_LOAD_CONFIG_DIRECTORY structure (the load config) which is in turn located from the corresponding entry in the DataDirectory of the IMAGE_OPTIONAL_HEADER in what is widely known as the image’s PE header.
Each target is represented by its Relative Virtual Address (RVA), meaning its offset in bytes from the image base. When the image is first loaded, the kernel prepares its own representation of the image’s list of RVAs and their related flags, and keeps it handy as a saving on the expense of re-parsing from the load config.
The RTL_RVA_LIST structure looks to have been introduced for the 1703 release of Windows 10 to accommodate the CFG feature’s expansion of tables and flags.
Earlier versions, with just one table and no state to keep for each RVA, have a much simpler structure: a dword for the whole size, and then a compessed RVA list. Microsoft’s name for this structure is not known: both its production and interpretation were done by the Memory Manager, plausibly with no visibility outside one source file. The feature’s expansion for the 1703 release brought with it the complications of creating one ordered (compressed) list from multiple ordered (uncompressed) tables and of tracking such things as which table each RVA came from.
The RTL_RVA_LIST structure is not documented. Neither is it declared in any C-language header that Microsoft has published with any sort of software development kit that has yet been obtained for inspection. The structure’s name and the names and types of the structure’s members are known from the public symbol files for URLMON.DLL.
The RTL_RVA_LIST is 0x20 and 0x40 bytes, respectively, in 32-bit and 64-bit Windows.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x00 | 0x00 |
ULONG_PTR RvaCount; |
1703 and higher |
0x04 | 0x08 |
ULONG StateBitsPerRva; |
1703 and higher |
0x08 | 0x10 |
UCHAR *CompressedBuffer; |
1703 and higher |
0x0C | 0x18 |
ULONG_PTR CompressedBufferSize; |
1703 and higher |
0x10 | 0x20 |
RTL_BITMAP RvaStateBitMap; |
1703 and higher |
0x18 | 0x30 |
ULONG *StateBitValueMap; |
1703 and higher |
0x1C | 0x38 |
PVOID ExtensionBuffer; |
1703 and higher |
The aim of the RTL_RVA_LIST is to package an efficient representation of possibly very many 32-bit RVAs which each have some 32-bit non-zero state. The RTL_RVA_LIST is always created with additional material in one allocation from the paged pool:
The compressed RVA list, even in versions that predate the RTL_RVA_LIST structure, is one uncompressed dword that is the first RVA and then a sequence of bytes that encode the differences between successive RVAs. In each byte, the high two bits select a scaling factor (a power of 0x40) for the low six bits:
Byte | Scaling Factor |
---|---|
00xxxxxx | 0x00040000 |
01xxxxxx | 0x00001000 |
10xxxxxx | 0x00000040 |
11xxxxxx | 0x00000001 |
The sequence for any one difference ends with a byte that has both its high bits set. In no byte are the low six bits ever zero except when 0xC0 terminates the representation of a difference that is a whole multiple of 0x40. See that every difference that is less than 0x40 encodes to one byte and every other difference up less than 0x1040 encodes to two bytes. For almost all images in real-world conditions, the compressed RVA list will be something like half the size of the corresponding array of 32-bit RVAs or a good bit less.
Just as an efficient representation doesn’t keep an array of 32-bit RVAs, neither does it keep an array of 32-bit states for each RVA. The possible values for an arbitrary RVA’s state are bitwise combinations of the 32-bit component values in the StateBitValueMap. The number of these components is the StateBitsPerRva since each RVA’s state can be represented by a sequence of this many bits that select from the components. The states for all the RVAs in the same order as for the compressed RVA list is then the RvaStateBitMap.
When the component values are successive powers of two, the indirection through the StateBitValueMap is redundant and StateBitValueMap can instead be NULL.
When StateBitsPerRva is 1, then the expectation that the state is non-zero means that all RVAs have the same state. No space is prepared for the bitmap in this case (and RvaStateBitMap is not initialised). The state is the one value in the StateBitValueMap, defaulting to 1 if there is no StateBitValueMap.
The ExtensionBuffer is left uninitialised. No use of it is known.