Windows System Software -- Consulting, Training, Development -- Engineering Excellent, Every Time.

Tracking an NTSTATUS to its Source

Tracking an NTSTATUS to its Source

Last reviewed and updated: 10 August 2020

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:

https://www.osronline.com/ShowThread.cfm?link=201029

https://www.osronline.com/showthread.cfm?link=264871

(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:

https://bugs.chromium.org/p/project-zero/issues/detail?id=127&redir=1

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:

kd> lm mnt
Browse full module list
start             end                 module name
fffff801`25c07000 fffff801`26490000   nt

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

kd> s -d fffff801`25c07000 fffff801`26490000 0xc00000a5
fffff801`260c4174  c00000a5 fffef8e9 ccccccff cccccccc  ................
fffff801`261df870  c00000a5 e4d878e9 d98bccff e4d9c7e9  .....x..........
fffff801`262132c4  c00000a5 ec525be9 4c8b48ff 29e86824  .....[R..H.L$h.)
fffff801`2622aed0  c00000a5 244c8d48 1f92e870 c38bffe8  ....H.L$p.......

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:

kd> s -d fffff801`25c07001 fffff801`26490000 0xc00000a5
fffff801`25dd0fa1  c00000a5 f57026e9 4f8b41ff 75c98514  .....&p..A.O...u
fffff801`260c4289  c00000a5 000db8c3 ccc3c000 cccccccc  ................
fffff801`2611d799  c00000a5 ccccc7eb cccccccc 48cccccc  ...............H
fffff801`2622395d  c00000a5 ee2f40e9 000dbbff 40ebc000  .....@/........@

kd> s -d fffff801`25c07002 fffff801`26490000 0xc00000a5
fffff801`25c6c88e  c00000a5 00e9c032 41fffffe 0000c6f7  ....2......A....
fffff801`25d9ab4e  c00000a5 ec9774e9 848b48ff 00010024  .....t...H..$...
fffff801`26164bce  c00000a5 fffe5ae9 ce8b49ff c38b96eb  .....Z...I......
fffff801`2622af62  c00000a5 8b480deb fc90e8cf 79bbffa3  ......H........y
fffff801`2625d4ee  c00000a5 244c8d48 f974e878 c38bffe4  ....H.L$x.t.....

kd> s -d fffff801`25c07003 fffff801`26490000 0xc00000a5
fffff801`260ccffb  c00000a5 351e840f c0850014 350b880f  .......5.......5
fffff801`26253a37  c00000a5 f0c2ffe9 0001b8ff 3ce9c000  ...............<

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:

nt!NtDuplicateToken+0x343:
fffff801`260c4173 b8a50000c0      mov     eax,0C00000A5h

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:

kd> u fffff801`260c4174
nt!NtDuplicateToken+0x344:
fffff801`260c4174 a5              movs    dword ptr [rdi],dword ptr [rsi]
fffff801`260c4175 0000            add     byte ptr [rax],al
fffff801`260c4177 c0e9f8          shr     cl,0F8h
fffff801`260c417a fe              ???
fffff801`260c417b ff              ???
fffff801`260c417c ffcc            dec     esp
fffff801`260c417e cc              int     3
fffff801`260c417f cc              int     3

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

kd> u fffff801`260c4174-1
nt!NtDuplicateToken+0x343:
fffff801`260c4173 b8a50000c0      mov     eax,0C00000A5h
fffff801`260c4178 e9f8feffff      jmp     nt!NtDuplicateToken+0x245 (fffff801`260c4075)
fffff801`260c417d cc              int     3

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:

kd> .foreach (${hit} { s -[1]d fffff801`25c07000 fffff801`26490000 0xc00000a5 }) { u ${hit} -1  L3} 
nt!NtDuplicateToken+0x343:
fffff801`260c4173 b8a50000c0      mov     eax,0C00000A5h
fffff801`260c4178 e9f8feffff      jmp     nt!NtDuplicateToken+0x245 (fffff801`260c4075)
fffff801`260c417d cc              int     3
nt!ExpWnfQueryCurrentUserSID+0x1b280b:
fffff801`261df86f b8a50000c0      mov     eax,0C00000A5h
fffff801`261df874 e978d8e4ff      jmp     nt!ExpWnfQueryCurrentUserSID+0x8d (fffff801`2602d0f1)
fffff801`261df879 cc              int     3
nt!NtPrivilegeCheck+0x13af73:
fffff801`262132c3 b8a50000c0      mov     eax,0C00000A5h
fffff801`262132c8 e95b52ecff      jmp     nt!NtPrivilegeCheck+0x1d8 (fffff801`260d8528)
fffff801`262132cd 488b4c2468      mov     rcx,qword ptr [rsp+68h]
nt!NtPrivilegedServiceAuditAlarm+0x102223:
fffff801`2622aecf bba50000c0      mov     ebx,0C00000A5h
fffff801`2622aed4 488d4c2470      lea     rcx,[rsp+70h]
fffff801`2622aed9 e8921fe8ff      call    nt!SeReleaseSubjectContext (fffff801`260ace70)

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:

kd> kc
 # Call Site
00 nt!SeAccessCheckWithHint
01 nt!SeAccessCheck
02 nt!SeIsAppContainerOrIdentifyLevelContext
03 nt!NtPowerInformation
04 nt!KiSystemServiceCopyEnd
05 ntdll!NtPowerInformation
06 wow64!whNtPowerInformation
07 wow64!Wow64SystemServiceEx
08 wow64cpu!ServiceNoTurbo
09 wow64!Wow64KiUserCallbackDispatcher
0a wow64!Wow64LdrpInitialize
0b ntdll!LdrpInitializeProcess
0c ntdll!_LdrpInitialize
0d ntdll!LdrpInitialize
0e ntdll!LdrInitializeThunk

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!