ShadowChain

Working of ShadowChain DLL Injector

This blog post is based on my research article published in IEEE access named - "A Threat-Informed Approach to Malware Evasion using DRM and TLS Callbacks". All diagrams have been taken from that paper.

ShadowChain is a modular digital enabled dll injector that has the capabilities of Anti-debugging and persistence. It is meant to demonstrate how APTs or threat groups could utilize DRM to prevent malware analysis and reverse engineering.

Think of a example usage scenario as follows - An APT wants to install an information stealer on the machine of a fortune 500 company's CISO. They want it to be stealthy and if caught, buy them as much time possible to get the job done. For this, they can utilize DRM to make sure the malware can't be analyzed easily.

The DRM forces the malware to execute in a specified computer which prevents sandbox analysis or analysis in a controlled environment. But defenders could potentially use debuggers to bypass it and redirect execution flow. Thus in order to prevent that and protect our DRM, anti-debugging using TLs callbacks is used. A TLS thread detects if a debugger is running even before the main thread starts and nulls out the entire malware. Thus rendering debugging useless. Of course more advanced techniques could also be used.

A simple persistence and DLL injection technique is used as a proof of concept but APTs could and will use more advanced techniques. IAT camouflage is utilized here as an auxiliary function to throw off defenders. Our β€œwhitelisted” APIs will confuse the static analysis scanners and allow the malicious binary to persist on disk if required.


Features Of ShadowChain

  1. Digital Rights Management(DRM) using volume serial number of the machine

  2. Anti-debugging using TLS Callbacks

  3. IAT Camouflage

  4. Remote process DLL Injection

  5. Persistence using Startup Folder


Explanation Of Working Of ShadowChain

Overall Flowchart Of ShadowChain

You can click on the expandable boxes down to get copious details of each module

Digital Rights Management (DRM)

The IsSameMachine() function in the ShadowChain.c file is responsible for implementing a Digital Rights Management (DRM) mechanism. This function ensures that the program runs only on the machine it was originally installed on by checking and patching the executable with the machine’s volume serial number. Here is a detailed explanation of how the IsSameMachine() function works:

The IsSameMachine() function performs the following steps:

  1. Retrieves the volume serial number of the C: drive.

  2. Compares the retrieved serial number with a stored constant value.

  3. If the serial number matches the stored value, it confirms that the program is running on the same machine.

  4. If the serial number does not match, it checks if the stored value is an initial placeholder value.

  5. If the stored value is the initial placeholder, it patches the executable with the current serial number.

  6. If the stored value is not the initial placeholder, it indicates that the program is running on a different machine.

Working of The DRM
  • The function uses GetVolumeInformationW to retrieve the volume serial number of the C: drive.

  • If the function fails or the serial number is zero, it prints an error message and returns FALSE.

  • The function prints the retrieved serial number and the stored constant value (g_dwSerialNumberConstVariable).

  • If the retrieved serial number matches the stored value, it confirms that the program is running on the same machine and returns TRUE.

Working Of DRM
  • If the stored value does not match the retrieved serial number, the function checks if the stored value is the initial placeholder (INITIAL_VALUE).

  • If the stored value is not the initial placeholder, it indicates that the program is running on a different machine and returns FALSE.

Patch Executable with Current Serial Number:

  • If the stored value is the initial placeholder, the function proceeds to patch the executable with the current serial number.

  • It retrieves the path of the current executable and reads its contents into memory using ReadSelfFromDiskW.

  • It locates the NT headers and the .rdata section where the serial number is stored.

  • It searches for the initial placeholder value in the .rdata section and replaces it with the current serial number.

  • It deletes the old executable from disk using DeleteSelfFromDiskW and writes the patched executable back to disk using WriteSelfToDiskW.

  • If the patching is successful, it sets the result to TRUE.

  • The function performs cleanup by freeing the allocated memory and returns the result.

The ReadSelfFromDiskW function reads the executable image of the current process from disk. Here is a detailed explanation of how the function works:

It takes three parameters.

  • szLocalImageName: The path to the executable image.

  • pModule: A pointer to store the address of the read image.

  • pdwFileSize: A pointer to store the size of the read image.

After accepting the required parameters, it -

  • Initializes variables for the file handle, file buffer, file size, and number of bytes read.

  • Checks if the input parameters are valid. If not, returns FALSE.

  • Opens the file for reading. If it fails, prints an error message and jumps to the cleanup section.

  • Retrieves the file size. If it fails, prints an error message and jumps to the cleanup section.

  • Allocates memory to store the file contents. If it fails, prints an error message and jumps to the cleanup section.

  • Reads the file into the allocated buffer. If it fails, prints an error message and jumps to the cleanup section.

  • Stores the file buffer address and size in the output parameters.

  • Closes the file handle and frees the allocated memory if necessary. Returns TRUE if successful, FALSE otherwise.

The WriteSelfToDiskW function writes the executable image to disk. Here is a detailed explanation of how the function works:

It takes three parameters.

  • szLocalImageName: The path to the executable image.

  • pImageBase: The address of the image to be written.

  • sImageSize: The size of the image to be written.

After accepting the required parameters, it -

  • Initializes variables for the file handle and number of bytes written.

  • Checks if the input parameters are valid. If not, returns FALSE.

  • Opens the file for writing. If it fails, prints an error message and jumps to the cleanup section.

  • Writes the image to the file. If it fails, prints an error message and jumps to the cleanup section.

  • Closes the file handle and returns TRUE if the write was successful, FALSE otherwise.

The DeleteSelfFromDiskW function deletes the executable image from disk by renaming it and then setting it for deletion.

It only takes one parameter.

  • szFileName: The path to the executable image to be deleted.

Then it -

  • Initializes variables for the result, file handle, file disposition info, and file rename info.

  • Checks if the input parameter is valid. If not, returns FALSE.

  • Generates a random name for the file.

  • Opens the file for deletion. If it fails, prints an error message and jumps to the cleanup section.

  • Renames the file. If it fails, prints an error message and jumps to the cleanup section.

  • Closes the file handle and reopens the file for deletion. If it fails, prints an error message and jumps to the cleanup section.

  • Sets the file for deletion. If it fails, prints an error message and jumps to the cleanup section.

  • Sets the result to TRUE if the file was successfully deleted.

  • Closes the file handle and returns the result.

Remote Process DLL Injection
DLL Injection Flowchart

The remote process Dll Injection takes place through two different functions. -

  1. GetRemoteProcessHandle Function

The GetRemoteProcessHandle function enumerates processes and gets the handle of a specified remote process. Here is a detailed explanation of how the function works:

Parameters

  • szProcessName: The name of the process to find.

  • dwProcessID: A pointer to store the process ID of the found process.

  • hProcess: A pointer to store the handle of the found process.

First it Initializes a PROCESSENTRY32 structure to store information about the processes.

Then it initializes a variable for the snapshot handle

Then it gets a snapshot of all running processes

  • Creates a snapshot of the processes using CreateToolhelp32Snapshot.

  • If the snapshot creation fails, prints an error message and jumps to the cleanup section.

  • Retrieves information about the first process in the snapshot using Process32First.

  • If the retrieval fails, prints an error message and jumps to the cleanup section.

  • Iterates through the remaining processes in the snapshot using Process32Next.

  • If the process name matches the specified process name, retrieves the process ID and opens a handle to the process using OpenProcess.

  • If the handle opening fails, prints an error message.

  • Closes the snapshot handle and returns TRUE if the process was found and the handle was successfully opened, FALSE otherwise.

  1. InjectDllToRemoteProcess Function

The InjectDllToRemoteProcess function injects a DLL into a remote process. Here is a detailed explanation of how the function works:

Parameters

  • hProcess: The handle of the remote process.

  • DllName: The name of the DLL to be injected.

First it Initializes variables for the state, the address of LoadLibraryW, and the address in the remote process.

It then calculates the size of the DLL name in bytes.

Then it loads LoadLibraryW Function:

  • Retrieves the handle of kernel32.dll using GetModuleHandle.

  • If the handle retrieval fails, prints an error message and jumps to the cleanup section.

  • Retrieves the address of LoadLibraryW using GetProcAddress.

  • If the address retrieval fails, prints an error message and jumps to the cleanup section.

Then allocates and loads DLL in remote process

  • Allocates memory in the remote process using VirtualAllocEx.

  • If the memory allocation fails, prints an error message and jumps to the cleanup section.

  • Writes the DLL name to the allocated memory in the remote process using WriteProcessMemory.

  • If the memory writing fails, prints an error message and jumps to the cleanup section.

  • Creates a remote thread in the remote process to load the DLL using CreateRemoteThread.

  • If the thread creation fails, prints an error message and jumps to the cleanup section.

  • Closes the thread handle and returns the state (TRUE if successful, FALSE otherwise).

Anti-Debugging Using TLS Callbacks

TLS callbacks are a set of callback functions specified within the TLS directory of a PE file, these callbacks are executed by the Windows loader before thread creation, meaning that a TLS callback can be executed before the main thread. From an anti-analysis perspective, TLS callbacks can be used to check if the implementation is being analyzed before executing the main function.

Flowchart Of TLS module

The ReadSelfFromDiskW function reads the executable image of the current process from disk. Here is a detailed explanation of how the function works:

Preprocessor code

Linker Directives

  • This directive instructs the linker to include the symbol _tls_used in the output file. This is necessary to ensure that the TLS (Thread Local Storage) callbacks are properly registered and executed. #pragma comment (linker, "/INCLUDE:_tls_used")

  • This directive instructs the linker to include the symbol CheckIfImgOpenedInADebugger in the output file. This is necessary to ensure that the TLS callback function ADTlsCallback is properly registered and executed. #pragma comment (linker, "/INCLUDE:CheckIfImgOpenedInADebugger")

Constants And Macros

  • Defines the size (in bytes) to be overwritten in the main function if an INT 3 instruction is detected. The value 0x500 (1280 bytes) is used as the overwrite size. #define OVERWRITE_SIZE 0x500

  • Defines the opcode for the INT 3 instruction, which is commonly used by debuggers to set breakpoints. The value 0xCC is the opcode for the INT 3 instruction. #define INT3_INSTRUCTION_OPCODE 0xCC

  • Defines the size of the error buffer used in the PRINT macro. The value is set to twice the maximum path length (MAX_PATH), which is typically 260 characters. #define ERROR_BUF_SIZE (MAX_PATH * 2)

  • Defines a macro for printing formatted strings to the console. The macro allocates a buffer, formats the string, writes it to the console, and then frees the buffer.

Custom memset Function

  • Declares an external memset function with the __cdecl calling convention.

  • Instructs the compiler to use the intrinsic version of the memset function, if available.

  • Instructs the compiler to use the user-defined version of the memset function, overriding the intrinsic version.

  • Implements a custom memset function that fills a block of memory with a specified value.

TLS Callback Function Prototypes

  • Declares the prototype for the TLS callback function ADTlsCallback.

  • Instructs the compiler to place the following constant data in the .CRT$XLB section.

  • Defines a constant pointer to the ADTlsCallback function and places it in the .CRT$XLB section. This ensures that the TLS callback function is registered and executed when the process is attached.

  • Resets the section to the default.

TLS Function Code

The ADTlsCallBack function requires three parameters -

  • hModule: A handle to the module.

  • dwReason: The reason for the callback. This can be one of several values, such as DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH, or DLL_PROCESS_DETACH.

  • pContext: Reserved for future use and is typically NULL.

After accepting the parameters, the function -

  • Initializes a variable to store the old protection attributes of the memory region.

  • Checks if the reason for the callback is DLL_PROCESS_ATTACH, which indicates that the process is being attached.

  • Uses the PRINT macro to print the address of the main function.

  • Checks if the first byte of the main function is the INT 3 instruction opcode (0xCC), which is commonly used by debuggers to set breakpoints.

  • If the entry point is patched with the INT 3 instruction, it prints a warning message.

  • Attempts to change the protection of the memory region containing the main function to PAGE_EXECUTE_READWRITE using VirtualProtect.

  • If successful, it overwrites the main function with 0xFF bytes using the custom memset function.

  • Prints a message indicating that the main function has been overwritten.

  • If the VirtualProtect call fails, it prints a message indicating that it failed to overwrite the entry point.

IAT Camouflage

The IATCamoflage2 function is designed to add whitelisted APIs to camouflage the Import Address Table (IAT). This function allocates memory, performs some checks, and then calls various registry-related functions to obfuscate the IAT. This is essential to obfuscate β€œOffensive APIs” by importing a bunch of useless whitelisted APIs.

Flowchart of IAT Camoflague

Detailed Steps

  1. Allocate Memory:

    • Allocates 256 bytes (0x100) of zero-initialized memory from the process heap.

    • If the allocation fails, the function returns immediately.

  1. Perform a Check on the Allocated Address:

    • Shifts the allocated address right by 8 bits and masks it with 0xFF.

    • If the result is greater than 0xFFFF, it calls various registry-related functions with NULL parameters. This is done to obfuscate the IAT by adding these function calls to the import table.

  1. Free the Allocated Memory:

    • Frees the allocated memory.

    • If the memory cannot be freed, the function returns immediately.

Persistence Using Startup Folder

The MoveToStartup function moves the current running binary to the startup folder to ensure it runs on system startup. Here is a detailed explanation of how the function works:

Persistence flowchart

Detailed Steps

  1. Initialize Variables:

    • Initializes variables to store the paths of the startup folder, the current binary location, and the new path in the startup folder.

  1. Get the Path of the Startup Folder:

    • Retrieves the path of the startup folder using SHGetFolderPath.

    • If the retrieval fails, prints an error message and returns FALSE.

  1. Get the Current Location of the Binary:

    • Retrieves the current location of the binary using GetModuleFileName.

    • If the retrieval fails, prints an error message and returns FALSE.

  1. Construct the New Path in the Startup Folder:

    • Finds the last backslash in the current path to separate the directory from the executable name.

    • Constructs the new path in the startup folder by appending the executable name to the startup folder path using StringCchPrintf.

    • If the construction fails, prints an error message and returns FALSE.

  1. Copy the Binary to the Startup Folder:

    • Copies the binary to the startup folder using CopyFile.

    • If the copy operation fails, prints an error message and returns FALSE.

  1. Print Success Message and Return

    • Prints a success message indicating that the binary was successfully moved to the startup folder.

    • Returns TRUE.


Results

Initial run of ShadowChain
Subsequent Runs Of ShadowChain in Same machine
When the same binary is run under a different machine
Main function being nulled out when debugger is detected
Shadowchain persisting in startup folder after execution

Last updated