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
Digital Rights Management(DRM) using volume serial number of the machine
Anti-debugging using TLS Callbacks
IAT Camouflage
Remote process DLL Injection
Persistence using Startup Folder
Explanation Of Working 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:
Retrieves the volume serial number of the C: drive.
Compares the retrieved serial number with a stored constant value.
If the serial number matches the stored value, it confirms that the program is running on the same machine.
If the serial number does not match, it checks if the stored value is an initial placeholder value.
If the stored value is the initial placeholder, it patches the executable with the current serial number.
If the stored value is not the initial placeholder, it indicates that the program is running on a different machine.

The function uses
GetVolumeInformationWto 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.
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
.rdatasection where the serial number is stored.It searches for the initial placeholder value in the
.rdatasection and replaces it with the current serial number.It deletes the old executable from disk using
DeleteSelfFromDiskWand writes the patched executable back to disk usingWriteSelfToDiskW.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
TRUEif successful,FALSEotherwise.
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
TRUEif the write was successful,FALSEotherwise.
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
TRUEif the file was successfully deleted.Closes the file handle and returns the result.
Remote Process DLL Injection
The remote process Dll Injection takes place through two different functions. -
GetRemoteProcessHandleFunction
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
TRUEif the process was found and the handle was successfully opened,FALSEotherwise.
InjectDllToRemoteProcessFunction
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.dllusingGetModuleHandle.If the handle retrieval fails, prints an error message and jumps to the cleanup section.
Retrieves the address of
LoadLibraryWusingGetProcAddress.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 (
TRUEif successful,FALSEotherwise).
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.
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_usedin 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
CheckIfImgOpenedInADebuggerin the output file. This is necessary to ensure that the TLS callback functionADTlsCallbackis properly registered and executed.#pragma comment (linker, "/INCLUDE:CheckIfImgOpenedInADebugger")
Constants And Macros
Defines the size (in bytes) to be overwritten in the
mainfunction if anINT 3instruction is detected. The value0x500(1280 bytes) is used as the overwrite size.#define OVERWRITE_SIZE 0x500Defines the opcode for the
INT 3instruction, which is commonly used by debuggers to set breakpoints. The value0xCCis the opcode for theINT 3instruction.#define INT3_INSTRUCTION_OPCODE 0xCCDefines the size of the error buffer used in the
PRINTmacro. 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
memsetfunction with the__cdeclcalling convention.Instructs the compiler to use the intrinsic version of the
memsetfunction, if available.Instructs the compiler to use the user-defined version of the
memsetfunction, overriding the intrinsic version.Implements a custom
memsetfunction 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$XLBsection.Defines a constant pointer to the
ADTlsCallbackfunction and places it in the.CRT$XLBsection. 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 asDLL_PROCESS_ATTACH,DLL_THREAD_ATTACH,DLL_THREAD_DETACH, orDLL_PROCESS_DETACH.pContext: Reserved for future use and is typicallyNULL.
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
PRINTmacro to print the address of themainfunction.
Checks if the first byte of the
mainfunction is theINT 3instruction opcode (0xCC), which is commonly used by debuggers to set breakpoints.If the entry point is patched with the
INT 3instruction, it prints a warning message.
Attempts to change the protection of the memory region containing the
mainfunction toPAGE_EXECUTE_READWRITEusingVirtualProtect.If successful, it overwrites the
mainfunction with0xFFbytes using the custommemsetfunction.Prints a message indicating that the
mainfunction has been overwritten.
If the
VirtualProtectcall 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.
Detailed Steps
Allocate Memory:
Allocates 256 bytes (
0x100) of zero-initialized memory from the process heap.If the allocation fails, the function returns immediately.
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 withNULLparameters. This is done to obfuscate the IAT by adding these function calls to the import table.
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:
Detailed Steps
Initialize Variables:
Initializes variables to store the paths of the startup folder, the current binary location, and the new path in the startup folder.
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.
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.
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.
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.
Print Success Message and Return
Prints a success message indicating that the binary was successfully moved to the startup folder.
Returns
TRUE.
Results





Last updated