Wednesday, May 15, 2013

Introduction to Windows Kernel Security Research

A few months ago, I mentioned a crash I'd encountered under memory pressure on windows. I was hoping sharing a reproducer might stimulate someone who was interested in learning about kernel debugging to investigate, learning some new skills and possibly getting some insight into researching security issues, potentially getting a head start on discovering their own.

Sadly, I've yet to hear back from anyone who had done any work on it. I think the path subsystem is obscure enough that it felt too daunting a task for someone new to jump into, and the fact that the reproducer is not reliable might have been scary. I've decided to help get you started, hopefully someone will feel inspired enough to continue. Before reading this post, I assume you're comfortable with kd, IDA Pro and debugging without source code.

First some background, hopefully you're already familiar with GDI basics and understand Pens, Brushes and so on. Paths are basically shapes that can be created by moving between points, they can be straight lines, or irregular and composed of curves, arcs, etc. Internally, a path is stored as a linked list of it's components (points, curves, etc).

The path subsystem is very old (pre-NT?), and uses it's own object allocator called PATHALLOC. PATHALLOC is relatively simple, it's backed (indirectly) by HeavyAllocPool() with the tag 'tapG', but does include it's own simple freelist implementation to reduce calls to HeavyAllocPool.

The PATHALLOC entrypoints are newpathalloc() and freepathalloc(), if you look at the code you'll see it's very simple and easy to understand. Notice that if an allocation cannot be satisfied from the freelist, newpathalloc will always memset zero allocations via PALLOCMEM(), similar to what you might expect from calloc.

However, take a look at the freelist check, if the allocation is satisfied from the freelist, it skips the memset zero. Therefore, it might return allocations that have not been initialized, and callers must be sure to do this themselves.

It turns out, getting an object from the freelist happens quite rarely, so the few cases that don't initialize their path object have survived over 20 years in NT. The case we're going to take a look at is EPATHOBJ::pprFlattenRec().

Hopefully you're familiar with the concept of flattening, the process of translating a bezier curve into a sequence of lines. The details of how this works are not important, just remember that curves are converted into lines.

EPATHOBJ::pprFlattenRec() is an internal routine for applying this process to a linked list of PATHRECORD objects. If you follow the logic, you can see that the PATHRECORD object retrieved from newpathrec() is mostly initialized, but with one obvious error:

The next pointer is never initialized! Most of the time you want a new list node to have the next pointer set to NULL, so this works if you don't get your object from the freelist, but otherwise it won't work!

How can we verify this hypothesis? Let's patch newpathrec() to always set the next pointer to a recognisable
value, and see if we can reproduce the crash (Note: I don't use conditional breakpoints because of the performance overhead, if you're using something fancy like virtualkd, it's probably better to use them).

This looks like convincing evidence that a newpathrec() caller is not initialising new objects correctly. When bFlatten() tried to traverse the linked list of PATHREC objects, it doesn't find the NULL it was expecting.

We know the PATHREC list starts at EPATHOBJ+8, the pointer to the first element is in PATHREC+14, and the next pointer is at +0, so we can look at the chain of PATHREC objects that caused bFlatten() to crash in kd.

The standard process for researching this kind of problem is to implement the various primitives we can chain together to get the behaviour we want reliably. These are the building blocks we use to control what's happening.

Conceptually, this is something like:

Primitive 1: Allocate a path object with as much contents controlled as possible.
Primitive 2: Get that path object released and added to the freelist.
Primitive 3: Trigger the bad allocation from the freelist.

Once we have implemented these, we can chain them together to move an EPATHOBJ reliably into userspace, and then we can investigate if it's exploitable. Let's work on this.

POINTFIX is documented in msdn as "A point structure that consists of {FIX x, y;}.". Let's try adding a large number of points to a path consisting of recognisable values as x,y coordinates with PolyBezierTo() and see if we can find it.

If we break during the bounds checking in newpathrec(), we should be able to dump out the points and see if we hit it.

This confirms the theory, that this trick can be used to get our blocks on the freelist. I spent some time making this reliable.

Spamming the freelist with our POINTFIX structures
Lets make sure that our paths are full of curves with these points, and then flatten them to resize them (thus reducing the number of points). If it works, just by chance we should start to see the original testcase crashing at recognisable addresses.

Success! Now we have at least some control over the address, which is something to work with. You can see in EPATHOBJ::bFlatten() the object is handed over to EPATHOBJ::pprFlattenRec(), which is a good place to start looking to look for exploitation opportunities, possibly turning this into code execution.

You can copy my portable shellcode from my KiTrap0D exploit if you need one, which should work reliably on many different kernels, the source code is available here:

If nobody jumps in, I'll keep adding more notes. Feel free to ask me questions if you get stuck or need a hand!

If you solve the mystery and determine this is a security issue, send me an email and I'll update this post. If you confirm it is exploitable, feel free to send your work to Microsoft if you feel so compelled, if this is your first time researching a potential vulnerability it might be an interesting experience.

Note that Microsoft treat vulnerability researchers with great hostility, and are often very difficult to work with. I would advise only speaking to them under a pseudonym, using tor and anonymous email to protect yourself.