AllocateContiguousMemorySpecifyCache/FreeContiguousMemory

Hi all,

I was having a look at a Windows third-party driver and I found an issue that results into kernel memory leak back to userland, but I believe there could be something more.

To be more specific, the driver does the following in those precise steps:

  1. Get a non-paged pool allocation via AllocateContiguousMemorySpecifyCache with MmCache flag
  2. Call IoAllocateMdl/MmBuildMdlForNonPagedPool to build an MDL for the next step
  3. Call MmMapLockedPagesSpecifyCache to map the non-paged pool page in userland
  4. Call MmFreeContiguousMemory to release the physical page(s)

Because the driver does not correctly initialize the non-paged pool page, this allows to leak memory to the mapped userland page.

Here starts the interesting part:

If someone requests 1 byte of allocation, the driver will only initialize 1 byte.

However, step1 will allocate an entire non-paged pool page, so the rest of the data can be leaked to userland.

Now, I can see that the non-initialized non-paged pool allocation contains copy of objects that are allocated in paged pool. My first impression is that calling AllocateContiguousMemorySpecifyCache causes that physical page to be paged out, or data to be moved to a different physical page, and then it is used to be mapped to non-paged pool which leaks that data.

In all my tests, I was able to allocate multiple objects that reside in paged pool and then abuse this flaw to leak them in userland via that non-paged pool allocation.

Trying to do anything with those objects in the userland mapping won’t have any effect in this case because it looks like this is just a copy of data previously resided in a physical page that is now paged-out or possible moved to a different page and now used by the non-paged pool allocation created through the AllocateContiguousMemorySpecifyCache . This is what I experienced by modifying those leaked objects. The original objects remained intact. I also checked the PFNs of the objects and I was able verify that the original objects were in a different physical page.

The driver will later call MmFreeContiguousMemory to release that physical page, assigned to the non-paged pool allocation, back to the system, but it never calls MmUnmapLockedPages to remove the mapping of those physical pages from userland.

So, my thinking was that those physical pages even though still ‘mapped’ in userland, they are now free to be reused by other processes or the kernel itself at will, which in that case we could actually modify interesting data that doesn’t necessarily belong to our process since we still hold the mapping to those physical pages.

However, during all my tests and after having mapped thousands of physical pages into my process, I never saw anything else reusing them while my process holds those mappings that occurred through the driver.

By looking at the process memory usage, I can tell that those are just mappings to the underlying physical pages because even after thousands and thousands of allocations my process will use very little memory which means those are not part of my process committed memory.

However, if someone attempts to do a memory dump of the process with process explorer or task manager, because those tools will dump also the memory viewed by those mappings, it results in a few GBs of dumped memory.

Here comes the 1 million dollars question
Since those physical pages appear to be only mapped and not committed, why don’t I see them ever being re-used by other tasks while my process is running?

Thank you all

PS: This is my first post here, so please don’t be too harsh. :slight_smile:

I have not read your post in detail, but the general presumption is that KM components have to be written ‘properly’ and not expose the system to vulnerabilities. KM code is trusted at the same level as the OS itself, so any driver call sequence that exposes arbitrary data to UM, would generally be considered a bug in that driver

Since those physical pages appear to be only mapped and not committed, why don’t I see them ever being re-used by other
tasks while my process is running?

…or, to rephrase it a bit, “why am I getting only a memory leak but not a memory corruption”.

In order to get a precise answer to this question, all you have to do is to download a copy of WRK, and check how MmMapLockedPagesSpecifyCache() is implemented

Anton Bassov

@MBond2 said:
I have not read your post in detail, but the general presumption is that KM components have to be written ‘properly’ and not expose the system to vulnerabilities. KM code is trusted at the same level as the OS itself, so any driver call sequence that exposes arbitrary data to UM, would generally be considered a bug in that driver

Thank you for taking the time to state the obvious and add nothing to this discussion. Looking at your overall activity in this forum, it’s obvious that you are jumping at random threads making comments without actually providing any help. Please don’t do this. Nobody will ever say “oh look at this guy. 276 comments. he must be 1337”. Nobody.

@anton_bassov said:

Since those physical pages appear to be only mapped and not committed, why don’t I see them ever being re-used by other
tasks while my process is running?

…or, to rephrase it a bit, “why am I getting only a memory leak but not a memory corruption”.

In order to get a precise answer to this question, all you have to do is to download a copy of WRK, and check how MmMapLockedPagesSpecifyCache() is implemented

Anton Bassov

Anton, thanks for providing another pointer to look into. I find this a quite interesting scenario, which doesn’t work as I originally expected. I believe this has to do more like how AllocateContiguousMemory works rather than MmMapLockedPagesSpecifyCache. For example, if you call ExAllocatePool to allocate a pool chunk and then you try to map this to userland, then you can directly modify the contents of that leaked kernel memory page from userland. Just to save you time, I know this in order to be done correctly the driver needs to allocate a buffer that is a multiple of the page size and initialize it to zero before mapping it to userland. Just stating this because nobody needs another MBond2-style reply. I will definitely have a look at the WRK. Thanks.

This is my first post here,

Thank you for taking the time to state the obvious and add nothing to this discussion. Looking at your overall activity in this forum,
it’s obvious that you are jumping at random threads making comments without actually providing any help.

Seems to be not so bad for a newcomer, don’t you think…

FYI, Marion has been posting to this NG for many,many years, and is generally known for providing useful answers and generating technically and intellectually exciting discussions. Ironically, what he said in his post is the only thing that actually matters in context of this discussion. More on it below

I believe this has to do more like how AllocateContiguousMemory works rather than MmMapLockedPagesSpecifyCache.

Well, they are both facets to the MM, and, apparently, call exactly the same internal routines and access exactly the same structures behind the scenes. Apparently, what happens here is that a call to MmMapLockedPagesSpecifyCache() simply modifies some bits in PFN descriptors of the target pages, and, as long as these bits maintain their state, the target pages cannot get repurposed. This is nothing more than just an implementation detail that may change from one OS version to another, so that it does not really matter… The only thing that really matters here is that your driver calls MmFreeContiguousMemory() without having had unmapped the target range, so that it does not really matter how this bug actually manifests itself (which may vary from one OS version to another)

Anton Bassov

@anton_bassov said:

Well, they are both facets to the MM, and, apparently, call exactly the same internal routines and access exactly the same structures behind the scenes. Apparently, what happens here is that a call to MmMapLockedPagesSpecifyCache() simply modifies some bits in PFN descriptors of the target pages, and, as long as these bits maintain their state, the target pages cannot get repurposed.

Thanks for the additional comments. I might have a more detailed look at this, but it seems that what you said is about right. Looks like those pages get marked and can’t be re-assigned while the mapping is still active.

The only thing that really matters here is that your driver calls MmFreeContiguousMemory() without having had unmapped the target range, so that it does not really matter how this bug actually manifests itself (which may vary from one OS version to another)

To make things clear, this is a third-party driver for which I don’t have the source code. I am doing RE, security research and exploit dev as part of my job. Since, in the past I had successfully exploited a similar issue, I wanted to see if this would be possible in this case as well.

However, since in this case the kernel—userland mapping occurs per page and not per pool chunk, it’s not possible to exploit this further because those pages are not re-used as I originally expected.

My previous findings, as explained in my previous post, confirm what you mentioned in your latest reply.

However, my nature tells me to not give up and spend some more time to find a way, if possible, to circumvent that behaviour.

Many thanks!

However, since in this case the kernel—userland mapping occurs per page and not per pool chunk, it’s not possible
to exploit this further because those pages are not re-used as I originally expected.

And what happens to these pages after the target app gets terminated? Do they get eventually reused, or are they gone forever (i.e. until the reboot)?

Anton Bassov

After the app gets terminated, the pages get eventually reused.

It’s interesting though that MmFreeContiguousMemory doesn’t really do what the documentation says:

The MmFreeContiguousMemory routine releases a range of physically contiguous memory that was allocated by an MmAllocateContiguousMemoryXxx routine.

Those pages won’t be reused for as long as the userland mapping exists, which in this case won’t be removed until the process is terminated.

This could be as well a security addition in recent NT kernel versions, to mitigate exploiting this further than privileged info disclosure.

However, this is just me thinking out loud since I haven’t tested this against earlier NT kernel builds.

Obviously, this is still quite a security issue because in my tests I managed to dump critical security information which otherwise wouldn’t be accessible by a non-privileged process.

Obviously, this is still quite a security issue because in my tests I managed to dump critical security information which otherwise
wouldn’t be accessible by a non-privileged process.

This is the reason why mapping driver-allocated memory to the userland is considered a poor and unsafe programming practice in the Windows NT world, and is generally frowned upon in this NG. Check the archives for more info - we had discussed it in detail quite a few times, so I am not going to regurgitate and rehash the same arguments over and over again. It may be not-so-obvious to someone who tends to think of the devices simply as of files that may be mmap()ed to the userland, but this is the way how it works under Windows NT

Anton Bassov

Yes, I am fully aware of what you are saying.

As I said, this is not something I developed. I just came across it during research.

The main motivation for this discussion was the fact that even though those pages were freed, wouldn’t be used again for as long the userland mapping was active. I wouldn’t have posted here if I hadn’t gone through a lot of analysis by playing with kernel objects for both paged and non-paged pool and so on. I thought I was missing something, but unfortunately in this case, I didn’t.

In a slightly different scenario that I encountered in the past, this programming mistake was exploitable to get an LPE in the affected host.

it’s a shame this is not the case for this one, as I had some fun with it during this research.

I guess I have to make me happy with a “privileged info leak” vulnerability in this case.

In any case, thanks for your replies.

I don’t know if is should be insulted or flattered by you description of my prowess; but consider the overall simplicity of the situation that you are describing and that we generally discuss proper engineering on this forum and not variously colored hat issues.

I have a tendency to ask the big picture why question more than to provide the technical minutia. And these days I do more UM programming than anything else, but I still volunteer my time to try to help others of all skill levels. Based on the language that each OP uses, I try to write responses that they will understand - and I’m sure that I get it wrong more that I get it right.

PS if you want to research my history on this form, the mbond2 is the result of something going wrong with my original mbond account. I think I joined around 2003 but I don’t remember exactly. I’m also sure that my posts were erratic a few years ago when I was undergoing cancer treatment. I like to think that I do better now but it is hard to be objective about ones own work

I guess I have to make me happy with a “privileged info leak” vulnerability in this case.

My point is that you found yourself in a position to get this info not because of the actual driver bug, but simply due to the very fact that a driver has mapped the memory that it has allocated, to the userland. If it did not call MmFreeContiguousMemory() while the mapping was still active you would still be in a position to capture this info, right…

Anton Bassov

My point is that you found yourself in a position to get this info not because of the actual driver bug, but simply due to the very fact that a driver has mapped the memory that it has allocated, to the userland. If it did not call MmFreeContiguousMemory() while the mapping was still active you would still be in a position to capture this info, right…

Yes, I know very well that the userland mapping not done correctly is the reason why I get the info leak. :slight_smile:

I know very well that the userland mapping not done correctly is the reason why I get the info leak.

I’ve got 2 questions for you,out of mere curiosity

  1. When and how can you do it? Are you able to capture this info only while the target process is still running, or only after it has been terminated, or both?
  2. Who is able to see this info? As long as it is available only to the processes running under the same user account (or a more privileged one), this is NOT a leak at all

Anton Bassov

When and how can you do it? Are you able to capture this info only while the target process is still running, or only after it has been terminated, or both?

I can’t go into further technical details at the moment because I haven’t disclosed the issue to the vendor as yet, but obviously you do all this through the exploit process while it’s running.

Who is able to see this info? As long as it is available only to the processes running under the same user account (or a more privileged one), this is NOT a leak at all

This is not about ‘who’ can see the info but ‘who can see what’. It can be leveraged to dump arbitrary kernel memory containing data that a non-privileged process wouldn’t be able to access otherwise.

obviously you do all this through the exploit process while it’s running.

Well, I am afraid I may disappoint you, but it looks like you haven’t discovered any vulnerabilities then. More on it below

It can be leveraged to dump arbitrary kernel memory containing data that a non-privileged process wouldn’t be able to access otherwise.

Leveraged by whom??? If it can get done by some process that runs under some other (presumably more limited) account than the one that your target process happens to be running under, and do it without any assistance from any higher-privileged processes, then this is, indeed, a vulnerability. However, as long as the attacker process runs under the same user account as the one it tries to exploit…well, then this is not a vulnerability but just a “feature” of Windows NT security model.

Under this model, you can open PROCESS_ALL_ACCESS handle to any process than runs under the same user account as yours, and do whatever you wish with it. This includes, but is not limited to, reading/writing its memory, as well as creating/terminating/changing context of threads that run in its address space. Therefore, as long as the attacker process runs under the same user account as the target one, all it has to do is to open a handle to it, to take a snapshot of its address space, and dump this snapshot to a file that may get subsequently opened by a restricted user. There is absolutely nothing that you can do about it

Anton Bassov

You missed the two most important parts of my reply: 1. Non-privileged process 2. Arbitrary kernel memory dump Just to save you time, I have written multiple kernel exploits in the past. I know how the NT security model works. I wouldn’t be here otherwise.

You missed the two most important parts of my reply:

  1. Non-privileged process
  2. Arbitrary kernel memory dump

Could you please provide some more details, preferably beyond the abstract “can be leveraged” platitude.

I have written multiple kernel exploits in the past.

Again, could you please provide some more details. Were they the practical real-life exploits, or were they just of “probably, may work in theory, but only in predefined and strictly-controlled lab settings” type “exploits” ? If you need some examples of the latter
(at least on my books), “Spectre” is the very first one that comes to my mind

Anton Bassov

I will be happy to provide further technical details once I’ve gone though the coordinated disclosure process with the vendor.

At that point, I will most probably publish a write-up about it with more information over the bug and the impact in the context of security.