Windows System Software -- Consulting, Training, Development -- Unique Expertise, Guaranteed Results

Tracking an NTSTATUS to its Source

Tracking an NTSTATUS to its Source

I found myself in a situation this week where I really wanted to call the API SeTokenIsAdmin. I vaguely remembered some issues around this API, and Googling quickly brought up a couple of threads from NTDEV and NTFSD hinting at a security issue that was fixed in 2015:

(Yes, I was on one of those threads…No, I did not remember it until now…)

The details here weren’t really sufficient to know if I was going to be bitten by this problem in my use case or how to properly resolve it. However, Google’s Project Zero filled in the blanks for me and even provided some POC code:

And, sure enough, this code passes on Windows 7 SP1 RTM but fails with 0xC00000A5 (STATUS_BAD_IMPERSONATION_LEVEL) on Windows 10 1703.

That’s all good because I don’t really care about old versions of Windows 7. However, after all this discussion it still wasn’t clear to me where this fix was made. Basically I wanted to know where 0xC00000A5 was coming from so that I could determine if I had to do something additional in my driver or if I could just trust in SeTokenIsAdmin.

There are a million other ways I could have done this, almost all of which would have been way faster, but given that the NTSTATUS code seemed somewhat unusual I decided to go for my standard trick of finding all the places where a module returns a particular NTSTATUS code. This comes in handy from time to time and I always like a chance to practice techniques so that I can rely on them in a panic.

Aside: Yes, I know that IDA solves this problem instantly with x-refs. My goal here though it to demonstrate how to do this without IDA and just using tools available within WinDbg.

First, I’m assuming that this NTSTATUS value is originating from the NT module. So, I need to get the base address and limit for the module in the debugger:

Now I want to search for instances of the DWORD 0xC00000A5 from the beginning of the module to the end of the module:

Now here’s the first trick…

The x86/x64 use a variable length instruction set, so we need to repeat the search on different byte boundaries to find all of the results. Let’s repeat 3 more times and increase the starting address by one each time:

OK, so that was returned in more places than I had originally expected or hoped 🙂 But we must forge ahead.

And now for the second trick…

The references that we’re finding to this NTSTATUS value are part of an instruction. For example, here’s what a move of 0xC00000A5 into EAX looks like:

Note that it’s a single byte (0xB8) followed by our DWORD of interest. So, if we want to set breakpoints on the locations where the value is loaded we need to fudge the address in the search results until we get what looks like an instruction.

For example, if I just unassemble the first search hit I get junk:

But if I back up the address by one I get a realistic looking instruction sequence:

I usually just start by subtracting one and checking to see if it looks OK. If not I subtract by one again, and so on. Usually only takes a few bytes before you get the instruction you’re looking for. If you get lots of hits you can even do a quick script to automate disassembling the results:

Rinse and repeat with the other alignments and with different subtracted values.

Once you have your list of addresses start setting breakpoints on everything and re-run your test case. Sometimes you need to disable some breakpoints because they’re hit so commonly that they don’t really help narrow down your case.

For me, I ran the POC and hit my breakpoint with this call stack:

This was pretty interesting as the error is getting triggered by a call to SeIsAppContainerOrIdentifyLevelContext. I couldn’t find any documentation on this API though it would appear to be specifically designed to catch the case from the Project Zero report (i.e. reject identify level tokens).

While this exercise was fun, it led me to an undocumented API that I can’t call in my driver. So my exercise of tracking down 0xC00000A5 was successful but turned out to be entirely unnecessary. Sorry!

Upon reflection. what I really need to know is if SeTokenIsAdmin is safe on Windows 10 and if does it “do the right thing” with identify tokens.

To get the answer this time I just set a breakpoint in SeTokenIsAdmin and stepped through it on Windows 10 versus Windows 7. This quickly gave me the answer that, thankfully, yes this API now works as expected. Instead of just checking the group membership, new versions of SeTokenIsAdmin also check to make sure that impersonation tokens have at least an impersonation level of SecurityImpersonation:

Kind of wish I had started there in the first place as I would have saved myself some time. However, sometimes debugging is all about taking the scenic route.

Hopefully the NTSTATUS trick turns out to be useful for you in the future. Also, if anyone wants to write something to automate locating the containing instruction I’d be more than happy to accept!