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!