Reading another process memory via page combining in Windows 10

Windows page combining

It's not as scary as it looks, and you're not able to read memory of any arbitrary process. At least without some preconditions. The main vulnerability is almost completely fixed now. Therefore, this post is rather a historical reference and is offered for your self-improvement. In addition, to my knowledge, no one has yet described the exploitation method that I offer.

To begin with, Microsoft was notified about this problem around a year and a half ago. In response, they told me the vulnerability had been fixed mostly, and that I can publish my small research.

So let's begin. Windows 8.1 and Windows 10 brought a memory or page combining feature at some point (the Windows Internals, 7th edition, part 1 book describes it in detail). The idea is quite simple: every 15 minutes the operating system searches the physical memory for pages with the same content and combines them into a single one in order to save RAM. Those processes that owned the same pages receive links to a new shared page with the read-only and copy-on-write attributes. If any process changes its page, a copy-on-write exception occurs, and the system copies the page again to physical memory, and the process receives an individual copy of this page.

This feature has been enabled by default for some time. As you may guess, absolutely amazing ways to attack the system have been developed based on it. You're strongly advised to read the "Dedup Est Machina" paper. It describes a way to take control of Microsoft Edge, as well as read private parts of nginx memory. Both attacks are remote! Although mostly irrelevant now, the material is very interesting and easy to read, despite its academic style.

I will briefly describe the main idea of the attack. After the system had combined physical pages with identical content into a single page (this happened every 15 minutes), and some of the processes that owned one of these pages tried to write to it, this took noticeably longer time than usual (because the page is first copied from shared memory to memory of the process). You can measure this time and thus determine whether someone else on the system uses the same data from this page. Simply speaking, if some process has a memory page that stores the password "123" that is of our interest, we can create a large number of pages with contents like "000", "001", "002", ..., "122", "123", "124", ..., "999". Next, 15 minutes later, when the system performs combining, we try to change the contents of each of these pages. Since our page "123" has been combined with another page (as their contents are the same), our process can see that the write operation takes significantly longer time. Based on this, our process is able to conclude that the contents of the "123" page are being held by some another process on the system, which means that this is exactly the password we are looking for. This one is a peculiar brute-force method. The technique described in the paper above, of course, is much more complicated, it uses a whole combination of methods and attacks to implement a full-fledged data leak.

Fortunately, Microsoft turned off memory combining and the problem was resolved. Combining of zero pages can be still performed in some cases, but it's generally safe. However, the original memory combining hasn't been removed from the kernel and, moreover, the system administrator can easily enable it. To do this, you could previously use the undocumented NtSetSystemInformation function (Windows Internals GitHub contains the full implementation), but the SystemCombinePhysicalMemoryInformation class has been disabled or removed from newer Windows 10 versions (or its index has been changed).

However, there is an even simpler and documented way to activate memory combining: this is the Enable-MMAgent -PageCombining PowerShell command, which should be run with administrator privileges. Memory combining can be disabled with the Disable-MMAgent -PageCombining command, respectively. You can get the current state of page combining using the Get-MMAgent command. There is a pitfall: the server administrator can enable this option in order to optimize memory consumption and, thus, open a vulnerability in the system. Servers often run many similar virtual machine instances with more or less similar memory contents, and such optimization may make sense from the administrator's point of view. Microsoft documentation, in turn, doesn't mention anywhere that turning this feature on can lead to big problems. For example, here's the documentation page on Enable-MMAgent. It doesn't tell anything about that pitfall:

Indicates that the cmdlet enables page combining.
If you do not specify this parameter, page combining remains in its current state, either enabled or disabled.

And here's another documentation page on the Microsoft website, which mostly describes memory combining advantages, and still says nothing about potential data leaks:

Enabling page combining may reduce memory usage on servers which have a lot of private, pageable pages with identical contents. For example, servers running multiple instances of the same memory-intensive app, or a single app that works with highly repetitive data, might be good candidates to try page combining. The downside of enabling page combining is increased CPU usage.

Here are some examples of server roles where page combining is unlikely to give much benefit:

  • File servers (most of the memory is consumed by file pages which are not private and therefore not combinable)
  • Microsoft SQL Servers that are configured to use AWE or large pages (most of the memory is private but non-pageable)

Page combining is disabled by default but can be enabled by using the Enable-MMAgent Windows PowerShell cmdlet. Page combining was added in Windows Server 2012.

The only drawback mentioned is increased CPU usage. So, administrators may try this setting on their servers. Thus, Microsoft documentation on this subject definitely could be better. I would suggest that they either delete these sections altogether, or explicitly indicate that this setting is outdated and should always remain disabled.

You can also enable Page Combining directly via the WMI by executing the following command (this also requires administrator privileges):

The Enable method with the PageCombining parameter is executed inside one of the svchost processes (the SysMain/Superfetch service) and changes some values of the HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Superfetch registry branch (possibly also reconfiguring the Windows memory manager).

But back to the paper that I mentioned earlier. It describes two scenarios of a remote attack on the system (via JavaScript in Edge or through external HTTP requests in nginx), but does not describe how to obtain information from another process on the system. Both attacks are designed for a remote attacker who can change memory of the attacked process (by executing JavaScript in Edge or sending HTTP requests to nginx). Suppose that our process with limited rights is already running on the system. I'll describe how a side-channel data transmission from another process can be organized. I won't measure time of write operations, I'm going to propose an alternative way. Also, I won't read real applications memory, and instead write a couple of experimental synthetic programs just to demonstrate how this can be implemented.

I'll code in C++, and I'll start with the process that will contain secret information in its memory, so its contents should never be accessible to other processes, and especially to ones with limited privileges. We will pass several numbers to the input of our program, and this will be the very secret that no one else should know. Let's start with the main function:

Everything is clear for now: we parse a list of numbers (which can be from 0 to 9999 inclusive) from the command line, and then somehow keep them in memory pages. The code of the keep_numbers_in_memory_pages function is the next point of interest. Let's add it to the anonymous namespace declared earlier:

First, we create a list (std::vector) of memory page addresses that we'll prepare for our secret numbers. Each individual number will be placed in its own memory page, and we allocate them in a loop using the VirtualAlloc function (inside the prepare_page function). We ask the system for 1 byte of memory, but Windows will automatically return a chunk of memory of a virtual page size (usually 4096 bytes), since we did not specify an address (we passed nullptr). Next, we copy the memory_pattern string and the string representation of the number being processed to this chunk. After that, in an endless loop, we access each allocated page so that the system does not cache them to disk and does not compress them, leaving them in physical memory. memory_pattern was chosen in a random way, so that no other process on the system would have the same memory pages and would interfere with our experiment.

As you can see, the program is synthetic, but nevertheless, it simply stores some pages of memory without sharing them with anyone, without trying to transfer their contents to other processes in any way. That is, the developer can expect the memory of this program be completely safe, and no other process without administrator privileges should be able to find out the secret numbers.

Let's move on to writing a second program that will try to read the secret numbers from the first process memory. To avoid duplication, we add new functions directly to the code that we already have:

Now, if no command line arguments are passed, the process will start in the leak mode, keeping the secret numbers in its memory. Otherwise, the code to read the secret memory will be executed. Both programs can be launched using a single EXE file, this is more convenient, but not critical for the exploitation. Let's now focus attention on the try_read_numbers_from_another_process function. First, we need the auxiliary memory_page_info structure, where we'll store memory page information:

In the structure constructor, we save a secret number, which is stored in the related page, and the address of this page. Then we request some information about the memory page using the QueryWorkingSetEx function and save this information in the prev_ws_info field. If the page is already shared with some processes, then the accuracy of reading another process memory may be reduced. We'll talk about this later.

Let's move on to the try_read_numbers_from_another_process function. I'll write it in the same anonymous namespace, immediately after the memory_page_info structure:

First, we allocate memory pages for each of the possible secret numbers, and then, just like the first victim program does, we keep them in memory in a loop so that the system doesn't cache them. In the same loop, we regularly check the attributes of each page. If at some point we see that the page has suddenly became shared, this can only mean one thing: Windows has combined this memory page with some page of another process. And this implies that another process has a memory page with exactly the same content as our spy process. And that's the way we find out what numbers were passed to the victim process! We don't really need to write to pages and measure the time, the system directly reports when it combined the pages via the QueryWorkingSetEx API! We can also find out when the number of shared copies of a page has changed. The number of links is stored in the ShareCount field, but it's three-bit wide, with possible values from 0 to 7 inclusive, so its usage can be limited in case there are more than seven identical copies of the page in memory. I advise to study the PSAPI_WORKING_SET_EX_BLOCK union to see what other useful information QueryWorkingSetEx returns.

Currently my method still works, but Windows limits page combining to processes of a particular user. If you have two processes owned by different users, the first process isn't able to read the secrets of the second one, since their pages are not going to be combined. Accordingly, a limited process cannot read the memory of a privileged one. Earlier, when I'd just sent the issue to Microsoft, Windows have been still combining elevated processes and filtered tokens processes memory pages (when these processes were owned by the same privileged user). That is, if you are a privileged user and run one process simply by double-clicking, and the second by explicitly choosing "Run as administrator" option with UAC confirmation, the first process could read the memory of the second one. This seems fixed in recent Windows 10 versions, although at the time when I discovered this, Microsoft informed me they didn't plan to do anything about it.

OK, so, it's still possible to read other processes memory given the same user with the same rights runs them (this user can be limited).

Let's compile the program and test it in action on one of the latest publicly available at the time of writing Windows 10 versions (2004 19041.329). For this, I created a separate user with limited privileges, and I'll run the test program as follows:

Page combining spy process

Now I'll run another instance of our test program (victim) for the same limited user. But now I'll pass several numbers to it:

Page combining victim process

If Microsoft had not by default turned off automatic memory combining, the vulnerability would have revealed itself in the next 15 minutes. We force memory combining by executing the command Enable-MMAgent -PageCombining (described above) in the privileged PowerShell console:

Enable page combining using PowerShell

Here's what we'll see right after that in the first console of the spy process:

Page combining spy - values leaked

The spy process have stolen the set of the victim process secret numbers!

The same thing would happen if an administrator had previously enabled memory combining. We would not have to execute Enable-MMAgent -PageCombining, and the leak would have happened in the next 15 minutes regardless. Now, since the pages of both processes are shared, when I close the victim process, this is what the spy process displays:

Page combining spy - numbers leaked again

The spy process has detected the victim process exited and freed its memory pages, as they no longer possess the shared attribute. Such leak does not require the use of any suspiocious APIs and therefore will not be noticed by any antivirus software.

Download the full source code of the example and its compiled binary (dxdxdx is the password for the archive).

In conclusion, I will recommend that Windows users and system administrators check page combining is disabled on the system, and never enable it. With regards to Microsoft, it would be nice for them to completely remove this feature (leaving zero-page combining only), or at least add some warnings to the documentation regarding its potential danger.

P.S. There is such feature in Linux, too, which is called kernel same page merging (KSM). I didn't examine it in detail, but it works differently and, it seems, its developers are already aware of potential security problems.

Leave a Reply

Your email address will not be published. Required fields are marked *