Saturday, September 10, 2016

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

Continuing the adventure from part 2.

In which we get organized

This is where we left off:


It's kind of hard to mentally keep track of what the various local variables mean when they're only identified by a number. Fortunately, IDA has a way to rename them. Right-click a local variable in the function's first chunk and choose Rename. Enter the new name, then hit OK. For instance, let's rename var_14 to domainInfoPtr, since it holds a pointer to the domain information buffer that we got from SamQueryInformationDomain. Similarly, hMem should really be called something like domainSidPtr, since it has a pointer to the SID for the SAM domain. The other local variables we've used so far won't really show up again, but I'll rename them just for fun. var_24 becomes samHandle and var_4 becomes domainHandle.

In which we tackle a challenging chunk

First, the domain information pointer is copied into ecx. Then, eax receives the sum of esi and ebx. Coming into this chunk, esi was a pointer to a newly allocated block of memory, and ebx was its size. Therefore, eax now points one byte past the end of that memory block. That data is then stored in the var_28 variable, which would more descriptively be named something like allocEndPlus1Ptr.


Much better.

Moving on, eax receives the address of that variable. esi (pointer to the start of the new memory block) and eax are pushed. ecx is still the pointer to the domain information, so the movzx instruction assigns the RPC_UNICODE_STRING's text length in bytes to edx. eax receives an address 8 bytes past the start of the memory block, and we'll see why soon. ecx receives the 32-bit value starting 4 bytes past the memory address in it, which, looking again at the definition of RPC_UNICODE_STRING in the DTYP protocol, is a pointer to a buffer of WCHARs (wide characters). The memory block's start plus 8 (eax) is pushed. edx, the domain name's length in bytes, is shifted to the right by one bit, dividing it by 2 and producing the domain name's length in wide characters. Since NetpCopyStringToBuffer is a proc near, it can receive data from the ecx and edx registers in addition to the stack.

Based on NetpCopyStringToBuffer's name, I think it's fair to assume that it just copied edx characters from the location specified in ecx (which points to the domain name's Buffer field) into our memory chunk starting 8 bytes past the start. Evidently, the string copying function returns zero on failure, since the green path (for a jz), goes down to a failure-reporting chunk. After all, we still need to provide the SID we collected. Follow the very short red path.

In which we deliver on the promise


I guess we're done checking the level, since this chunk starts out by placing the pointer we have to our SID into edi. The address 4 bytes past the start of the memory block is stored in eax, and it's pushed after a literal 1. The address just past the end of the memory block is retrieved and pushed. The bufptr variable is actually a pointer to a pointer, and after two moves and an addition, eax holds the address 8 bytes past the start of the buffer, which should be the address of the start of the string that was just copied in. That's pushed, followed by the pointer to the domain SID.

That one last push passes data to RtlLengthSid, while the others are for the upcoming NetpCopyDataToBuffer call. The address of the SID and the length of it (retrieved from RtlLengthSid via eax) are passed to the data copier via registers.

Remember that we're trying to fill out a USER_MODALS_INFO_2 structure. Also notice from the documentation on NetUserModalsGet that a single call to NetApiBufferFree releases all resources used up by the original call. Therefore, it's very likely that both the returned structure (which is just two pointers) and the things the pointers point to are all in the same block of memory for easy freeing. An RPC_UNICODE_STRING's length doesn't include the null terminator, while that terminator is necessary for an LPWSTR. That explains why the memory block has to be 10 bytes larger than the sum of the SID's length and the string's length in bytes: it needs 8 bytes to store the returned structure, plus 2 for the string's null terminator. Despite being variable-length, SIDs don't need null terminators because they include the count of subauthorities.

It's also interesting that we never have to assign the members of the structure explicitly. The NetpCopy-prefixed copying functions do it for us, otherwise there would be no reason to e.g. provide NetpCopyStringToBuffer with the start address of the returned structure. Somehow, the copied string is placed after the copied SID. I really can't tell what makes that happen.

Anyway, after the last copy call, there's another test and jump-if-zero. Once again, a return value of zero means failure - the row of things that assign to esi all seem to include error codes. We follow the red line again.


In which we clean up

That xor just sets esi to zero, as opposed to all the other destinations in that row, which assign it a code. Then we end up in a familiar place:


This time, the arrival here doesn't mean failure. It just means we're done, and it's time to clean up all the SAM handles and related things we've allocated in the course of our data collection. A series of tests for zero on various things follows, freeing them appropriately if they actually contain anything. This one shown above retrieves a local variable that wasn't used on level 2's path. Therefore, we skip over the call to SamFreeMemory. There are quite a few of these arrangements.


Not all of them need to be released with SamFreeMemory, there are other types of resources too.


Finally, we get to the function's epilogue:


Remember that stdcall functions return their result in eax. Therefore, when coming down the blue or green path as we do for success, esi (which was used by the hub of failure to store the error code, or was zeroed in case of success) is copied into eax. The red arrow comes in from some early failure case; it sets eax directly. Finally, the calling function's registers are restored, the stack pointer is adjusted back to how it was, and the function returns (retn), clearing 12 (0xC) bytes of arguments off the stack.

No comments:

Post a Comment