0

Trying to resolve it but all efforts are in vain so far. The workflow as follows

Windows service running as LocalSystem creates child using CreateProcessAsUser(...) with token of current logged user.

const auto session = WTSGetActiveConsoleSessionId();
auto result = WTSQueryUserToken(session, &token);

HANDLE primary;
result = DuplicateTokenEx(token,
    TOKEN_QUERY_SOURCE | TOKEN_ALL_ACCESS | TOKEN_IMPERSONATE |
    TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ADJUST_PRIVILEGES,
    nullptr, SecurityImpersonation, TokenPrimary, &primary);

const auto args = std::to_string(reinterpret_cast<long>(access));
CreateProcessAsUser(primary,
                      const_cast<LPSTR>(command.c_str()), // module name
                      const_cast<LPSTR>(args.c_str()),    // Command line
                      nullptr, // Process handle not inheritable
                      nullptr, // Thread handle not inheritable
                      TRUE,    // Set handle inheritance to TRUE
                      0,       // No creation flags
                      nullptr, // Use parent's environment block
                      nullptr, // Use parent's starting directory
                      &si,     // Pointer to STARTUPINFO structure
                      &pi);    // Pointer to PROCESS_INFORMATION structure

The child process is launched in user workstation\desktop and main thread captures user I/O events. The child process impersonation is as follows

 void impersonate() {
  const auto args = GetCommandLine();
  const auto system_token = reinterpret_cast<HANDLE>(std::stol(args, nullptr));

  if (SetThreadToken(nullptr, system_token) == TRUE) {
   auto result = OpenThreadToken(GetCurrentThread(),
                            TOKEN_QUERY | TOKEN_QUERY_SOURCE, TRUE, &token);
  if (result == TRUE)
  {
    DWORD dwSize = 0;

   if (!GetTokenInformation(token, TokenStatistics, NULL, 0, &dwSize)) {
      const auto dwResult = GetLastError();

          if (dwResult != ERROR_INSUFFICIENT_BUFFER) {
            cout << "GetTokenInformation Error: " << dwResult;

          } else {
          // Allocate the buffer.
            PTOKEN_STATISTICS statistics =
                (PTOKEN_STATISTICS)GlobalAlloc(GPTR, dwSize);

            // Call GetTokenInformation again to get the group information.
            if (!GetTokenInformation(token, TokenStatistics, statistics, dwSize,
                           &dwSize)) {
              cout << "GetTokenInformation Error: " << error;
            } else {
              const auto level = statistics->ImpersonationLevel;
              std::string str;

              switch (level) {
              case SecurityAnonymous:
                str = R"(anonymous)";
                break;
              case SecurityIdentification:
                str = R"(identification)";
                break;
              case SecurityImpersonation:
                str = R"(impersonation)";
                break;
              case SecurityDelegation:
                str = R"(delegation)";
                break;
              }

              // This outputs identification.
              cout << "impersonation level : " << str;  
          }
      }
   }
 }

void thread_main() 
{
   impersonate();

   // if impersonation is successful, file opening fails otherwise not.
   const auto file = CreateFile(R"(C:\foo.txt)",                // name of the write
                   GENERIC_WRITE,          // open for writing
                   0,                      // do not share
                   NULL,                   // default security
                   CREATE_NEW,             // create new file only
                   FILE_ATTRIBUTE_NORMAL,  // normal file
                   NULL);                  // no attr. template

  if (file == INVALID_HANDLE_VALUE) {

  } else {
    // Rest of code;
  }
} 

Though current user is Administrator and added "Impersonate a Client After authentication" it still reporting "Security Identification".

Q: Is there anything else required to raise it to Security impersonate? Thanks,

jeeran
  • 23
  • 4
  • 1
    you not show code - how you impersonate, but i assume that your child process have not `SeImpersonatePrivilege` - as result `SetThreadToken` ok, but `ImpersonationLevel` is set to **Identification**. for check this - call `GetTokenInformation` with `TokenStatistics` and check `TOKEN_STATISTICS.ImpersonationLevel` – RbMm Aug 10 '17 at 12:57
  • @RbMm I added more information as per suggested findings. – jeeran Aug 10 '17 at 16:06
  • from your code - unclear how you impersonate. – RbMm Aug 10 '17 at 16:14
  • you need open **thread** token - not process token. all info which you paste - irrelevant. you need show - how you impersonate and **thread** token *ImpersonationLevel* – RbMm Aug 10 '17 at 16:16
  • @RbMm I have added impersonate method, thats all what child thread is impersonating. – jeeran Aug 10 '17 at 21:33
  • so - was exactly as I guess at begin - "Security Identification". your process have not `SeImpersonatePrivilege` and your user not admin really. as result in call `SetThreadToken` - the `ImpersonationLevel` in token downgraded from impersonation to identification. with this impersonation level - all security checks will fail – RbMm Aug 10 '17 at 21:45
  • You guessed it right. Is there any way that standard user process can be elevated or impersonated? – jeeran Aug 10 '17 at 21:54
  • this is special (disable impersonation and elevation) by design. so way exist only as "exploit" – RbMm Aug 10 '17 at 21:56
  • Presumably you're duplicating a handle to `system_token` from the parent to the child; if the only purpose of the impersonation is to write the file, consider duplicating a file handle rather than a token handle. Or you could run the child process as local system and have it impersonate the user rather than the other way around. – Harry Johnston Aug 10 '17 at 23:36
  • @HarryJohnston purpose of impersonation is privilege so standard users can't not write/modified application objects and application can execute system privileged tasks like other process information etc. The process can't be created using CreateProcess as it should in logged on user session [Winsta0\default-desktop] so it capture user keyboard and mouse events. – jeeran Aug 11 '17 at 07:20
  • @jeeran - really exist solution, by create child process with elevated user token (not restricted) - after call `WTSQueryUserToken` need get `TokenLinkedToken` (if you have `SE_TCB_PRIVILEGE` - but service have if run as *LocalSystem*). i now update self answer – RbMm Aug 11 '17 at 07:29
  • 1
    You can create a process that runs as local system in the user's session. See https://stackoverflow.com/a/21127414/886887 – Harry Johnston Aug 11 '17 at 23:34
  • @HarryJohnston - yes, this is possible solution, if not need access say `HKEY_CURRENT_USER`, current user profile, etc – RbMm Aug 11 '17 at 23:46

2 Answers2

5

how I understand you do next - you duplicate LocalSystem token, from service, to child process (via inherit handle) and pass it handle value in command line. then you call SetThreadToken.

but documentation of SetThreadToken is wrong and incomplete.

here only said that token must have TOKEN_IMPERSONATE access rights. nothing said about Thread handle access rights - it must have THREAD_SET_THREAD_TOKEN

but main:

When using the SetThreadToken function to impersonate, you must have the impersonate privileges and make sure that the SetThreadToken function succeeds

what is mean under you must have ? usually this mean that calling thread (or process to which calling thread belong in case thread have no token) must have impersonate privileges in token.

but this is wrong and not true. which privilege you ( calling thread ) have - does not matter. the process (even if target thread have token) to which target (not calling !) thread belong must have SeImpersonatePrivilege privilege or have the same logon session id as impersonation token, otherwise .. no, function not fail, and return succeeds, but it silently replace SECURITY_IMPERSONATION_LEVEL member in token to SecurityIdentification (look in WRK-v1.2\base\ntos\ps\security.c PsImpersonateClient function - begin from SeTokenCanImpersonate (implemented in WRK-v1.2\base\ntos\se\token.c - here and checked TOKEN_HAS_IMPERSONATE_PRIVILEGE and LogonSessionId) and if fail (STATUS_PRIVILEGE_NOT_HELD) returned by SeTokenCanImpersonate - the PsImpersonateClient function set ImpersonationLevel = SecurityIdentification ;

so even if you call SetThreadToken from service (which have impersonation privilege) for child process thread - call is "fail" if child process have not impersonation privilege. and visa versa - if you say pass(duplicate) own thread handle (with THREAD_SET_THREAD_TOKEN access rights) to restricted process, which have not impersonation privilege - he can success call SetThreadToken for your thread - impersonation level will be not reset to SecurityIdentification

in your case, because child process have no SeImpersonatePrivilege (usually it exist only in elevated processes, but if user enter to system with LOGON32_LOGON_INTERACTIVE - even "admins" have really restricted token (so they not really true admins)) and have different session id (compare local system token session id) - after SetThreadToken your thread have SecurityIdentification impersonation level. as result any system call, where security checked (say open file or registry key) will fail with error ERROR_BAD_IMPERSONATION_LEVEL.

how about solution ? if user have admin privileges - you need create elevated child process in user session (like "run as admin" ). for this you need query elevation type of token returned by WTSQueryUserToken and if it is TokenElevationTypeLimited - we need get linked token by GetTokenInformation call with TokenLinkedToken.

this is complete undocumented, but which token returned in TOKEN_LINKED_TOKEN structure depend from are calling thread (or process) have SE_TCB_PRIVILEGE - if yes - TokenPrimary is returned. otherwise TokenImpersonation is returned with SECURITY_IMPERSONATION_LEVEL set to SecurityIdentification (so this token can be used only for query). because service running under Local system account have SE_TCB_PRIVILEGE - you got the primary token, which you need use in CreateProcessAsUser call as is. so you need next function:

ULONG GetElevatedUserToken(PHANDLE phToken)
{
    union {
        ULONG SessionId;
        TOKEN_ELEVATION_TYPE tet;
        TOKEN_LINKED_TOKEN tlt;
        TOKEN_TYPE tt;
    };

    SessionId = WTSGetActiveConsoleSessionId();

    if (SessionId == MAXDWORD)
    {
        return ERROR_NO_SUCH_LOGON_SESSION;
    }

    HANDLE hToken;

    if (!WTSQueryUserToken(SessionId, &hToken))
    {
        return GetLastError();
    }

    ULONG len;

    ULONG dwError = NOERROR;

    if (GetTokenInformation(hToken, TokenElevationType, &tet, sizeof(tet), &len))
    {
        if (tet == TokenElevationTypeLimited)
        {
            if (GetTokenInformation(hToken, TokenLinkedToken, &tlt, sizeof(tlt), &len))
            {
                CloseHandle(hToken);
                hToken = tlt.LinkedToken;
            }
            else
            {
                dwError = GetLastError();
            }
        }
    }
    else
    {
        dwError = GetLastError();
    }

    if (dwError == NOERROR)
    {
        if (GetTokenInformation(hToken, TokenType, &tt, sizeof(tt), &len))
        {
            if (tt != TokenPrimary)
            {
                dwError = ERROR_INVALID_HANDLE;
            }
        }
        else
        {
            dwError = GetLastError();
        }

        if (dwError == NOERROR)
        {
            *phToken = hToken;
            return NOERROR;
        }

        CloseHandle(hToken);
    }

    return dwError;
}

and use next code for start child

HANDLE hToken;

ULONG dwError = GetElevatedUserToken(&hToken);

if (dwError == NOERROR)
{
    STARTUPINFO si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    //***
    if (CreateProcessAsUser(hToken, ***, &si, &pi))
    {
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
    }

    CloseHandle(hToken);
}

in this case you may be not need at all impersonate to LocalSystem in child process. however if still need LocalSystem - you can duplicate such token in child process and in this case SetThreadtoken will be full ok, because child process will be have impersonate privileges

RbMm
  • 31,280
  • 3
  • 35
  • 56
  • Keep in mind that this will only work if the logged-on user has administrator privilege. Whether or not that is a problem depends on the intended use. – Harry Johnston Aug 11 '17 at 23:37
  • @HarryJohnston - yes, you right. i forget clear say, that user need have potential administrator privileges. unfortunately, by design, we can not use impersonation on thread from restricted process. even if we try do this from local system process or even from kernel mode – RbMm Aug 11 '17 at 23:42
  • I guess that if you *could* do that, the outcome would be a thread with administrative privilege that a non-privileged user has complete control over. So it makes sense that it is not allowed. – Harry Johnston Aug 11 '17 at 23:44
0

Forgive me for asking what should be obvious but it needs to be asked:

Are you checking the return values of these functions? Calling GetLastError when they fail? What error codes are you getting back?

If this is C++ are you setting an unhandled exception handler?

Joe
  • 5,394
  • 3
  • 23
  • 54
  • I handled all error codes, `CreateFile` is crashing application though file exists and contents can be read if I commented out impersonation method. – jeeran Aug 10 '17 at 12:55
  • Fair enough. Next step is Win32 SetUnhandledExceptionFilter wrapping the CreateFile call. That should catch the "crash" live and give you enough info to at least dump it out out somewhere... https://msdn.microsoft.com/en-us/library/windows/desktop/ms680634(v=vs.85).aspx Edit: I should say a structured exception try block around the CreateFile call but you should have an unhandled exception filter already set up. Note that I am assuming you are *not* using C++ exceptions here. You cannot mix C++ and structured exceptions in the same function. – Joe Aug 10 '17 at 13:12