Friday, September 9, 2016

An arbitrary dive into assembler and certain Windows internals, part 2

Resuming the adventure from last time.

In which we regain our bearings

When we left off, we had a SAM handle at the var_24 local variable, and a domain handle at var_4. Something unknown is possibly at hMem, since we also passed that address to UaspOpenDomain. We find ourselves here:


In which we continue the journey

Local variable var_14 was zeroed at the start of NetUserModalsGet and, looking through the path we traveled, hasn't been changed since. Its address is pushed, followed by a literal 5, followed by the contents of var_4 (our domain handle). A call to SamQueryInformationDomain follows. Double-clicking its name shows that it's an imported function.


We saw __stdcall on NetUserModalsGet too. It's a common calling convention for cross-library function calls. MSDN explains that functions with this calling convention expect their arguments to be pushed in the opposite order that they're declared. The callee "cleans the stack," i.e. is responsible for popping the arguments. Press Escape to go back to NetUserModalsGet.

Notice how the first thing pushed is the address of var_14. Consulting the callee's definition again, it corresponds to the Buffer parameter, so after SamQueryInformationDomain returns, we should have a pointer to the desired information at var_14. But what will the structure of that information be?

In which we search through documentation

I can't find an information page on PSAMPR_DOMAIN_INFO_BUFFER or the non-P-prefixed version in that documentation set, so we'll go directly to the full interface definition for the SAMR protocol. Ctrl+F for "info_buffer" and the first result is a type definition. It's a switched union, which means it has completely different members in different situations. This one depends on a DOMAIN_INFORMATION_CLASS, which is an enumeration fortunately defined right above the union. The second argument to SamQueryInformationDomain has that type too, and in this case we're passing the value of 5: DomainNameInformation. So, we see from the switch that the info buffer will contain a SAMPR_DOMAIN_NAME_INFORMATION. Ctrl+F for that, and we find that its definition contains only one member: DomainName, of type RPC_UNICODE_STRING. This will be important soon.

In which another choice is made

Immediately after the call to SamQueryInformationDomain, eax is tested against itself, then there's a js instruction: "jump if signed." It doesn't really make sense to make a decision based on the highest bit of a memory location, so I'm confident that the function we called puts its return value in eax. And indeed, that's true of stdcall functions.

I don't know whether the result should have the high bit set, so let's follow both arrows. Suppose it is signed, and we take the green path.


NetpNtStatusToApiStatus_Access sure sounds like a function designed to convert return values from one set of values to another. To me, that seems like something done when bailing out and reporting failure. Let's follow the unconditional jump (blue arrow) just to be sure.


That's the hub of failure again. If this path is followed, we'll exit without returning anything useful. Double-click the rightmost blue arrow to backtrack, then double-click the incoming green arrow on the previous chunk to get back to the js. Apparently, SamQueryInformationDomain will return a non-signed value on success. Follow the red path.

In which we make use of our resources


Interesting, the hMem local variable appears again. The last time we saw it, it was zero and its address was passed to UaspOpenDomain. Now it's being passed to RtlLengthSid, which expects a pointer to a valid SID. Therefore, it would seem that UaspOpenDomain populated it with the SID of the current domain.

Immediately after the call, ecx receives the value of local variable var_14, which holds a pointer to the buffer we got from SamQueryInformationDomain. Now we meet some new syntax and a new instruction:

movzx ebx, word ptr [ecx]

A search reveals that movzx means "move and zero-extend," so it seems that our source is going to be smaller in width than the target. And so it is. word ptr before a dereference specifies that we only want one word (two bytes) instead of four bytes. So, that instruction means "store the two bytes starting at the memory location specified by ecx in ebx, zero-extending to fit in the wide register." Since ecx had a pointer to the domain info buffer, we're storing the buffer's first word in ebx. But why?

The SAMR interface definition page helpfully has a link to the IDL that specifies basic data types. Ctrl+F that for "RPC_UNICODE_STRING" and you find that type's definition. Sure enough, its first member - an unsigned short, only two bytes long - is the length of the string. ebx now holds the length of the domain name in bytes.

Two addition instructions follow. add just adds the second thing to the first thing, so we add 10 (0xA) and eax to the length of our domain name. RtlLengthSid, like all stdcall functions, gives its return value in eax, so we're adding the SID length. (Its return type, ULONG, is only 32 bits wide, so it fits in a normal register.) It seems like we're about to allocate some memory, though I don't know why we just added that 10 to the byte count.

Anyway, let's follow the blue line.

In which memory is indeed allocated


Here, lea is just used as a fancy adder - ebx is a size, not a memory location. A jump is made, essentially, if ebx plus 2 is less than or equal to ebx. That will never happen unless ebx is so large that adding 2 to it makes it overflow. The green path is clearly for errors, and it indeed goes down to the hub of failure, assigning 0x216 (ERROR_ARITHMETIC_OVERFLOW) to esi. Let's follow the red path on that jbe.


The first instruction here, inc, increments a register by 1. and sets a register to the binary-AND of itself and another thing. This particular and preserves all but the last bit, so the first two instructions here just round ebx up to a multiple of 2 if necessary.

Then there's another call, this time to LocalAlloc. As IDA helpfully points out, it specifies ebx as the uBytes parameter (number of bytes desired in the block) and 0x40 as the uFlags parameter (options to the allocator). Consulting that function's documentation, 0x40 means LMEM_ZEROINIT: make sure the memory is all zeros at first.

The value of the bufptr variable - the pointer to the buffer the caller wants the data in - is copied into ecx. eax is the HLOCAL (handle to the allocated memory chunk) received from LocalAlloc, and it's put into esi. Then it's copied into the memory address specified by ecx, which results in the HLOCAL being copied into the first four bytes of the caller-provided buffer. Since the LocalAlloc call didn't specify LMEM_MOVEABLE, the HLOCAL is actually a perfectly usable pointer. Then, esi is tested to see if it's zero. If it is zero, that means the allocation failed, and NetUserModalsGet bails out. Follow the red path.


edi has still not been touched since it was assigned the level, so we still want it to be 2. Follow the red path.

In which we wait for the thrilling conclusion


In this post, we discovered that we have a SID in the hMem variable, and we got a pointer to some domain information in the var_14 variable. We have a pointer to a chunk of memory (with size ebx) at esi. We'll finish it off next time.

Continue: Part 3

No comments:

Post a Comment