Case of the Admin Check Fail

After users had migrated from Windows XP to Windows 7 an application Continuum ( OmsInst.exe) started to fail with error on some users:

Continuum Database Initialization

You must have NT local administrative priviledges to run this application

image

However …

This user was a member of local administrators group

In addition I had been advised the “ForceAdminAccess” SHIM had been applied – this tricks applications into thinking they are running as admin, allowing many applications to run as standard users.

I asked support person to grab a .dmp file of application when at error message, using Task Manager –> right click Create Dump File.

First thing I wanted to confirm 100% the ForceAdminAccess SHIM was being applied.

SHIMs work by modifying the Import Address Table (IAT). So first I found address of module

0:000:x86> !dh OmsInst.exe

File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
     14C machine (i386)
       4 number of sections
51C91F20 time date stamp Tue Jun 25 14:40:00 2013

       0 file pointer to symbol table
       0 number of symbols
      E0 size of optional header
     103 characteristics
            Relocations stripped
            Executable
            32 bit word machine

OPTIONAL HEADER VALUES
     10B magic #
    8.00 linker version
   AE000 size of code
   D1000 size of initialized data
       0 size of uninitialized data
   92F59 address of entry point
    1000 base of code
         —– new —–
0000000000400000 image base <—Image Base , we need this
    1000 section alignment
    1000 file alignment
       2 subsystem (Windows GUI)
    4.00 operating system version
    0.00 image version
    4.00 subsystem version
  184000 size of image
    1000 size of headers
  1815E3 checksum
0000000000100000 size of stack reserve
0000000000001000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit
       0  DLL characteristics
       0 [       0] address [size] of Export Directory
   DDB04 [     168] address [size] of Import Directory
  102000 [   81DF0] address [size] of Resource Directory
       0 [       0] address [size] of Exception Directory
       0 [       0] address [size] of Security Directory
       0 [       0] address [size] of Base Relocation Directory
       0 [       0] address [size] of Debug Directory
       0 [       0] address [size] of Description Directory
       0 [       0] address [size] of Special Directory
       0 [       0] address [size] of Thread Storage Directory
       0 [       0] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
   AF000 [     660] address [size] of Import Address Table Directory <- we need this table address
   DDA7C [      40] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory

Now we used the Display Words And Symbols command with offset of image base + import address table:

0:000:x86> dps 400000+AF000 400000+AF000+660
004af000  74ba2b58*** ERROR: Symbol file could not be found.  Defaulted to export symbols for advapi32.dll –
advapi32!OpenServiceA
004af004  74ba485f advapi32!RegQueryValueExA
004af008  74b9cd9a advapi32!RegQueryValueA
004af00c  74bba149 advapi32!RegEnumKeyA
004af010  74bba767 advapi32!RegDeleteKeyA
004af014  74b9cb9d advapi32!RegOpenKeyA
004af018  74bf2151 advapi32!EnumServicesStatusA
004af01c  74ba429c advapi32!OpenThreadToken
004af020  716d823f AcGenral!NS_ForceAdminAccess::APIHook_GetTokenInformation <- Force Admin Acces SHIM is loaded

I launched the process with API monitor http://www.rohitab.com/apimonitor

In API monitor’s API Filter under Security and Identity –> Authorization –> Basic Access Control –> Advapi32.dll I enabled GetTokenInformation

From this I immediately saw issue:

#    Time of Day    Thread    Module    API    Return Value    Error    Duration
1    6:34:12.556 PM    1    omsinst.exe    GetTokenInformation ( 0x000007b8, TokenGroups, 0x0018da80, 2048, 0x0018da74 )    FALSE    122 = The data area passed to a system call is too small.     0.0000246

Many Windows APIs require a buffer and the program must tell the API what the buffer size is. If the buffer is not large enough the application need to re-allocate the buffer to a size required as returned by the API. Looking at the MSDN documentation for http://msdn.microsoft.com/en-us/library/windows/desktop/aa446671(v=vs.85).aspx we can see the 4th parameter is “TokenInformationLength” which in this program is fixed size of 2048.

Using API monitor we can set a breakpoint on function return

image

Rerunning application we then see how large a token was required – 6160 bytes. The reason for large token is because this was a domain user that was a member of many Active Directory groups. In the XP environment the users had been on Novell eDirectory.

At this point we can guess a workaround – run as a local account, where token size is small. I tested this and it worked fine.

clip_image001

So now we know exactly what to advise the vendor support to fix…

But what if it’s an old application that is no longer supported. We can patch this easily by removing the admin check completely.

In this case I will use IDA but it can also be done with free tools like OllyDbg.

First thing I do is go to imports table and double click GetTokenInformation

image

I then select Get Token Information and hit ‘x’ – Jump xref to operand

image

We can see here the push 800h is setting the TokenInformationLength to 2048. We could try and increase that value but that will be risky as the memory won’t have been properly allocated for that size of Token.

image

Instead I scrolled up to top of function, and again selected ‘jump xref to operand’

image

Following this function we see the instruction we need to patch:

image

Test eax, eax does a bitwise AND between arguments, and in this case if EAX is zero the result sets the ZF (Zero Flag) to 1.

The following instruction will “jump if not zero” – that is if the Zero Flag is not set. We will want it to always jump however (You can either work this out through reading the disassembly, or if you have trouble following it, set a breakpoint here, and see where it goes on broken machine, and force the opposite.)

The JNZ instruction has opcode 0F 85 cw/cd  (JNZ rel16/32) Cw = 2 byte value for code offset, CD = 4 byte value for code offset.

I didn’t find the exact replacement jmp instruction in the documentation I had on hand, so I added a jz after the JNZ.

This type of change is easy as you usually just increment/decrement by one to change between JNZ and JZ.

image

image

The final patched code looked like this:

You can see I just copied byte for byte the previous instruction

0F 85 9B 00 00 00

and decreased 85 –> 84 to make JNZ into JZ. I also adjusted subtracted six from 9B due to the instruction being “six bytes” so that both jumped to the same location. (This particular type of jnz/jz instruction is a relative jump)

to get

0F 84 95 00 00 00

image

I then used Edit –> Patch Program –> Apply Patches to Input File, ensuring to take a backup in case of issue with patch.

Now the program worked fine, and no pesky admin check.

After providing all this information to the vendor they still argued it wasn’t a bug and was our configuration problem so I provided some example codes.

The vendor’s code is something like this:

#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <Windows.h>

BOOL IsAdmin();
	
int _tmain(int argc, _TCHAR* argv[])
{
	if (IsAdmin())
	{
		std::cout << "Member of Administrators Group";
	}
	else
	{
		std::cout << "Not a member of Administrators Group";
	}
	return 0;
}

BOOL IsAdmin()
{
	CONST int		BUFFER_SIZE = 2048; // hardcoded!!!
	HANDLE          hToken = NULL;
	PSID            pAdminSid = NULL;
	BYTE			buffer[BUFFER_SIZE];
	PTOKEN_GROUPS   pGroups = (PTOKEN_GROUPS)buffer;
	DWORD           dwSize = 0;
	DWORD           i;
	BOOL            bSuccess;
	DWORD			dwResult = ERROR_SUCCESS;
	SID_IDENTIFIER_AUTHORITY siaNtAuth = SECURITY_NT_AUTHORITY;

	// get token handle
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
		return false;

	bSuccess = GetTokenInformation(
		hToken,
		TokenGroups,
		(LPVOID)pGroups, 
		BUFFER_SIZE,
		&dwSize);

	CloseHandle(hToken);

	if (!bSuccess)
	{
		// no error checking!!! just fail admin check!
		return false;
	}
	
	if (!AllocateAndInitializeSid(&siaNtAuth, 2,
		SECURITY_BUILTIN_DOMAIN_RID,
		DOMAIN_ALIAS_RID_ADMINS,
		0, 0, 0, 0, 0, 0, &pAdminSid))
		return false;

	bSuccess = FALSE;
	for (i = 0; (i < pGroups->GroupCount) && !bSuccess; i++)
	{
		if (EqualSid(pAdminSid, pGroups->Groups[i].Sid))
			bSuccess = TRUE;
	}
	FreeSid(pAdminSid);
	return bSuccess;
}

The fixed code looks like this:

#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <Windows.h>

BOOL IsAdmin();
	
int _tmain(int argc, _TCHAR* argv[])
{
	DWORD dwResult;
	if (IsAdmin())
	{
		std::cout << "Member of Administrators Group" << std::endl;
	}
	else
	{
		dwResult = GetLastError();
		if (dwResult == ERROR_SUCCESS)
		{
			std::cout << "Not a member of Administrators Group" << std::endl;
		}
		else
		{
			LPTSTR errorText = NULL;

			FormatMessage(
				// use system message tables to retrieve error text
				FORMAT_MESSAGE_FROM_SYSTEM
				// allocate buffer on local heap for error text
				| FORMAT_MESSAGE_ALLOCATE_BUFFER
				// Important! will fail otherwise, since we're not 
				// (and CANNOT) pass insertion parameters
				| FORMAT_MESSAGE_IGNORE_INSERTS,
				NULL,    // unused with FORMAT_MESSAGE_FROM_SYSTEM
				dwResult,
				MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
				(LPTSTR)&errorText,  // output 
				0, // minimum size for output buffer
				NULL);   // arguments - see note 

			if (NULL != errorText)
			{
				std::wcout << "Failed to get users groups. Error #" << dwResult << " Reason: " << errorText << std::endl;
				// release memory allocated by FormatMessage()
				LocalFree(errorText);
				errorText = NULL;
			}
		}
	}
	return 0;
}

BOOL IsAdmin()
{
	HANDLE          hToken = NULL;
	LPBYTE			lpBytes = NULL;
	PSID            pAdminSid = NULL;
	PTOKEN_GROUPS   pGroups = NULL;
	DWORD           dwSize = 0;
	DWORD           i;
	BOOL            bSuccess = FALSE;
	DWORD			dwResult = ERROR_SUCCESS;
	SID_IDENTIFIER_AUTHORITY siaNtAuth = SECURITY_NT_AUTHORITY;

	// get token handle
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
		return false;

	// pass "0" as size, so we get actual size required
	GetTokenInformation(
		hToken,
		TokenGroups,
		NULL, 
		0,
		&dwSize);

	dwResult = GetLastError();

	if (dwResult == ERROR_INSUFFICIENT_BUFFER)
	{
		// allocate buffer
		lpBytes = new BYTE[dwSize];
		pGroups = (PTOKEN_GROUPS)lpBytes;
		bSuccess = GetTokenInformation(
			hToken,
			TokenGroups,
			pGroups,
			dwSize,
			&dwSize);

		dwResult = GetLastError();
	}
	
	CloseHandle(hToken);

	if (!bSuccess)
	{
		// failed to get users groups
		SetLastError(dwResult);
		delete[] lpBytes;
		return false;
	}
	
	if (!AllocateAndInitializeSid(&siaNtAuth, 2,
		SECURITY_BUILTIN_DOMAIN_RID,
		DOMAIN_ALIAS_RID_ADMINS,
		0, 0, 0, 0, 0, 0, &pAdminSid))
		return false;

	bSuccess = FALSE;
	for (i = 0; (i < pGroups->GroupCount) && !bSuccess; i++)
	{
		if (EqualSid(pAdminSid, pGroups->Groups[i].Sid))
			bSuccess = TRUE;
	}
	FreeSid(pAdminSid);
	delete[] lpBytes;
	return bSuccess;
}

However on Windows XP and later it is far simpler to use this code as per http://msdn.microsoft.com/en-us/library/windows/desktop/aa376389(v=vs.85).aspx

#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <Windows.h>

BOOL IsAdmin();
	
int _tmain(int argc, _TCHAR* argv[])
{
	DWORD dwResult;
	if (IsAdmin())
	{
		std::cout << "Member of Administrators Group" << std::endl;
	}
	else
	{
		dwResult = GetLastError();
		if (dwResult == ERROR_SUCCESS)
		{
			std::cout << "Not a member of Administrators Group" << std::endl;
		}
		else
		{
			LPTSTR errorText = NULL;

			FormatMessage(
				// use system message tables to retrieve error text
				FORMAT_MESSAGE_FROM_SYSTEM
				// allocate buffer on local heap for error text
				| FORMAT_MESSAGE_ALLOCATE_BUFFER
				// Important! will fail otherwise, since we're not 
				// (and CANNOT) pass insertion parameters
				| FORMAT_MESSAGE_IGNORE_INSERTS,
				NULL,    // unused with FORMAT_MESSAGE_FROM_SYSTEM
				dwResult,
				MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
				(LPTSTR)&errorText,  // output 
				0, // minimum size for output buffer
				NULL);   // arguments - see note 

			if (NULL != errorText)
			{
				std::wcout << "Failed to get users groups. Error #" << dwResult << " Reason: " << errorText << std::endl;
				// release memory allocated by FormatMessage()
				LocalFree(errorText);
				errorText = NULL;
			}
		}
	}
	return 0;
}

BOOL IsAdmin()
{
	HANDLE          hToken = NULL;
	LPBYTE			lpBytes = NULL;
	PSID            pAdminSid = NULL;
	PTOKEN_GROUPS   pGroups = NULL;
	DWORD           dwSize = 0;
	DWORD           i;
	BOOL            bSuccess = FALSE;
	DWORD			dwResult = ERROR_SUCCESS;
	SID_IDENTIFIER_AUTHORITY siaNtAuth = SECURITY_NT_AUTHORITY;

	// get token handle
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
		return false;

	// pass "0" as size, so we get actual size required
	GetTokenInformation(
		hToken,
		TokenGroups,
		NULL, 
		0,
		&dwSize);

	dwResult = GetLastError();

	if (dwResult == ERROR_INSUFFICIENT_BUFFER)
	{
		// allocate buffer
		lpBytes = new BYTE[dwSize];
		pGroups = (PTOKEN_GROUPS)lpBytes;
		bSuccess = GetTokenInformation(
			hToken,
			TokenGroups,
			pGroups,
			dwSize,
			&dwSize);

		dwResult = GetLastError();
	}
	
	CloseHandle(hToken);

	if (!bSuccess)
	{
		// failed to get users groups
		SetLastError(dwResult);
		if (lpBytes!=NULL) delete[] lpBytes;
		return false;
	}
	
	if (!AllocateAndInitializeSid(&siaNtAuth, 2,
		SECURITY_BUILTIN_DOMAIN_RID,
		DOMAIN_ALIAS_RID_ADMINS,
		0, 0, 0, 0, 0, 0, &pAdminSid))
		return false;

	bSuccess = FALSE;
	for (i = 0; (i < pGroups->GroupCount) && !bSuccess; i++)
	{
		if (EqualSid(pAdminSid, pGroups->Groups[i].Sid))
			bSuccess = TRUE;
	}
	FreeSid(pAdminSid);
	delete[] lpBytes;
	return bSuccess;
}

In addition I provided a scenario where you could reproduce the bug (even without Active Directory)

To reproduce in an isolated environment:

  1. Create 49 local groups
  2. Use local Administrator account
  3. Add administrator to 49 groups (plus already a member of Administrators group)
  4. Log off / log on as local Administrator
  5. OmsInst will fail to launch
  6. Remove user from Group 49
  7. Log off / log on
  8. OmsInst will launch correctly.

clip_image002

About chentiangemalc

specializes in end-user computing technologies. disclaimer 1) use at your own risk. test any solution in your environment. if you do not understand the impact/consequences of what you're doing please stop, and ask advice from somebody who does. 2) views are my own at the time of posting and do not necessarily represent my current view or the view of my employer and family members/relatives. 3) over the years Microsoft/Citrix/VMWare have given me a few free shirts, pens, paper notebooks/etc. despite these gifts i will try to remain unbiased.
This entry was posted in API Monitor, Application Compatibility, Debugging, Patching, WinDbg and tagged . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s