Process & Thread Notifications

Chapter 3

  • One of the most powerful features of drivers in the context of EDRs is the ability to be notified when a system event occurs. These system events might include creating or terminating new processes and threads, requesting to duplicate processes and threads, loading images, taking actions in the registry, or requesting a shutdown of the system.

  • To do this, the driver registers callback routines which notify the driver if any type of event occurs in the system. As a result of these notifications, the driver can take action. Sometimes it might simply collect telemetry from the event notification.

  • Alternatively, it might opt to do something like provide only partial access to the sensitive process, such as by returning a handle with a limited-access mask (for example, PROCESS_QUERY_LIMITED_INFORMATION instead of PROCESS_ALL_ACCESS).

  • Callback routines may be either pre-operation, occurring before the event completes, or post-operation, occurring after the operation. Pre-operation callbacks are more common in EDRs, as they give the driver the ability to interfere with the event or prevent it from completing, as well as other side benefits.

  • Post-operation callbacks are useful too, as they can provide information about the result of the system event, but they have some drawbacks. The largest of these is the fact that they’re often executed in an arbitrary thread context, making it difficult for an EDR to collect information about the process or thread that started the operation.

Process Notifications

  • Callback routines can notify drivers whenever a process is created or terminated on the system. These notifications happen as an integral part of the process creation or termination.

  • Below is the call stack for creation of a child process for notepad.exe which was spawned as a child process of cmd.exe.

2: kd> bp nt!PspCallProcessNotifyRoutines
2: kd> g
Breakpoint 0 hit
nt!PspCallProcessNotifyRoutines:
fffff803`4940283c 48895c2410           mov qword ptr [rsp+10h],rbx
1: kd> k
 # Child-SP RetAddr Call Site
00 ffffee8e`a7005cf8 fffff803`494ae9c2 nt!PspCallProcessNotifyRoutines
01 ffffee8e`a7005d00 fffff803`4941577d nt!PspInsertThread+0x68e
02 ffffee8e`a7005dc0 fffff803`49208cb5 nt!NtCreateUserProcess+0xddd
03 ffffee8e`a7006a90 00007ffc`74b4e664 nt!KiSystemServiceCopyEnd+0x25
04 000000d7`6215dcf8 00007ffc`72478e73 ntdll!NtCreateUserProcess+0x14
05 000000d7`6215dd00 00007ffc`724771a6 KERNELBASE!CreateProcessInternalW+0xfe3
06 000000d7`6215f2d0 00007ffc`747acbb4 KERNELBASE!CreateProcessW+0x66
07 000000d7`6215f340 00007ff6`f4184486 KERNEL32!CreateProcessWStub+0x54
08 000000d7`6215f3a0 00007ff6`f4185b7f cmd!ExecPgm+0x262
09 000000d7`6215f5e0 00007ff6`f417c9bd cmd!ECWork+0xa7
0a 000000d7`6215f840 00007ff6`f417bea1 cmd!FindFixAndRun+0x39d
0b 000000d7`6215fce0 00007ff6`f418ebf0 cmd!Dispatch+0xa1
0c 000000d7`6215fd70 00007ff6`f4188ecd cmd!main+0xb418
0d 000000d7`6215fe10 00007ffc`747a7034 cmd!__mainCRTStartup+0x14d
0e 000000d7`6215fe50 00007ffc`74b02651 KERNEL32!BaseThreadInitThunk+0x14
0f 000000d7`6215fe80 00000000`00000000 ntdll!RtlUserThreadStart+0x21
  • This call stack was obtained by using WinDbg to set a breakpoint on nt!PspCallProcessNotifyRoutines(), the internal kernel function that notifies drivers with registered callbacks of process-creation events. When the breakpoint is hit, the k command returns the call stack for the process under which the break occurred.

  • Whenever a user wants to run an executable, cmd.exe calls the cmd!ExecPgm() function. This stub ends up making the syscall for ntdll!NtCreateUserProcess(), where control is transitioned to the kernel.

Registering a Process Callback Routine

  • To register process callback routines, EDRs use one of the following two functions: nt!PsSetCreateProcessNotifyRoutineEx() or nt!PsSetCreateProcess NotifyRoutineEx2().

  • The latter can provide notifications about non-Win32 subsystem processes. These functions take a pointer to a callback function that will perform some action whenever a new process is created or terminated.

  • Below code shows how to register a callback function. This code registers the calback function and passes three arguments - PsCreateProcessNotifySubsystems, entry point of callback routine and a boolean value indicating whether the callback routine should be removed.

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegPath)
{
        NTSTATUS status = STATUS_SUCCESS;
        --snip--

        status = PsSetCreateProcessNotifyRoutineEx2(
                 PsCreateProcessNotifySubsystems,
                 (PVOID)ProcessNotifyCallbackRoutine,
                 FALSE
        );

         --snip--
         
}

void ProcessNotifyCallbackRoutine(
          PEPROCESS pProcess,
          HANDLE hPid,
          PPS_CREATE_NOTIFY_INFO pInfo)
{
          if (pInfo)
          {
                   --snip--
          }
}
  • PsCreateProcessNotifySubsystems, indicates the type of process notification that is being registered. This value tells the system that the callback routine should be invoked for all subsystems, including Win32 and WSL.

  • In the code, the entry point of the callback routine points to the internal ProcessNotifyCallbackRoutine() function. When process creation occurs, this callback function will receive information about the event.

  • The third argument is FALSE because in this example code, the callback routine is being registered. When we unload the driver, we’d set this to TRUE to remove the callback from the system. After registering the callback routine, we define the callback function itself.

Viewing Registered Callback Routines

  • When a new callback routine is registered, a pointer to the routine is added to an array of EX_FAST_REF structures, which are 16-byte aligned pointers stored in an array at nt!PspCreateProcessNotifyRoutine.

1: kd> dq nt!PspCreateProcessNotifyRoutine
fffff803`49aec4e0 ffff9b8f`91c5063f ffff9b8f`91df6c0f
fffff803`49aec4f0 ffff9b8f`9336fcff ffff9b8f`9336fedf
fffff803`49aec500 ffff9b8f`9349b3ff ffff9b8f`9353a49f
fffff803`49aec510 ffff9b8f`9353acdf ffff9b8f`9353a9af
fffff803`49aec520 ffff9b8f`980781cf 00000000`00000000
fffff803`49aec530 00000000`00000000 00000000`00000000
fffff803`49aec540 00000000`00000000 00000000`00000000
fffff803`49aec550 00000000`00000000 00000000`00000000
  • This seems complicated, but there is a way to iterate over this structure to enumerate registered process creation callback routines.

1: kd> dx ((void**[0x40])&nt!PspCreateProcessNotifyRoutine)
.Where(a => a != 0)
.Select(a => @$getsym(@$getCallbackRoutine(a).Function))
[0]         : nt!ViCreateProcessCallback (fffff803`4915a2a0)
[1]         : cng!CngCreateProcessNotifyRoutine (fffff803`4a4e6dd0)
[2]         : WdFilter+0x45e00 (fffff803`4ade5e00)
[3]         : ksecdd!KsecCreateProcessNotifyRoutine (fffff803`4a33ba40)
[4]         : tcpip!CreateProcessNotifyRoutineEx (fffff803`4b3f1f90)
[5]         : iorate!IoRateProcessCreateNotify (fffff803`4b95d930)
[6]         : CI!I_PEProcessNotify (fffff803`4a46a270)
[7]         : dxgkrnl!DxgkProcessNotify (fffff803`4c116610)
[8]         : peauth+0x43ce0 (fffff803`4d873ce0)
  • Not everything is a security driver though e.g. tcpip!CreateProcessNotifyRoutineEx.

  • WdFilter+0x45e00 is the driver for microsoft defender.

Collecting Process Creation Details

  • EDRs collect notifications by enumerating the PS_CREATE_NOTIFY_INFO structure which is shown below:

typedef struct _PS_CREATE_NOTIFY_INFO {
    SIZE_T Size;
    union {
         ULONG Flags;
             struct {
                 ULONG FileOpenNameAvailable : 1;
                 ULONG IsSubsystemProcess : 1;
                 ULONG Reserved : 30;
             };
        };
    
    HANDLE ParentProcessId;
    CLIENT_ID CreatingThreadId;
    struct _FILE_OBJECT *FileObject;
    PCUNICODE_STRING ImageFileName;
    PCUNICODE_STRING CommandLine;
    NTSTATUS CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
  • The PS_CREATE_NOTIFY_INFO structure contains a significant amount of valuable data relating to process-creation events on the system including -

    • ParentProcessId - The parent process of the newly created process. This isn’t necessarily the one that created the new process.

    • CreatingThreadId - Handles to the unique thread and process responsible for creating the new process.

    • FileObject - A pointer to the process’s executable file object

    • ImageFileName - A pointer to a string containing the path to the newly created process’s executable file.

    • CommandLine - The command line arguments passed to the creating process.

    • FileOpenNameAvailable - A value that specifies whether the ImageFileName member matches the filename used to open the new process’s executable file.

  • EDRs utilize sysmon to interact with the telemetry. The driver collects these supplemental pieces of information by investigating the context of the thread under which the event was generated and expanding on members of the structure.

  • By leveraging the data collected from this event and the associated structure, EDRs can also create internal mappings of process attributes and relationships in order to detect suspicious activity.

  • This data could also provide the agent with useful context for determining whether other activity is malicious. For example, the agent could process command line arguments into a machine learning model to figure out whether the command’s invocation is malicious.

Thread Notifications

  • Thread notifications are less valuable than process creation notification as they receive less information. Although thread-creation callbacks pass far less data to the callback, they do provide the EDR with another datapoint against which detections can be built.

Registering Thread Callback Routine

  • When a thread is created or terminated, the callback routine receives three pieces of data: the ID of the process to which the thread belongs, the unique thread ID, and a Boolean value indicating whether the thread is being created.

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegPath)
{
        NTSTATUS status = STATUS_SUCCESS;
        --snip--

        status = PsSetCreateThreadNotifyRoutine(ThreadNotifyCallbackRoutine);
        --snip--
}

void ThreadNotifyCallbackRoutine(
        HANDLE hProcess,
        HANDLE hThread,
        BOOLEAN bCreate)
{
        if (bCreate)
        {
                 --snip--
        }
}
  • As with process creation, an EDR can receive notifications about thread creation or termination via its driver by registering a thread-notification callback routine with either nt!PsSetCreateThreadNotifyRoutine() or the nt!PsSetCreateThreadNotifyRoutineEx(), which adds the ability to define the notification type.

  • If the Boolean indicating whether the thread is being created or terminated is TRUE, the driver performs some action defined by the developer. Otherwise, the callback would simply ignore the thread events.

Detecting Remote Thread Creation

  • Remote thread creation occurs when one process creates a thread inside another process. This technique is core to a ton of attacker tradecraft, which often relies on changing the execution context. EDRs detect it by comparing the thread's process id with the current's process ID; as shown below:

void ThreadNotifyCallbackRoutine(
        HANDLE hProcess,
        HANDLE hThread,
        BOOLEAN bCreate)
{
        if (bCreate)
        {
                if (PsGetCurrentProcessId() != hProcess)
                 {
                 --snip--
                 }
        }
}
  • The fact that a thread was created remotely doesn’t automatically make it malicious as remote thread creation happens under a number of legitimate circumstances. One example is child process creation. When a process is created, the first thread executes in the context of the parent process. To account for this, many EDRs simply disregard the first thread associated with a process.

  • Certain internal operating system components also perform legitimate remote thread creation. An example of this is Windows Error Reporting.

Evading Creation Callbacks

Command Line Tampering

  • Some of the most commonly monitored attributes of process-creation events are the command line arguments with which the process was invoked.

  • EDRs can find arguments in the CommandLine member of the structure passed to a process-creation callback routine. When a process is created, its command line arguments are stored in the ProcessParameters field of its process environment block (PEB). This field contains a pointer to an RTL_USER_PROCESS_PARAMETERS structure that contains, among other things, a UNICODE_STRING with the parameters passed to the process at invocation.

0:000> ?? @$peb->ProcessParameters->CommandLine.Buffer
wchar_t * 0x000001be`2f78290a
 "C:\Windows\System32\rundll32.exe ieadvpack.dll,RegisterOCX payload.exe"
  • Adam Chester suggested a method to modify the command line arguments used to invoke a process. First, you create the child process in a suspended state using your malicious arguments. Next, you use ntdll!NtQueryInformationProcess() to get the child process’s PEB address, and you copy it by calling kernel32!ReadProcessMemory(). You retrieve its ProcessParameters field and overwrite the UNICODE_STRING represented by the CommandLine member pointed to by ProcessParameters with spoofed arguments. Lastly, you resume the child process. This is shown below.

void main()
{
        --snip--
        
        if (CreateProcessW(
                 L"C:\\Windows\\System32\\cmd.exe",
                 L"These are my sensitive arguments",
                 NULL, NULL, FALSE,
                 CREATE_SUSPENDED,
                 NULL, NULL, &si, &pi))
         {
         
          --snip--
 
         LPCWSTR szNewArguments = L"Spoofed arguments passed instead";
         SIZE_T ulArgumentLength = wcslen(szNewArguments) * sizeof(WCHAR);
 
         if (WriteProcessMemory(
                  pi.hProcess,
                  pParameters.CommandLine.Buffer,
                  (PVOID)szNewArguments,
                  ulArgumentLength,
                  &ulSize))
                  {
 
                            ResumeThread(pi.hThread);
 
                   }
 
          }

         --snip--
}
  • The best way to get the address of the process parameters in the PEB is to use ntdll!NtQueryInformationProcess(), passing in the ProcessBasicInformation information class. This should return a PROCESS_BASIC_INFORMATION structure that contains a PebBaseAddress member.

  • We can then read our child process’s PEB into a buffer that we allocate locally. Using this buffer, we extract the parameters and pass in the address of the PEB. Then we use ProcessParameters to copy it into another local buffer.

  • We overwrite the existing parameters with a new string via a call to kernel32!WriteProcessMemory(). Assuming that this all completed without error, we call kernel32!ResumeThread() to allow our suspended child process to finish initialization and begin executing.

  • While this technique remains one of the more effective ways to evade detection based on suspicious command line arguments, it has a handful of limitations. One such limitation is that a process can’t change its own command line arguments. This means that if we don’t have control of the parent process, as in the case of an initial access payload, the process must execute with the original arguments.

  • Additionally, the value used to overwrite the suspicious arguments in the PEB must be longer than the original value. If it is shorter, the overwrite will be incomplete, and portions of the suspicious arguments will remain.

Parent Process ID Spoofing

  • Nearly every EDR has some way of correlating parent–child processes on the system. Thus, in order to hide malicious behavior on the host, attackers often wish to spoof their current process’s parent. If we can trick an EDR into believing that our malicious process creation is actually normal, we’re substantially less likely to be detected.

  • The most common way to accomplish this is by modifying the child’s process and thread attribute list. This evasion relies on the fact that, on Windows, children inherit certain attributes from parent processes, such as the current directory and env variables and no dependencies exist between them. So we can just simply change them to spoof the PPID.

  • The primary API used for this purpose is the aptly named kernel32!CreateProcess() API.

// kernel32!CreateProcess() definition
BOOL CreateProcessW(
    LPCWSTR               lpApplicationName,
    LPWSTR                lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL                  bInheritHandles,
    DWORD                 dwCreationFlags,
    LPVOID                lpEnvironment,
    LPCWSTR               lpCurrentDirectory,
    LPSTARTUPINFOW        lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation
);
  • The ninth parameter passed to this function is a pointer to a STARTUPINFO or STARTUPINFOEX structure. The STARTUPINFOEX structure extends the basic startup information structure by adding a pointer to a PROC_THREAD_ATTRIBUTE_LIST structure.

typedef struct _STARTUPINFOEXA {
        STARTUPINFOA                 StartupInfo;
        LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;
  • When creating our process, we make a call to kernel32!InitializeProcThreadAttributeList() to initialize the attribute list and then call to kernel32!UpdateProcThreadAttribute() to modify it.

  • This allows us to set custom values to the attributes (For example PROC_THREAD_ ATTRIBUTE_PARENT_PROCESS) of the child process as shown below:

Void SpoofParent() {
    PCHAR szChildProcess = "notepad";
    DWORD dwParentProcessId = 1 7648;
    HANDLE hParentProcess = NULL;
    STARTUPINFOEXA si;
    PROCESS_INFORMATION pi;
    SIZE_T ulSize;

    memset(&si, 0, sizeof(STARTUPINFOEXA));
    si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

    hParentProcess = OpenProcess(
         PROCESS_CREATE_PROCESS,
         FALSE,
         dwParentProcessId);
         
    InitializeProcThreadAttributeList(NULL, 1, 0, &ulSize);
    si.lpAttributeList =
         (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
         GetProcessHeap(),
         0, ulSize);
    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &ulSize);

    UpdateProcThreadAttribute(
         si.lpAttributeList,
         0,
         PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
         &hParentProcess,
         sizeof(HANDLE),
         NULL, NULL);
    
    CreateProcessA(NULL,
         szChildProcess,
         NULL, NULL, FALSE,
         EXTENDED_STARTUPINFO_PRESENT,
         NULL, NULL,
         &si.StartupInfo, &pi);
    
    CloseHandle(hParentProcess);
    DeleteProcThreadAttributeList(si.lpAttributeList);
}
  • This evasion technique is extremely easy to detect using the EDR driver. The structure passed to the driver on a process-creation event contains two separate fields related to parent processes: ParentProcessId and CreatingThreadId.

  • While these two fields will point to the same process in most normal circumstances, when the parent process ID (PPID) of a new process is spoofed, the CreatingThreadId.UniqueProcess field will contain the PID of the process that made the call to the process-creation function. This is shown in the DbgView output below.

12.67045498 Process Name: notepad.exe
12.67045593 Process ID: 7892
12.67045593 Parent Process Name: vmtoolsd.exe
12.67045593 Parent Process ID: 7028
12.67045689 Creator Process Name: ppid-spoof.exe
12.67045784 Creator Process ID: 7708
  • You can see here that the spoofed vmtoolsd.exe shows up as the parent process, but the creator (the true process that launched notepad.exe) is identified as ppid-spoof.exe.

  • Another approach to detecting PPID spoofing uses ETW trace data to capture process-creation events on the host whenever notepad.exe is spawned.

Process Image Modification

  • In addition to hiding the execution of the malware or tooling, process image modification could allow attackers to bypass application whitelisting, evade per-application host firewall rules, or pass security checks against the calling image before a server allows a sensitive operation to occur.

  • Process creation on Windows involves a complex set of steps, many of which occur before the kernel notifies any drivers. As a result, attackers have an opportunity to modify the process’s attributes in some way during those early steps.

  • Here is the entire process-creation workflow:

    1. Validate parameters passed to the process-creation API.

    2. Open a handle to the target image.

    3. Create a section object from the target image.

    4. Create and initialize a process object.

    5. Allocate the PEB.

    6. Create and initialize the thread object.

    7. Send the process-creation notification to the registered callbacks.

    8. Perform Windows subsystem-specific operations to finish initialization.

    9. Start execution of the primary thread.

    10. Finalize process initialization.

    11. Start execution at the image entry point.

    12. Return to the caller of the process-creation API.

  • Step 3 is one of the most important steps as it can be exploited to run our malicious payload in another benign process. he kernel creates a section object from the process image. The memory manager caches this image section once it is created, meaning that the section can deviate from the corresponding target image.

  • Thus, when the driver receives its notification from the kernel process manager, the FileObject member of the PS_CREATE_NOTIFY_INFO structure it processes may not point to the file truly being executed.

Hollowing

  • Using this technique, the attacker creates a process in a suspended state, then unmaps its image after locating its base address in the PEB. Once the unmapping is complete, the attacker maps a new image, such as the adversary’s shellcode runner, to the process and aligns its section. If this succeeds, the process resumes execution.

Process hollowing

Doppelgänging

  • Process doppelgänging relies on two Windows features: Transactional NTFS (TxF) and the legacy process-creation API, ntdll!NtCreateProcessEx().

  • TxF is a now-deprecated method for performing filesystem actions as a single atomic operation. It allows code to easily roll back file changes, such as during an update or in the event of an error, and has its own group of supporting APIs.

  • ntdll!NtCreateProcessEx() has the notable benefit of taking a section handle rather than a file for the process image but comes with some significant challenges which required developers to re-create steps such as writing process parameters to the new process’s address space and creating the main thread object.

Process doppelgänging

Herpaderping

  • Process herpaderping leverages many of the same tricks as process doppelgänging. It can evade a driver’s image-based detections but its primary aim is to evade detection of the contents of the dropped executable.

  • To perform herpaderping, an attacker first writes the malicious code to be executed to disk and creates the section object, leaving the handle to the dropped executable open. They then call the legacy process-creation API, with the section handle as a parameter, to create the process object.

  • Before initializing the process, they obscure the original executable dropped to disk using the open file handle and kernel32!WriteFile() or a similar API. Finally, they create the main thread object and perform the remaining process spin-up tasks.

  • This results in the EDR driver receiving bogus data while scanning the file’s contents using the FileObject member of the structure passed to the driver on process creation.

Process herpaderping

Ghosting

  • Process ghosting relies on the fact that Windows only prevents the deletion of files after they’re mapped into an image section and doesn’t check whether an associated section actually exists during the deletion process.

  • If a user attempts to open the mapped executable to modify or delete it, Windows will return an error. If the developer marks the file for deletion and then creates the image section from the executable, the file will be deleted when the file handle is closed, but the section object will persist.

Process Ghosting
  • When the driver receives a notification about the process creation and attempts to access the FILE_OBJECT backing the process (the structure used by Windows to represent a file object), it will receive a STATUS_FILE_DELETED error, preventing the file from being inspected.

Detection

  • We can detect all of these due to the technique’s reliance on two things: the creation of an image section that differs from the reported executable, whether it is modified or missing, and the use of the legacy process-creation API to create a new, non-minimal process from the image section.

  • Unfortunately, most of the detections for this tactic are reactive, occurring only as part of an investigation, or they leverage proprietary tooling.

  • First, in kernel mode, the driver could search for information related to the process’s image either in the PEB or in the corresponding EPROCESS structure, the structure that represents a process object in the kernel. Because the user can control the PEB, the process structure is a better source.

  • Drivers may query these paths by using APIs such as nt!SeLocateProcess ImageName() or nt!ZwQueryInformationProcess() to retrieve the true image path, at which point they still need a way to determine whether the process has been tampered with.

  • For example, the WinDbg output of a tampered process shows an empty string. It should be a full filepath for an untampered process.

0: kd> dt nt!_EPROCESS SeAuditProcessCreationInfo @$proc
+0x5c0 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO
0: kd> dt (nt!_OBJECT_NAME_INFORMATION *) @$proc+0x5c0
0xffff9b8f`96880270
+0x000 Name             : _UNICODE_STRING ""
  • Further checking another attribute:

0: kd> dt nt!_EPROCESS ImageFileName @$proc
+0x5a8 ImageFileName : [15] "THFA8.tmp"
  • The ".tmp" extension is suspicious. Upon further investigation:

1: kd> dt nt!_PEB ProcessParameters @$peb
+0x020 ProcessParameters : 0x000001c1`c9a71b80 _RTL_USER_PROCESS_PARAMETERS
1: kd> dt nt!_RTL_USER_PROCESS_PARAMETERS ImagePathName poi(@$peb+0x20)
+0x060 ImagePathName : _UNICODE_STRING "C:\WINDOWS\system32\notepad.exe"

1: kd> !peb
PEB at 0000002d609b9000
InheritedAddressSpace:               No
ReadImageFileExecOptions:            No
BeingDebugged:                       No
ImageBaseAddress:                    00007ff60edc0000
NtGlobalFlag:                        0
NtGlobalFlag2:                       0
Ldr                                  00007ffc74c1a4c0
Ldr.Initialized:                     Yes
Ldr.InInitializationOrderModuleList: 000001c1c9a72390 . 000001c1c9aa7f50
Ldr.InLoadOrderModuleList:           000001c1c9a72500 . 000001c1c9aa8520
Ldr.InMemoryOrderModuleList:         000001c1c9a72510 . 000001c1c9aa8530
        Base Module
 1 7ff60edc0000 C:\WINDOWS\system32\notepad.exe
  • The first image in memory is notepad.exe which indicates that image tampering of some kind has taken place. But this information isn't enough to determine what technique is being used. For that, additional information is required.

  • To get that information, the EDR might first try to investigate the file directly, such as by scanning its contents through the pointer stored in the process structure’s ImageFilePointer field. If malware created the process by passing an image section object through the legacy process-creation API, as in the proof of concept, this member will be empty.

  • The use of the legacy API to create a process from a section is a major indicator that something weird is going on. At this point, the EDR can reasonably say that this is what happened.

  • Another place to look for anomalies is the virtual address descriptor (VAD) tree used for tracking a process’s contiguous virtual memory allocations. The VAD tree can provide very useful information about loaded modules and the permissions of memory allocations. The root of this tree is stored in the VadRoot member of the process structure.

  • To detect process-image modifications, you’ll probably want to look at the mapped allocation types, which include READONLY file mappings, such as COM+ catalog files and EXECUTE_WRITECOPY executable files. In the VAD tree, you’ll commonly see the Win32-rooted path for the process image.

;; unmodified

0: kd> !vad
VAD Commit
ffffa207d5c88d00 7 Mapped NO_ACCESS Pagefile section, shared commit 0x1293
ffffa207d5c89340 6 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\notepad.exe
ffffa207dc976c90 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\oleacc.dll

;; modified

0: kd> !vad
VAD Commit
ffffa207d5c96860 2 Mapped NO_ACCESS Pagefile section, shared commit 0x1293
ffffa207d5c967c0 6 Mapped Exe EXECUTE_WRITECOPY \Users\dev\AppData\Local\Temp\THF53.tmp
ffffa207d5c95a00 9 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\gdi32full.dll
  • Now that we know the path to the image of interest, we can investigate it further. One way to do this is to use the ntdll!NtQueryInformationFile() API with the FileStandardInformation class, which will return a FILE_STANDARD_ INFORMATION structure.

  • This structure contains the DeletePending field, which is a Boolean indicating whether the file has been marked for deletion. Under normal circumstances, you could also pull this information from the DeletePending member of the FILE_OBJECT structure. Inside the EPROCESS structure for the relevant process, this is pointed to by the ImageFilePointer member. In the case of the ghosted process, this pointer will be null, so the EDR can’t use it.

  • Thus the EDR can use ImageFileName, FileObject, and IsSubsystemProcess to identify potentially ghosted processes. After observing the difference between a normal instance of notepad.exe and one that has been ghosted, we’ve identified a few indicators:

    • There will be a mismatch between the paths in the ImagePathName inside the ProcessParameters member of the process’s PEB and the ImageFileName in its EPROCESS structure.

    • The process structure’s image file pointer will be null and its Minimal and PicoCreated fields will be false.

    • The filename may be atypical (this isn’t a requirement).

  • Below is an example of how the driver logic could look like:

void ProcessCreationNotificationCallback(
   PEPROCESS pProcess,
   HANDLE hPid,
   PPS_CREATE_NOTIFY_INFO psNotifyInfo)
   {
     if (pNotifyInfo)
       {
          if (!pNotifyInfo->FileObject && !pNotifyInfo->IsSubsystemProcess)
             {
               PUNICODE_STRING pPebImage = NULL;
               PUNICODE_STRING pPebImageNtPath = NULL;
               PUNICODE_STRING pProcessImageNtPath = NULL;
                      
               GetPebImagePath(pProcess, pPebImage);
               CovertPathToNt(pPebImage, pPebImageNtPath);
                      
               CovertPathToNt(psNotifyInfo->ImageFileName, pProcessImageNtPath);
 
               if (RtlCompareUnicodeString(pPebImageNtPath, pProcessImageNtPath, TRUE))
                   {
                        --snip--
                   }
             }
       }
      --snip--
}

Last updated