Deep Analysis of KSLØT Keylogger (Turla APT)

5 minute read

Sample MD5: 59b57bdabee2ce1fb566de51dd92ec94

First I used DIE to see what type of binary we have, It seems that it’s a 64 bit DLL.

1

Next I loaded the dll into pestudio, we can see that is has a small number of imports and readable strings so I suspected that it loads the required functions dynamically (using LoadLibrary and GetProcAddress).

2

3

Now let’s fire up IDA and see how it goes.

First we see a call to sub_1800017D0.

4

This function calls a sub routine multiple times with parameters pointing to the .data section, so it might be decrypting some data.

5

If we take a look at maybe_decrypt function we can see that it XORs the data with some byte array so we can safely say that it’s a decryption routine.

6

Here we have two options, we can use a debugger to decrypt the data or we can use IDA Python to do that, for simplicity I will use a debugger here. we set a breakpoint after the call and examine the .data section in the dump.

7

Decrypted Data:

..int.......................................................KSL0
.TVer=21.0.._msimm.dat..SPUNINST.....Start.[u]:.[-h GetForegroun
dWindow]:%d......[%02d.%02d.%04d %02d:%02d:%02d.%03d]...[h]:%d..
[-pid GetWindowThreadProcessId]:%d......[-k GetWindowThreadProce
ssId]:%d....[pid]:%d....[-OpenProcess]:%d...[-pn GetModuleFileNa
meEx]:%d....[pn]:%s.....[-t GetWindowText]:%d...[t]:%s..<#RShift
><#LShift><#RCtrl><#LCtrl><!RShift><!LShift><!RCtrl><!LCtrl>-+[]
\;/`',.<PageUp><PageDown><NumLock><r/><r*><r-><r+><r1><r2><r3><r
4><r5><r6><r7><r8><r9><r0><r.><F1><F2><F3><F4><F5><F6><F7><F8><F
9><F10><F11><F12><Down><Up><Right><Left><Del><Print><End><Insert
><CapsLock><Enter><Backspace><Esc><Tab>.kernel32.dll............n

The decrypted data has some strings like <F1> ans <LShift>, we can assume that they are used to record the keystrokes.

Next we see a call to GetModuleHandleW and GetProcAddress then the results are passed as arguments along with the qword array unk_1800105A0 to sub_1800039C0. The module is kernel32.dll and the proc is GetProcAddress handle.

sub_1800039C0 is a huge function and it looks scary at first.

8

First this function stores the parameters in local variables then it stores some values into the rest of local variables, so far so good.

Next there is a call to sub_180001000 multiple times with the previously filled local variables as parameters.

9

10

Here we can see the data XORed with 0x55 so this function is used to decrypt the values of the local variables.

After the decryption routine we see a call to GetProcAddress, using the debugger we can see that it returns a handle to LoadLibraryA.

11

After that it loads the needed libraries then fills the qword array with the needed functions.

12

To get the imported functions I used the debugger to get them one by one (definitely there is a better option but I’m lazy). Then I used IDA Python to rename the addresses with the corresponding function names.

Imported Functions:

"GetProcessImageFileNameW"
"GetForegroundWindow"
"GetWindowThreadProcessId"
"GetWindowTextW"
"GetKeyboardState"
"GetKeyboardLayout"
"ToUnicodeEx"
"MapVirtualKeyExW"
"CallNextHookEx"
"SetWindowsHookExW"
"UnhookWindowsHookEx"
"GetMessageW"
"TranslateMessage"
"DispatchMessageW"
"CommandLineToArgvW"
"swprintf"
"wcsncat"
"wcsstr"
"wcscat"
"malloc"
"memset"
"memcpy"
"strlen"
"wcslen"
"wcsrchr"
"free"
"GetSystemTime"
"GetLastError"
"OpenProcess"
"CreateThread"
"SystemTimeToFileTime"
"FileTimeToSystemTime"
"FileTimeToLocalFileTime"
"CreateMutexW"
"OpenMutexW"
"GetFileSize"
"lstrcatW"
"GetModuleFileNameW"
"FindFirstFileW"
"FindClose"
"CreateFileW"
"SetFilePointer"
"WriteFile"
"CloseHandle"
"GetProcAddress"
"LoadLibraryA"
"GetUserNameExW"
import idc

base = 0x1800105A0

names = ["GetProcessImageFileNameW", "GetForegroundWindow", "GetWindowThreadProcessId", 
         "GetWindowTextW", "GetKeyboardState", "GetKeyboardLayout", "ToUnicodeEx",
         "MapVirtualKeyExW", "CallNextHookEx", "SetWindowsHookExW", "UnhookWindowsHookEx",
         "GetMessageW", "TranslateMessage", "DispatchMessageW", "CommandLineToArgvW",
         "swprintf", "wcsncat", "wcsstr", "wcscat", "malloc", "memset", "memcpy", "strlen",
         "wcslen", "wcsrchr", "free", "GetSystemTime", "GetLastError", "OpenProcess", 
         "CreateThread", "SystemTimeToFileTime", "FileTimeToSystemTime", "FileTimeToLocalFileTime",
         "CreateMutexW", "OpenMutexW", "GetFileSize", "lstrcatW", "GetModuleFileNameW", 
         "FindFirstFileW", "FindClose","CreateFileW", "SetFilePointer", "WriteFile", "CloseHandle",
         "GetProcAddress", "LoadLibraryA", "GetUserNameExW"]

offsets = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 36, 37, 38, 39, 40, 41, 42, 
           43, 44, 45, 46, 47, 15, 16, 17, 18, 19, 20, 21, 23, 24, 22, 25, 26, 27, 28,
           29, 30, 31, 32, 33, 34, 35]

for i in range(47):
	idc.set_name(base + (offsets[i] * 8), '__' + names[i])

14

Viola!!! great success, now for reversing the real keylogger functionality.

Back to the main function, the malware gets the username and domain name of the machine then creates a Mutex with the name DOMAIN_NAME.USER_NAME (in my case it was IEUSER-PC.IEUSER).

15

16

The last call is to sub_180003960 which create a new thread with a starting address at sub_180001B70

17

This function listens for windows messages (keystrokes in this case), but first it calls setup_the_hook function.

18

This function sets up the keyboard hook with the callback keyboardProc.

19

The callback function deals with all the keyboard keys with a huge switch statement. Each block of this switch loads the keyboard_consts character array (which was decrypted earlier while decrypting the .data section) then adds an offset to this consts array to get the right character for the corresponding key and concatenates it with the keystroke variable. This variable is passed as a parameter along with it’s length to sub_180001100 which I suspect is the logging function.

20

This function is quite long and complex but we can see a call to WriteFIle at the end of it, also note the loop with XORing which indicates that the logs are encrypted before being written.

21

Moving up we can see the call to CreateFileW, to get the new file path we can let the malware run in x64dbg.

The malware creates a file with the name msimm.dat (which was in the decrypted .data section) in the same directory of the malware.

22

If we take a look at the created file we see some garbage because the file was encrypted before being written.

23

We can write a small script to decrypt this file using the encryption keys.

24

The encryption process is just XOR with the encryption keys.

encryption_keys = [0x0a, 0x19, 0x59, 0x2d, 0x6c, 0x59, 0x6f, 0xfa, 0x8b, 0x6f, 0x9b, 0xff, 0x37, 
                   0x9b, 0xbd, 0x7b, 0x59, 0x4b, 0x7b, 0xdd, 0x0f, 0x64, 0x91, 0xc7, 0xd6, 0x9c,
                   0x6f, 0x7b, 0x9c, 0x01, 0x9c, 0x91, 0x79, 0xc7, 0xc8, 0xc9, 0xdf, 0xe1, 0xfa,
                   0xff, 0x04, 0x08, 0x59, 0xe6, 0x64, 0x6d, 0x37, 0x9b, 0x38, 0x81, 0x2d, 0x81,
                   0x65, 0x7d, 0x66, 0x9a, 0x6f, 0xbd, 0x65, 0x59, 0x4b, 0x2d, 0x1a, 0x63, 0x59,
                   0x7b, 0x65, 0x59, 0x59, 0x0b, 0x4e, 0x85, 0x8c, 0x91, 0x88, 0x59, 0x0c, 0x01,
                   0x4e, 0x3a, 0x0d, 0x58, 0x38, 0x16, 0x91, 0x57, 0x7e, 0x68, 0x6a, 0x55, 0x42,
                   0x55, 0x5d, 0xc5, 0x9e, 0x4e, 0x17, 0x3b, 0x0f, 0x42]

decrypted_data = []

with open("msimm.dat", "rb") as file:
	data = file.read()
	i = 0
	for c in data:
		decrypted_data.append(c^encryption_keys[i])
		i = (i+1)%100

with open("decrypted.dat", "wb") as file:
	file.write(bytearray(decrypted_data))

Looking at the decrypted file:

26

Great!!! We can see the logs for each process and that wraps up this analysis.

IOCs:

Hashes:

The keylogger: 59b57bdabee2ce1fb566de51dd92ec94

Mutexes:

$DOMAIN_NAME.$USER_NAME

Files:

msimm.dat