FinFisher Malware Analysis - Part 3 (Last)

  • Posted on: 9 October 2014
  • By: siteadm

I've already covered most parts of FinFisher malware in last two articles (part1, part2). This time, in this article, which is last article related to FinFisher, I'll cover last important tricks, methods and techniques used by FinFisher. So I'll make categorize them by subject:

A) User-mode hooks and code (DLL) injection into all processes

This is possibly most revealing and unnecessary function of FinFisher, it injects DLL into all processes, install user-mode hooks and checks for existence of hooks in all processes persistently. Initially it happens in the DLL dropped from main FinFisher malware, which FinFisher malware calls it msvcr90.dll. This compressed and encrypted DLL, stays on disk in an encrypted format (XORing each 4 bytes to their next 4 byte will decrypt it, plus an initial value for first 4 byte), but main FinFisher malware executable, will load it, decrypt it and run it. This DLL does have an exported function called "RunDll". When this function simply called (rundll32.exe msvcr90.dll,RunDll) it will start to create several threads. One of the threads entry point is sub_40DB45. This function simply keeps calling sub_40C0E1 which it's sole purpose is allocating a memory space (0x1000 bytes), calling ZwQuerySystemInformation API with first parameter (SYSTEM_INFORMATION_CLASS) being 0x05 which is SystemProcessInformation. So it keeps getting list of all running processes, then it checks process names to taskmgr.exe, procexp.exe and procexp64.exe. If it matches, it tries to create an event called "ntapi + PID of process it wants to inject code into", if it returns error 5 (ACCESS_DENIED) which means, it already injected code into this PID and such event already exists, it won't do it again, but if it returns any error code other than ACCESS_DENIED, it will call sub_40D5C1 with PID of process it wants to inject code into. So psuedo code of this thread would be:

void sub_40C0E1()
	DWORD retLen;
	DWORD dwSize = 0x100000;
	while (1)
		DWORD lpAddress = VirtualAlloc(0, dwSize, 0x1000, 0x04);
		if (!lpAddress) 
			if ( !ZwQuerySystemInformation(5, lpAddress, dwSize, &retLen )
				return lpAddress;
			VirtualFree(lpAddress, 0, 0x4000);
			VirtualFree(lpAddress, 0, 0x8000);
			dwSize += 65536;

DWORD WINAPI sub_40D845( LPVOID lpParam )
	UNICODE_STRING Proc1, Proc2, Proc3;
	wchar_t EventName[MAX_PATH];
	DWORD retVal, errCode;
	RtlInitUnicodeString(&Proc1, L"taskmgr.exe");
	RtlInitUnicodeString(&Proc2, L"procexp.exe");
	RtlInitUnicodeString(&Proc3, L"procexp64.exe");
	while ( WaitForSingleObject(((HANDLE *)lpParam->StopInjection), 500))
		DWORD ProcessListMemoryPage = sub_40C0E1();
		while (1)
			if ((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName) // ProcessName is not NULL
				if ((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ProcessId) // PID is not NULL
					if (RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc1, 1) || 
						RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc2, 1) ||
						RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc3, 1))
						// sub_40F055 is just a string formatting function, I simply renamed it to FormatString
						FormatString(&EventName, MAX_PATH, L"%s%d", L"ntapi", (PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ProcessId);
						retVal = OpenEventW(EVENT_ALL_ACCESS, 0, &EventName);
						errCode = GetLastError();
						if (retVal) 
							if (errCode != 5)
								InjectCode(lpParam,  (PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ProcessId, 0);
						if (!DWORD*)ProcessListMemoryPage)
						ProcessListMemoryPage = (PSYSTEM_PROCESS_INFORMATION)(((PUCHAR)ProcessListMemoryPage)+ ProcessListMemoryPage->NextEntryOffset);

I hope FinFisher won't mind me sharing their source codes. Anyway, this is a thread and it keeps running, it keeps injecting a DLL into all Windows task manager and Sysinternals process explorer (32 and 64 bit) instances. This is done repeatedly, but there is surprisingly another function that initially injects DLL to all already running processes and hooks CreateProcess API and several other APIs in them, so it will be aware of all new process creations and subsequently can inject code into them too. That function is sub_40D77F. Psuedo code for that function would be:

int sub_40D77F(HINSTANCE hInst)
	UNICODE_STRING Proc1, Proc2, Proc3, Proc4, Proc5, Proc6, Proc7, svchostProc;
	DWORD nNumberOfBytesToWrite;
	RtlInitUnicodeString(&Proc1, L"smss.exe");
	RtlInitUnicodeString(&Proc2, L"csrss.exe");
	RtlInitUnicodeString(&Proc3, L"spoolsv.exe");
	RtlInitUnicodeString(&Proc4, L"wininit.exe");
	RtlInitUnicodeString(&Proc5, L"lsm.exe");
	RtlInitUnicodeString(&Proc6, L"lsass.exe");
	RtlInitUnicodeString(&Proc7, L"services.exe");
	RtlInitUnicodeString(&svchostProc, L"svchost.exe");
	DWORD winver = LOBYTE(LOWORD(GetVersion()));
	wchar_t CurDir[MAX_PATH], FileName[MAX_PATH];
	memset(CurDir, 0, 520);
	GetCurrentDirectory(MAX_PATH, CurDir);
	// actually it is sub_404919, which just sort of an strncpy
	SortOfStrNCpy(icoStr, 260, L"ico"); 
	FormatString(&FileName, MAX_PATH, L"%s\\%s%s", &CurDir, &icoStr, L"_ty23.ico");
	// Locates data from resource section with given parameters return HANDLE (HGLOBAL)
	lpBuffer = sub_40D4FB(hInst, "BIN", 103, &nNumberOfBytesToWrite);
	 // reads nNumberOfBytesToWrite from given HGLOBAL handle
	sub_402E05(lpBuffer, nNumberOfBytesToWrite);
	// Decrypt Data (XOR initial 4 byte to 0x5F1ECA67, XOR each 4 byte (before decryption) to next 4 byte)
	sub_402DCD(lpBuffer, nNumberOfBytesToWrite);
	// Write to File
	sub_40D554(&FileName, lpBuffer, (DWORD)nNumberOfBytesToWrite);
	DWORD ProcessListMemoryPage = sub_40C0E1();
	while (1)
		if ( ((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName &&
			 (PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ProcessId) &&
			 !RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc1, 1)
			 !RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc2, 1)
			 !RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc3, 1)
			 !RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc4, 1)
			 !RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc5, 1)
			 !RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc6, 1)
			 !RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, Proc7, 1)
			 && (winver < 6 || (PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->SessionId || RtlEqualUnicodeString((PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ImageName, svchostProc, 1) ))
			FormatString(&EventName, MAX_PATH, L"%s%d", L"ntapi", (PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ProcessId);
			retVal = OpenEventW(EVENT_ALL_ACCESS, 0, &EventName);
			errCode = GetLastError();
			if (nNumberOfBytesToWrite) break;
			if (retVal == 5) continue;
			FormatString(&SecondEvent, MAX_PATH, L"\\Session\\%d\\BaseNamedObjects\\%s%d", 
			retVal2 = OpenEventW(EVENT_ALL_ACCESS, 0, &SecondEvent);
			if (retVal2) 
				if (errCode != 5)
						InjectCode(lpParam,  (PSYSTEM_PROCESS_INFORMATION)ProcessListMemoryPage->ProcessId, 0);
		if (!DWORD*)ProcessListMemoryPage)
		ProcessListMemoryPage = (PSYSTEM_PROCESS_INFORMATION)(((PUCHAR)ProcessListMemoryPage)+ ProcessListMemoryPage->NextEntryOffset);
	int CurProccess = GetCurrentProcess();
	// To free allocated lpBuffer
	sub_40D442(CurProccess, lpBuffer);
	return 1;

So in all OSes prior to Windows Visa (dwMajorVersion < 6), above code causes injection into ALL processes, including those in exception list (smss, csrss, spoolsv, etc.). 

The injected code (ico_ty23.ico) which is a DLL, will install user-mode hooks, hooked functions are:

// Kernel32 hooks

// Advapi32 hooks

// Gdi32 Hooks

// ntdll hooks

So using these hooks, malware will be able to hide any process, hide any network connection, hide registry values, capture all image files being displayed, capture printed documents and much more...

This DLL first reads and decrypts (same XOR with 4 byte initial value of 0x5F1ECA67) resource ID BIN\101. This is dynamically generated from injector, injector process writes (encrypted with same XOR algorithm) location of original injector DLL which also does have all other required files (random folder in Windows\Installer) into resource of ico_ty23.dll before injection, so injected code will know location of required files.

About hooking process, sub_362AAC in ico_ty23.dll is responsible for storing original function's address + 5 (bypassing hook jump, so calling original createprocess) and overwriting first 5 byte with jmp HookFuncAddr. Initially, it only installs hook for CreateProcessA, CreateProcessW, CreateProcessAsUserA and CreateProcessAsUserW. Then it sends signal through LPC (Local Procedure Call - NtRequestWaitReplyPort API), if injector signals back OK, it will install additional hooks. This message's identifier is 0xAAAAAAAA. It's unique in injector (msvcr90.dlland injected DLL (ico_ty23.dll). There is 3 more message identifier which is not related to this subject, but they are 0xBBBBBBBB, 0xCCCCCCCC and 0xDDDDDDDD. Anyway, additional hooks (if OK signal received) are:

ntdll.ZwDeviceIoControlFile, ntdll.ZwEnumerateValueKey, ntdll.ZwQueryKey, ntdll.ZwQuerySystemInformation, advapi32.OpenTraceA, advapi32.OpenTraceW

Now DLL checks to see if running process name is opera.exe, if it's not, it will install additional hooks for gdi32.dll, but if it's opera.exe, it wont:

Those hooks are (all from gdi32.dll)

GetDeviceCaps, StartDocA, StartDocW, EndPage, EndDoc, StartPage, SetAbortProc, DeleteDC, DPtoLP, Escape, ResetDCA, ResetDCW, CreateDCA, CreateDCW.

Each hook does have some use, for example:

- CreateProcessA hook will call real CreateProcessA and then it will inject it's own module into it.

- CreateProcessW hook will call real CreateProcessW, but first it will check for it's lpEnvironment (7th argument) for "fore" string. If it was a match, it won't inject into this process and will remove "fore" from lpEnvironment parameter and will make it NULL before calling real CreateProcessW.

- CreateProcessAsUserA hook will call real CreateProcessAsUserA and then it will inject it's own module into it.

CreateProcessAsUserW is same as CreateProcessAsUserW, but with one (I think) unnecessary additional piece of code. It checks current process name (now injector) to winlogon.exe, if it's winlogon.exe, it does following:

    memset(SysDir, 0, 520);
    memset(CurDir, 0, 520);
    GetCurrentDirectoryW(MAX_PATH, &CurDir);
    GetSystemDirectoryW(SysDir, MAX_PATH);

If you think this piece of code for winlogon.exe does have some special meaning, please let me know.

ZwDeviceIoControlFile hook is interesting. Initially, it calls real ZwDeviceIoControlFile, then it checks results and hide some of them:

  •  If IOControlCode (6th parameter of ZwDeviceIOControlFile) was 0x12001B (IOCTL_NSI_GETALLPARAM), it will call sub_3637D6. This function will use LPC to communicate with main malware component, it will send signal with 0xCCCCCCCC identifier, to receive hostname of malware (FinFisher's C&C server, which in my sample, it was If result of ZwDeviceIOControlFile with IOControlCode of IOCTL_NSI_GETALLPARAM had FinFisher malware's hostname/ip, this function (sub_3637D6) will return 1 and it will cause hooker function to remove the hostname entry from result. So you will not be able to see communication between your computer to FinFisher C&C server from any process that does have ico_ty23.dll hooks installed.
  • if IOControlCode was 0x12003 (AFD_BIND) and InputBufferLength (8th parameter) was 36, it does same as it did for IOCTL_NSI_GETALLPARAM (hiding hostname from result)

- ZwEnumerateValueKey hook, first will call real ZwEnumerateValueKey with given parameters, if returned key value was "rundll32" string, it will increase Index parameter by one (2nd parameter), then it will call real ZwEnumerateValueKey and return result. This will hide any registry entry with "rundll32" as value name.

- ZwQueryKey hook is interesting too. This hook will check to see if the key being queried does have "rundll32" inside. If it does have, it checks for _KEY_INFORMATION_CLASS (second parameter), if it's KeyFullInformation or KeyCachedInformation, it will decrement size of entries inside this key by one.

- ZwQuerySystemInformation hook is simply just for hiding process. It checks SYSTEM_INFORMATION_CLASS parameter to see if it is SystemProcessInformation, if so, it communicates through LPC to see if there is any PID needs to be hidden, if so, it hides given PID. As far as I've seen, it can only hide one process, the code structure doesn't have loop, so it can hide only one process.

- OpenTrace hooks (A and W), does something unusual, something I've never seen before, it's really interesting. Each time OpenTrace API being called, before calling real OpenTrace API, it modifies it's EventCallback parameter to point to a new location. Hook function, allocates a new 0x14 (20 bytes) in memory, sets it's protection to PAGE_EXECUTE_READWRITE. Then it writes following code into newly allocated 20 bytes space:

PUSH 0x????????? // original (real) Callback function address
MOV EAX, 0x363B4E // hook function
RETN 0x04

So not only it hooks OpenTrace function, it also installs a hook for callback function of OpenTrace API result. In callback hook function, it checks for PID to hide, it checks PEVENT_TRACE->PEVENT_TRACE_HEADER->ProcessID, then communicates using LPC to get PID of process to hide and then remove it from result.

I think it's enough about injection and hooks part of FinFisher, even thought it does interesting stuff with CreateDC, EndPage and EndDocHook etc. and how they communicate through mailslot protocol, but already the article became too long and there is too much of other things to say about this malware. Generally speaking, these hooks are there for capturing files sent for printing, it captures anything victim tries to print.

B) MBR modification and mssounddx and driverw

For analysis of MBR, first of all, to make sure I'm not analyzing wrong code, I allowed FinFisher to infect my MBR, I did it so by redirecting code's flow manually to MBR infector function and allowed it to run. After MBR infection, I confirmed infection with MBRCheck. As next step, I used Hiew32 to dump well enough of MBR code. If you want to do same, you need to do followings:

  • Open physical drive: hiew32 \\.\PhysicalDrive0
  • Switch to hex mode: Press F4, then choose hex.
  • At very beginning of the disk, press *, now hold page down well enough to copy entire MBR and who cares, some additional bytes. Then re-press *. This will select the area between two locations you pressed *.
  • Now press F2, enter filename, don't enter offset and press Enter. Now you got whole MBR and maybe also some additional parts.

As next step, I used MBR package from HexBlog and created my image file to run in IDA. Here you can see the result:

Fun started very early at initial MBR code analysis, by seeing FinFisher using PUSH + RET to jmp to new location (0x0000:7C37). Then it checks for extensions (AH = 0x41, BX = 55AA, INT 0x13). As result EBX becomes 0xAA55 and AH holds major version of extension, CX will have support bits. Next it calls interrupt 0x13 again, this time to get current drive parameters (ah = 08). After getting initial disk info, it calls INT 0x13 again, but this time for reading current drive parameters. For this purpose, it sets DL register to 0x80 which means first drive and set AH to 0x08

Next, it proceeds to next fun part, which is reading additional MBR code from last sectors of HDD and jumping to loaded MBR code. For reading data from disk, it uses INT 0x13 again, but this time AH = 0x42 and DS:SI points to disk address packet

Data in 0x1000 Before INT 13 call:

After INT 13 call:

Explanation of the call is here:

If you wonder how MBR calculates the LBA, it does not. Address of this additional MBR code is hard-coded (but dynamically calculated and generated per PC based on location that Finfisher malware chooses to store data in it) in MBR, which comes from malware during MBR infection. You can see that embedded value here: (highlighted in yellow)

The location of MBR backup and additional MBR code (next-stage) is 0x927 cluster before last byte in disk. So you can say, 0x927 x (Sectors Per Cluster, default is 8 in NTFS) x (Bytes Per Sector, which is 512) bytes before end of disk.

Reading bytes given address happens 0x14 (20) times. Here is the jump to location of data loaded from disk:

Here is the code at 0x1000 (next stage):

In next stage, first, it queries system address map, for this purpose, it puts 0xE820 function code in EAX and calls INT 0x15, see:

Next it stores original INT 13 handler two times in two different variables:

Variable 1:

Variable 2:

Finally storing new INT 13 handler address, which is 0x9D330:

Now it installed an INT 13 hook, so all INT 13 calls will be handled at 0x9D330 address. Now it needs to write some code at INT 13 handler address:

After successfully writing the code using rep movsb instruction:

Just 10 instruction later, it uses INT 13 handler which it's first INT 13 call that Finfisher INT13 hook should handle. This call starts reading REAL windows MBR code and overwrites it to original MBR location in memory, because windows MBR expects to be at 0000:7C00, so it writes original code at very same location and it jumps to "start" as IDA calls it, which is 7C00:


From now on, everything goes as expected, with one difference, all INT 13 calls are handled by Finfisher malware. So what this code does? INT 13 hook handler, cares about two function of INT 13, function AH=02 (Read Disk Sectors) and function AH=42 (Extended Read).

If AH != 02 and AH != 42, simply it will forward call to original INT 13 handler. When it's function 02 (read sector), it's handled at offset +0x9A. Here, it checks MBR to see if file system is NTFS or FAT. See:

So each one have separate handler as you can see in image above. Anyway, article already became too long and I have to cut it, so using this hook, malware is able to check all disk accesses, so it starts looking for a signature and it starts patching bytes, for instance one important part is here, before patch:

After patch:

Here is the result of patch:

Code above will cause jump to be taken to 0x09D5F2, actually disassembled code above is wrong, IDA took it wrong, anyway, correct code should be:

PUSH 8 / PUSH 0009D5F2 / RETFW

Before patch it was this:

Again disassembly is wrong, real code is:

PUSH 10101010 / PUSH 8 / PUSH EBX / RETFW

Which returns to a function responsible for choosing boot disk and start loading OS from chosen disk:

So we call this function OSLOADER, the OSLOADER_Hijacker function, does have some signature checks:

and when signature matches, it patches another function with JMP instruction and given address, then emulates original OSLOADER's push and jumps to where original OSLOADER was supposed to jump:

This patch is for getting notified when kernel is loaded, so, when kernel is loaded completely, it jumps to 0x0009D82D function which is loaded and controlled by malware. This part is NTOSKRNL patcher. This one scans memory to locate NTOSKRNL header and kernel base:

It does sounds interesting, doesn't it? Now it disables write protection bit (CR0) to be able to write to kernel code pages:

Next it copies 8 byte from 0x8056AAD6 (which is IOCreateDevice) to 0x806BF000, which is empty. Before:


and in next line (mov [edi], eax) it copies location of NTOSKRNL to next 4 bytes in front of patched 8 bytes, so next bytes (84 37 88 37) will become "00 70 4D 80" which is 0x804D7000

Next it patches original IoCreateDevice, simply it puts address of new IoCreateDevice (0x806BF000) with a JMP to location of IoCreateDevice. See:

Then it does have a one boring one-byte at time data copy loop, it reads code from memory where MBR code and additional data was loaded from last sectors of hard disk, here it starts copying scrambled driver code into kernel space, you can see MZ and 0x90 header with a little encryption.

After finishing copying bytes, it re-enables kernel protection. Later, it moves scrambled data to a new location:

Next step, it allows windows to load mssounddx service and it's binary. If it exists, it compares binary file of mssounddx service with it's own stored driver file. If they are match, it allows windows to load the service and moves on, if driver file doesn't exists or service binary doesn't match, it determines something is not right, at this point, it attempts to install and re-run driver itself. The MBR code and driver also share a unique Tag which is 0x11223344. Driver allocates a pool with this tag string:

So does the MBR code:

In an infected computer, when you run "poolmon /iD3??" command (0x11 doesn't represent printable char, 0x22 is double quote, 0x33 is "3" and 0x44 is "D") and considering that tag names are in reverse order, if you see following results, you're possibly infected:

That was about tag part, about the driver itself, The code responsible for loading driver is here. Here is screenshot before loading the driver data:

It loops until it reads whole driver file into 0x821AF000 with size of driver which is 11264 bytes. I dumped driver from memory here, MD5 of it was: 0C3DEDCD8CD69137641C60A046687C34 which is MD5 of mssounddx.sys.

After loading correct driver into memory, it calls sub_0B2A of this driver, which creates a kernel thread in memory. See driver:

and some codes below, it creates a kernel thread:

and now here is the code that MBR directly jumps at this function and causes a hidden thread creation:

So this is why anti-rootkit applications like GMER detects a thread, but can't detect it's origin, see:

I think I've covered enough of FinFisher malware. If anyone needs more details, just please leave a comment or contact me via email.


Outstanding work! TIL that it's possible to debug entire OS using Bochs and IDA! Please keep posting, I learn a lot from each of your articles.

The reason for the redirection for getsysystemdirectory is based on the fact that on Windows x64, this will return the path to your System32 directory regardless of your application being a native x64 application or if it is a x86 application running under WoW64.

What Windows relies on to get this working in WoW64 is the file system redirection. This makes it so that an x86 application will redirect the System32 directory to Syswow64 silently without anybody having to know or care. So even on x64 Windows, this directory is System32.
You could always write a simple test application yourself to check it out since calling this is easy

Could try to capture some network traffic and share the resulting pcap?
It would be very nice and useful to be able to create signatures for this malware based on that.

Nice writeup sir.
Any ideas as to why its impossible to grab the rootkit out of memory with volatility post exploitation? IDA yielded bad offsets. I had to single step the thing to get both the DLL responsible for remote communication and the driver file before it was deleted.

Another epic post. Waiting for the next

Nice job! Specially MBR analysis part, it was totally new to me. Is it possible to do kernel debugging using this method? I mean, instead of kernel debugger, can we use BOCHS and IDA?

The technique of hooking getmessage and putmessage are described several years ago (!) by CR0W on uninformed (so far I remember). At that time he did it with hooking the Nt.... Funcs. But anyway, spending 150 K Euro for that (No hypervisor stuff, simple XOR...) - Great, cause they're throwing out money instead of buying Teazers and riot sticks :-)))))!

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.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Enter the characters shown in the image.