Peeling Back the Layers: Understanding Windows components Architecture through SAC/EMS Reversing

In this article, we will be exploring the inner workings of how SAC operates! This will also give you a better understanding of the low-level components of Windows implementation details, and what you can look forward to after reversing them.

14 min read
Peeling Back the Layers: Understanding Windows components Architecture through SAC/EMS Reversing

Windows have a cool feature EMS (Emergency management support) and SAC (Special Administrative Console) that can be highly useful in windows servers and cloud environment. You can read more about EMS/SAC and discover their functionalities, feel free to explore my previous article https://nixhacker.com/setting-up-and-playing-with-ems-sac/.

In this article, we will be exploring the inner workings of how SAC operates! This will also give you a better understanding of the low-level components of Windows implementation details, and what you can look forward to after reversing them.

The EMS can be reached through Serial COM port. All the low level work of EMS  is handled by SAC service in windows. First level of interaction with serial port occurs within the kernel driver SACSRV.sys  which is responsible for processing the data recieved from serial port and handling major tasks. In instances where the EMS feature is enabled by default, this driver is initialized during the system's boot process. You will find the registry key associated with the driver service at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\sacdrv .

The operational tasks on the user side are managed by a Windows service designated as sacsvr, which incorporates its service-specific functionality within the sacsvr.dll file. You will find the registry key associated with the service at same registry location as above.

Another user side component integral to the EMS that facilitates managing command execution session is sacsess.exe known as SAC service helper binary.


SAC infrastructure provide following functionalities to the user:

  • Command console
  • Restart
  • Shutdown
  • crashdump
  • Killing process
  • Process priority modification
  • Limit memory
  • Set time
  • Machine information
  • Set IP address
  • Live dump
  • Process dump

Each functionality discussed above is handled by different components  of SAC. The flow of responsibility transfer and handling (which component has which functionality implemented) looks like below. :

The majority of functionality of SAC is implemented in sacdrv.sys. Requests associated for the Command console and Process dump are directed towards the user-side service sacsvr. Within the sacsvr , the functionality pertaining to process dumping is effectively implemented; however, in the case of command console requests, sacsvr initiates the child process sacess, which facilitates all interactions with the command shell. Subsequently, this service launches cmd.exe, which serves as a pseudo console for the execution of commands.

Diving into SAC driver (sacdrv.sys )

On driver entry, SAC creates a device object at \Device\SAC and initialize global data(InitializeGlobalData) and device data(InitializeDeviceData). During global data initialization the driver perform following steps

  • Creates symbolic link of \Device\SAC to \DosDevices\SAC
  • Setting flag CommandConsoleLaunchingEnabled based on following registry key HKLM\System\CurrentControlSet\Services\sacdrv\DisableCmdSessions.
  • Allocate memory to store serial port buffer.
  • Assign the sacsvr service Start registry value in ImposeSacCmdServiceStartTypePolicy (Registry key \Registry\Machine\System\CurrentControlSet\Services\sacsvr\Start to RegistryValueBuffer.
  • Initialize channel manager ( ChanMgrInitialize). Note: We will go into more details of channel later.
  • It creates an event \SACEvent using IoCreateSynchronizationEvent(). more on events object here.
  • Initialize machine information in InitializeMachineInformation().  The machine information are extracted from following registry keys and apis:
  1. RtlGetVersion()
  2. RtlGetProductInfo
  3. HKLM\System\Setup\SystemSetupInProgress
  4. HKLM\System\CurrentControlSet\Control\ComputerName\ComputerName
  5. HKLM\System\CurrentControlSet\Control\Session Manager\Environment\PROCESSOR_ARCHITECTURE
  6. HKLM\System\CurrentControlSet\Control\MiniNT
  • If event creation succeed, it will register a callback named \Callback\Phase1InitComplete using ExRegisterCallback. This callback can be accessed by other kernel drivers.

InitializeDeviceData is mainly responsible to start a system thread using PsCreateSystemThread that will run in background and is responsible for consuming buffer from serial port ( ConMgrSerialPortConsumer) as well as connection manager initialization ( ConMgrInitialize).

ConMgrIntialize- Responsible to Create a new channel( ChanMgrCreateChannel), setting it's flag (like channel id) and getting it's handle( ChanMgrGetByHandle ).

Understanding SAC Channels

For each connection request on serial port, SAC creates a new channel (of size 0x3f0).

Channel is essentially a structure defined in heap memory to manager each connection. The structure attributes are mentioned below (from React OS project code):

LONG 	Index
 
SAC_CHANNEL_ID 	ChannelId
 
HANDLE 	CloseEvent
 
PVOID 	CloseEventObjectBody
 
PKEVENT 	CloseEventWaitObjectBody
 
HANDLE 	HasNewDataEvent
 
PVOID 	HasNewDataEventObjectBody
 
PKEVENT 	HasNewDataEventWaitObjectBody
 
HANDLE 	LockEvent
 
PVOID 	LockEventObjectBody
 
PKEVENT 	LockEventWaitObjectBody
 
HANDLE 	RedrawEvent
 
PVOID 	RedrawEventObjectBody
 
PKEVENT 	RedrawEventWaitObjectBody
 
PFILE_OBJECT 	FileObject
 
SAC_CHANNEL_TYPE 	ChannelType
 
SAC_CHANNEL_STATUS 	ChannelStatus
 
WCHAR 	NameBuffer [SAC_CHANNEL_NAME_SIZE+1]
 
WCHAR 	DescriptionBuffer [SAC_CHANNEL_DESCRIPTION_SIZE+1]
 
ULONG 	Flags
 
GUID 	ApplicationType
 
LONG 	WriteEnabled
 
ULONG 	IBufferIndex
 
PCHAR 	IBuffer
 
LONG 	ChannelHasNewIBufferData
 
UCHAR 	CursorRow
 
UCHAR 	CursorCol
 
UCHAR 	CellForeColor
 
UCHAR 	CellBackColor
 
UCHAR 	CellFlags
 
PCHAR 	OBuffer
 
ULONG 	OBufferIndex
 
ULONG 	OBufferFirstGoodIndex
 
LONG 	ChannelHasNewOBufferData
 
PSAC_CHANNEL_CREATE 	ChannelCreate
 
PSAC_CHANNEL_DESTROY 	ChannelDestroy
 
PSAC_CHANNEL_OFLUSH 	ChannelOutputFlush
 
PSAC_CHANNEL_OECHO 	ChannelOutputEcho
 
PSAC_CHANNEL_OWRITE 	ChannelOutputWrite
 
PSAC_CHANNEL_OREAD 	ChannelOutputRead
 
PSAC_CHANNEL_IWRITE 	ChannelInputWrite
 
PSAC_CHANNEL_IREAD 	ChannelInputRead
 
PSAC_CHANNEL_IREAD_LAST 	ChannelInputReadLast
 
PSAC_CHANNEL_IBUFFER_FULL 	ChannelInputBufferIsFull
 
PSAC_CHANNEL_IBUFFER_LENGTH 	ChannelInputBufferLength
 
SAC_CHANNEL_LOCK 	ChannelAttributeLock
 
SAC_CHANNEL_LOCK 	ChannelOBufferLock
 
SAC_CHANNEL_LOCK 	ChannelIBufferLock

Each channel once created get stored in ChannelArray which is of size 0x10. This further implies that a maximum of 10 concurrent connections may operate simultaneously.

Code responsible for channel creation is present at ChanMgrCreateChannel. Following function first removes any zombie channel (inactive channels) followed by an assessment of whether the specified channel name is already present within the array. In the absence of such a name, a universally unique identifier (UUID) is generated utilizing the ExUuidCreate function, subsequently leading to the instantiation of the channel through the ChannelCreate function.The ChannelCreate function is tasked with initializing both the name and descriptor of the channel via ChannelSetDescription, in addition to incorporating associated events and locks, ultimately designating the channel as active through the ChannelSetStatus function.This encapsulates the essential information pertaining to channels within the System Administration Console (SAC). Next, let us examine the subsequent occurrences following the completion of all initialization procedures.

The aforementioned thread (which initiates its operation through PsCreateSystemThread) continuously monitors for any input emerging from the serial port. Once the input is passed through serial pipeline,  it directed toConMgrSerialPortConsumer in SAC driver. ConMgrSerialPortConsumer  evaluates the integrity of the communication channel and performs several validity assessments on the input (which is present in the InputBuffer) before invoking ConMgrProcessInputLine, provided the input conforms to the anticipated format and ASCII sequence.

ConMgrProcessInputLine task is to process input and call the relevant functions based on input send by consumer. Each  SAC feature has it's own standalone implementation present in functions starting with Do* like DoKillCommand. The pseudocode of ConMgrProcessInputLine looks like below(source ReactOS):

 if (!strncmp(InputBuffer, "t", 1))
    {
        DoTlistCommand();
    }
    else if (!strncmp(InputBuffer, "?", 1))
    {
        DoHelpCommand();
    }
    else if (!strncmp(InputBuffer, "help", 4))
    {
        DoHelpCommand();
    }
    else if (!strncmp(InputBuffer, "f", 1))
    {
        DoFullInfoCommand();
    }
    else if (!strncmp(InputBuffer, "p", 1))
    {
        DoPagingCommand();
    }
    else if (!strncmp(InputBuffer, "id", 2))
    {
        DoMachineInformationCommand();
    }
    else if (!strncmp(InputBuffer, "crashdump", 9))
    {
        DoCrashCommand();
    }
    else if (!strncmp(InputBuffer, "lock", 4))
    {
        DoLockCommand();
    }
    else if (!strncmp(InputBuffer, "shutdown", 8))
    {
        ExecutePostConsumerCommand = Shutdown;
    }
    else if (!strncmp(InputBuffer, "restart", 7))
    {
        ExecutePostConsumerCommand = Restart;
    }
    else if (!strncmp(InputBuffer, "d", 1))
    {
        EnablePaging = GlobalPagingNeeded;
        Status = HeadlessDispatch(HeadlessCmdDisplayLog,
                                  &EnablePaging,
                                  sizeof(EnablePaging),
                                  NULL,
                                  NULL);
        if (!NT_SUCCESS(Status)) SAC_DBG(SAC_DBG_INIT, "SAC Display Log failed.\n");
    }
    else if (!strncmp(InputBuffer, "cmd", 3))
    {
        if (CommandConsoleLaunchingEnabled)
        {
            DoCmdCommand(InputBuffer);
        }
        else
        {
            SacPutSimpleMessage(148);
        }
    }
    else if (!(strncmp(InputBuffer, "ch", 2)) &&
             (((strlen(InputBuffer) > 1) && (InputBuffer[2] == ' ')) ||
              (strlen(InputBuffer) == 2)))
    {
        DoChannelCommand(InputBuffer);
    }
    else if (!(strncmp(InputBuffer, "k", 1)) &&
             (((strlen(InputBuffer) > 1) && (InputBuffer[1] == ' ')) ||
              (strlen(InputBuffer) == 1)))
    {
        DoKillCommand(InputBuffer);
    }
    else if (!(strncmp(InputBuffer, "l", 1)) &&
             (((strlen(InputBuffer) > 1) && (InputBuffer[1] == ' ')) ||
              (strlen(InputBuffer) == 1)))
    {
        DoLowerPriorityCommand(InputBuffer);
    }
    else if (!(strncmp(InputBuffer, "r", 1)) &&
             (((strlen(InputBuffer) > 1) && (InputBuffer[1] == ' ')) ||
              (strlen(InputBuffer) == 1)))
    {
        DoRaisePriorityCommand(InputBuffer);
    }
    else if (!(strncmp(InputBuffer, "m", 1)) &&
             (((strlen(InputBuffer) > 1) && (InputBuffer[1] == ' ')) ||
              (strlen(InputBuffer) == 1)))
    {
        DoLimitMemoryCommand(InputBuffer);
    }
    else if (!(strncmp(InputBuffer, "s", 1)) &&
             (((strlen(InputBuffer) > 1) && (InputBuffer[1] == ' ')) ||
              (strlen(InputBuffer) == 1)))
    {
        DoSetTimeCommand(InputBuffer);
    }
    else if (!(strncmp(InputBuffer, "i", 1)) &&
             (((strlen(InputBuffer) > 1) && (InputBuffer[1] == ' ')) ||
              (strlen(InputBuffer) == 1)))
    {
        DoSetIpAddressCommand(InputBuffer);
    }
    else if ((InputBuffer[0] != '\n') && (InputBuffer[0] != ANSI_NULL))
    {
        SacPutSimpleMessage(SAC_UNKNOWN_COMMAND);
    }
}

Let's dive into each feature calls on how they are implemented:

DoTlistCommand - List down the running process in the machine.

The command first creates a global buffer ( GlobalBuffer) to store the process list. Then call GetTListInfo which get the list of running process. GetTListInfo first check some system information mentioned below:

  • SystemTimeOfDayInformation
  • SystemBasicInformation
  • SystemPageFileInformation
  • SystemFileCacheInformation
  • SystemPerformanceInformation
  • SystemProcessInformation

If all the infos are retrieved successfully, it traverse the ProcessInfo structure to get the list of all running process. The pseudo code looks something like this below:

while (TRUE)
    {
        /* Does the process have a name? */
        if (ProcessInfo->ImageName.Buffer)
        {
            /* Is the process name too big to fit? */
            if ((LONG)BufferLength < ProcessInfo->ImageName.Length)
            {
                /* Bail out */
                SAC_DBG(SAC_DBG_ENTRY_EXIT, "Exiting, error(7).\n");
                return STATUS_INFO_LENGTH_MISMATCH;
            }
 
            /* Copy the name into our own buffer */
            RtlCopyMemory((PVOID)P,
                          ProcessInfo->ImageName.Buffer,
                          ProcessInfo->ImageName.Length);
            ProcessInfo->ImageName.Buffer = (PWCHAR)P;
 
            /* Update buffer lengths and offset */
            BufferLength -= ProcessInfo->ImageName.Length;
            P += ProcessInfo->ImageName.Length;
 
            /* Are we out of memory? */
            if ((LONG)BufferLength < 0)
            {
                /* Bail out */
                SAC_DBG(SAC_DBG_ENTRY_EXIT, "Exiting, no memory(8).\n");
                return STATUS_NO_MEMORY;
            }
        }
 
        

After this PrintTListInfo is called to print the retrieved list with few additional information.

DoCrashCommand: Crash the machine using KeBugCheckEx.

DoPagingCommand: Modify the global paging flag.

DoMachineInformationCommand: Print the machine information (earlier stored during driver initialization).

DoKillCommand : Kill a process. This first check if the process is part of job \BaseNamedObjects\SACX . If so, kill the job using ZwTerminateJobObject otherwise kill the process using ZwTerminateProcess. The pseudo code will look like below:

StringCbPrintfW(
      GlobalBuffer,
      (unsigned int)GlobalBufferSize,
      L"\\BaseNamedObjects\\SAC%d",
      v5,
      ClientId.UniqueProcess,
      ClientId.UniqueThread,
      *(_QWORD *)&DestinationString.Length,
      DestinationString.Buffer,
      *(_QWORD *)&ObjectAttributes.Length,
      ObjectAttributes.RootDirectory,
      ObjectAttributes.ObjectName,
      *(_QWORD *)&ObjectAttributes.Attributes,
      ObjectAttributes.SecurityDescriptor,
      ObjectAttributes.SecurityQualityOfService);
    RtlInitUnicodeString(&DestinationString, GlobalBuffer);
    ObjectAttributes.RootDirectory = 0LL;
    ObjectAttributes.ObjectName = &DestinationString;
    ObjectAttributes.Length = 48;
    ObjectAttributes.Attributes = 576;
    &ObjectAttributes.SecurityDescriptor = 0LL;
    status = ZwOpenJobObject(&JobHandle, 0x2000000u, &ObjectAttributes);
    ObjectAttributes.RootDirectory = 0LL;
    ObjectAttributes.ObjectName = 0LL;
    ClientId.UniqueThread = 0LL;
    ObjectAttributes.Length = 48;
    ObjectAttributes.Attributes = 576;
    &ObjectAttributes.SecurityDescriptor = 0LL;
    nt_status = status;
    ClientId.UniqueProcess = (HANDLE)v5;
    nt_status2 = ZwOpenProcess(&ProcessHandle, 0x2000000u, &ObjectAttributes, &ClientId);
    if ( nt_status2 >= 0 )
    {
      if ( nt_status >= 0 && ZwIsProcessInJob(ProcessHandle, JobHandle) == 292 )
      {
        killed_job = 1;
        killed_process = 0;
        nt_status2 = ZwTerminateJobObject(JobHandle, 1);
        ZwMakeTemporaryObject(JobHandle);
      }
      else
      {
        killed_job = 0;
        killed_process = 1;
        nt_status2 = ZwTerminateProcess(ProcessHandle, 1);
      }
    }

DoLowerPriorityCommand: Lower the process priority.

Open the process using ZwOpenProcess and lower the priority using ZwSetInformationProcess.  

ObjectAttributes.RootDirectory = 0LL;
      ObjectAttributes.ObjectName = 0LL;
      ClientId.UniqueThread = 0LL;
      ClientId.UniqueProcess = (HANDLE)v5;
      ObjectAttributes.Length = 48;
      ObjectAttributes.Attributes = 576;
      *(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0LL;
      v6 = ZwOpenProcess(&ProcessHandle, 0x2000000u, &ObjectAttributes, &ClientId);
      ZwSetInformationProcess(ProcessHandle, ProcessBasePriority, (char *)&v19 + 8, 4u);

DoRaisePriorityCommand: Raise the priority of process. Works similar way as DoLowerPriorityCommand.

DoLimitMemoryCommand: This will limit the maximum memory space used by a process. It is implemented in following way:

  • Open the process using ZwOpenProcess.
  • It creates a job object \BaseNamedObjects\SACx and assign process to that job object using ZwAssignProcessToJobObject.
  • If the job object already exist, check if process already part of the job object using ZwIsProcessInJob.
  • Use ZwSetInformationJobObject to limit the memory usage to that job object.

The pseudo code looks like below:

process_status = ZwOpenProcess(&ProcessHandle, 0x2000000u, (POBJECT_ATTRIBUTES)&ObjectAttributes_8, &ClientId_8);
  if ( process_status >= 0 )
  {
    if ( v12 < 0 )
    {
      *((_QWORD *)&ObjectAttributes_8 + 1) = 0LL;
      *(_QWORD *)&ObjectAttributes_24 = &DestinationString;
      LODWORD(ObjectAttributes_8) = 48;
      DWORD2(ObjectAttributes_24) = 592;
      ObjectAttributes_40 = 0LL;
      object_status = ZwCreateJobObject(&JobHandle, 0x2000000u, (POBJECT_ATTRIBUTES)&ObjectAttributes_8);
      if ( object_status < 0 )
      {
        Message = GetMessage(61LL);
        goto exit()
      }
      assign_jobstatus = ZwAssignProcessToJobObject(JobHandle, ProcessHandle);
      ZwClose(ProcessHandle);
    else if ( ZwIsProcessInJob(ProcessHandle, JobHandle) != 292 )
    {
      v0 = 90LL;
      goto LABEL_53;
    }
  }
  
  LODWORD(JobInformation[2]) |= 0x300u;
  JobInformation[14] = (unsigned int)(v10 << 20);
  JobInformation[15] = JobInformation[14];
  v24 = ZwSetInformationJobObject(JobHandle, JobObjectExtendedLimitInformation, JobInformation, 0x90u);
  

DoSetTimeCommand: Set time of system. Use ZwSetSystemTime to set the system time.

DoSetIpAddressCommand: Set Ip address of machine. Uses following method:

  • Calls SacIpGetNetInfo which later calls SacIppEnumerateAddresses and then  RtlIpv6AddressToStringW.
  • Convert string to ip using RetrieveIpAddressFromString
  • Calls SacIpSetNetInfo that uses NsiSetAllParameters.

DoRebootCommand: Reboot the machine using NtShutdownSystem.

DoLiveDumpCommand: This will create kernel live dump and store it in file C:\Windows\MEMORY.DMP. It uses ZwSystemDebugControl for live dump with following flag SysDbgQueryTraceInformation.

DoCmdCommand: Creates a new command channel. This is depending on usermode service. We will talk more about this in next section.

DoChannelCommand: Provide multiple sub-commands related to channels like listing, info, attaching channel etc. This calls following functions based on parameter passed to the command: DoChannelListCommand, DoChannelCloseByNameCommand, DoChannelSwitchByNameCommand, ConMgrSetCurrentChannel.

SACSVR - Usermode SAC service

Request from DoProcessDumpCommand(used for process dumping) and DoCmdCommand (used to create command execution channel) are sent to user side service sacsvr.

During initialization of SAC driver, function ImposeSacCmdServiceStartTypePolicy verify the Start registry key of \Registry\Machine\System\CurrentControlSet\Services\sacsvr to check the status of sacsvr service. In DoProcessDumpCommand and DoCmdCommand the driver calls InvokeUserModeService which set an event object.

KeWaitForSingleObject(&SACSubcommandMutex, Executive, 0, 0, 0LL);
  memmove(&SACSubcommand, &InputBuffer, v4);
  LODWORD(SACSubcommandLength) = v4;
  KeReleaseMutex(&SACSubcommandMutex, 0);
  Object[0] = RequestSacCmdSuccessEventObject;
  Object[1] = RequestSacCmdFailureEventObject;
  Timeout.QuadPart = -90000000LL;
  KeSetEvent((PRKEVENT)RequestSacCmdEventObject, 1, 1u);
  result = KeWaitForMultipleObjects(2u, Object, WaitAny, Executive, 1, 0, &Timeout, 0LL);

Coming to sacsvr service. The service code is implementated in sacsvr.dll . In the sacsvr.dll, ServiceMain calls  SacSvrServiceRun  which calls CreateProcessDump or CreateClient based on passed argument through buffer to sacsvr.

if ( !strcmp(Str1, "cmd") )
        {
          CreateClient(&v4);
        }
else if ( !strncmp(Str1, "procdump", 8uLL) )
        {
          CreateProcessDump((__int64)Str1);
        }
        

CreateProcessDump: This creates process dump and save it in the filepath passed as argument to command.
Internally it calls CreateMiniDumpThread which creates a new thread with start address of MiniDumpThreadProc. MiniDumpThreadProc will then use MiniDumpWriteDump windows api to get the memory dump of the process by passing following flags MiniDumpIgnoreInaccessibleMemory|MiniDumpWithThreadInfo|MiniDumpWithFullMemoryInfo|MiniDumpWithUnloadedModules|MiniDumpWithHandleData|MiniDumpWithFullMemory.

CreateClient: This is used to invoke command session when user input cmd in SAC.

This will just start sacsess.exe using CreateProcess.

The work of sacsvr service is limited to handling mentioned above two functionalities only.

SAC helper program ( sacsess.exe)

sacsess process is responsible to handle the entirety of the interplay between the command shell and the SAC service. It encompasses various things like session initialization, authentication, and sending the data to and fro to command shell.

How sacess to communicate with the driver?

Beside events, sacess uses undocumented api RtlSubscribeWnfStateChangeNotification and DeviceIoControl.

During the initialization, it subscribes to the following event WNF_EMS_SACSVR_STATE_CHANGE.

The function also invokes CSession::Init, which serves the purpose of initializing the Command session.This function subsequently calls two significant routines: CSession::Authenticate and CShell::StartProcess .Initially, CSession::Authenticate assesses whether credential verification is necessitated ( NeedCredentials) and subsequently proceeds to authenticate the credentials through CSecurityIoHandler::AuthenticateCredentials.The method CSecurityIoHandler::AuthenticateCredentials employs LogonUserExExW to facilitate user login and retrieves the token handle via NtQueryInformationToken.The pseudocode is illustrated as follows:

Using this handle, CShell::StartProcess start the cmd.exe. Following things happen in CShell::StartProcess:

  • Create two named pipe for read and write data between cmd and sacsess using CreatePipe.
  • Pass the pipes handle to CreatePseudoConsoleAsUser to create pseudo console. You can read more about pseudo console here.
  • Start the cmd.exe using CreateProcessAsUserW

The pseudo code will looks like below:

SystemDirectoryW = GetSystemDirectoryW(v5, 0x105u);
  if ( !SystemDirectoryW )
  {
	exit();
  }
  system_dir_addr = SystemDirectoryW + 9;
  cmd_fullpath1 = (wchar_t *)operator new[](saturated_mul(SystemDirectoryW + 9, 2uLL));
  cmd_fullpath = swprintf_s(cmd_fullpath1, system_dir_addr, L"%s\\%s", v5, L"cmd.exe");
  operator delete[](v5);
  if ( cmd_fullpath == -1 )
  {
    v7 = cmd_fullpath1;
    exit();
  }
  if ( !cmd_fullpath1 )
    exit();
  *((_QWORD *)this + 6) = -1LL;
  if ( (unsigned int)CShell::IsLoadProfilesEnabled(v11) && (unsigned int)NeedCredentials() )
  {
    UtilLoadProfile(token_handle, (void **)this + 6);
    env_addr = UtilLoadEnvironment(token_handle, &Environment);
    Environment = (LPVOID)(-(__int64)env_addr & (unsigned __int64)Environment);
  }
  SetConsoleCtrlHandler(0LL, 0);
  NTSTATUS = CreatePipe(&hReadPipe, (PHANDLE)this + 7, 0LL, 0);
  if ( !NTSTATUS )
  {
    exit();
  }
  NTSTATUS = CreatePipe((PHANDLE)this + 8, &hWritePipe, 0LL, 0);
  if ( !NTSTATUS )
  {
    exit()
  }
  v16 = (PVOID *)((char *)this + 8);
  if ( (int)CreatePseudoConsoleAsUser(
              token_handle,
              *((unsigned int *)this + 4),
              hReadPipe,
              hWritePipe,
              0,
              (char *)this + 8) < 0 )
  {
    exit();
  }
  StartupInfo.cb = 112;
  hReadPipe = (void *)-1LL;
  hWritePipe = (void *)-1LL;
  InitializeProcThreadAttributeList(0LL, 1u, 0, &Size);
  v17 = Size;
  ProcessHeap = GetProcessHeap();
  buffer_addr = (struct _PROC_THREAD_ATTRIBUTE_LIST *)HeapAlloc(ProcessHeap, 0, v17);
  lpAttributeList = buffer_addr;
  NTSTATUS = InitializeProcThreadAttributeList(v19, 1u, 0, &Size);
  if ( NTSTATUS )
  {
    NTSTATUS = UpdateProcThreadAttribute(lpAttributeList, 0, 0x20016uLL, *v16, 8uLL, 0LL, 0LL);
    if ( NTSTATUS )
    {
      if ( (unsigned int)NeedCredentials()
        && (unsigned __int8)IsCreateWindowStationWPresent()
        && IsSetUserObjectSecurityPresent() )
      {
        v20 = CreateSACSessionWinStaAndDesktop(token_handle, &hWinSta, (HWINSTA *)this + 9, (HDESK *)this + 3, &v27);
        v4 = v27;
        NTSTATUS = v20;
        if ( !v20 )
          exit();
        StartupInfo.lpDesktop = v27;
      }
      else
      {
        StartupInfo.lpDesktop = L"winsta0\\default";
      }
      NTSTATUS = CreateProcessAsUserW(
                   token_handle,
                   cmd_fullpath1,
                   (LPWSTR)L"cmd.exe",
                   0LL,
                   0LL,
                   1,
                   0x80C00u,
                   Environment,
                   0LL,
                   &StartupInfo,
                   &ProcessInformation);
      if ( NTSTATUS )
      {
        GetExitCodeProcess(ProcessInformation.hProcess, &ExitCode);
        if ( ExitCode != 259 )
          exit();
        hThread = ProcessInformation.hThread;
        *((_QWORD *)this + 5) = ProcessInformation.hProcess;
        if ( hThread != (HANDLE)-1LL )
          CloseHandle(hThread);
        if ( hWinSta )
          SetProcessWindowStation(hWinSta);
        NTSTATUS = 1;
      }
    }
  }

Upon initialization, the sacsess component will persistently await an event from the driver ( CSession::WaitForSacEvent).This process employs WaitForMultipleObjectsEx to obtain notifications regarding state changes that were subscribed to via RtlSubscribeWnfStateChangeNotification.The transmission and reception of data are facilitated through SacChannelRead and SacChannelWrite (which utilizes DeviceIoControl).Following a thorough analysis, I developed functional code for both event subscription and data reading/writing.

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

#define IOCTL_CODE_READ 0x224010
#define IOCTL_CODE_WRITE 0x22800C
#define IOCTL_CODE_QUERYSUBCOMMAND 0x22402C


typedef struct _WNF_TYPE_ID {
    GUID                              TypeId;
} WNF_TYPE_ID, * PWNF_TYPE_ID;

typedef struct _WNF_STATE_NAME {
    ULONG                             Data[2];
} WNF_STATE_NAME, * PWNF_STATE_NAME;

WNF_STATE_NAME WNF_EMS_SACSVR_STATE_CHANGE{ 0xA3BC0875, 0x41950328 };

typedef const WNF_TYPE_ID* PCWNF_TYPE_ID;
typedef ULONG WNF_CHANGE_STAMP, * PWNF_CHANGE_STAMP;

typedef NTSTATUS(NTAPI* RtlPublishWnfStateData_t)(
    _In_ WNF_STATE_NAME StateName,
    _In_opt_ PCWNF_TYPE_ID TypeId,
    _In_reads_bytes_opt_(Length) const VOID* Buffer,
    _In_opt_ ULONG Length,
    _In_opt_ const VOID* ExplicitScope
    );

typedef struct _WNF_CONTEXT_HEADER {
    USHORT NodeTypeCode;
    USHORT NodeByteSize;
} WNF_CONTEXT_HEADER, * PWNF_CONTEXT_HEADER;

typedef struct _WNF_STATE_NAME_INTERNAL {
    ULONG64 Version : 4;
    ULONG64 NameLifetime : 2;
    ULONG64 DataScope : 4;
    ULONG64 PermanentData : 1;
    ULONG64 Unique : 53;
} WNF_STATE_NAME_INTERNAL, * PWNF_STATE_NAME_INTERNAL;

typedef struct _WNF_DELIVERY_DESCRIPTOR {
    ULONG64 SubscriptionId;
    WNF_STATE_NAME StateName;
    WNF_CHANGE_STAMP ChangeStamp;
    ULONG StateDataSize;
    ULONG EventMask;
    WNF_TYPE_ID TypeId;
    ULONG StateDataOffset;
} WNF_DELIVERY_DESCRIPTOR, * PWNF_DELIVERY_DESCRIPTOR;

typedef struct _WNF_NAME_SUBSCRIPTION {
    WNF_CONTEXT_HEADER Header;
    ULONG64 SubscriptionId;
    WNF_STATE_NAME_INTERNAL StateName;
    WNF_CHANGE_STAMP CurrentChangeStamp;
    LIST_ENTRY NamesTableEntry;
    PWNF_TYPE_ID TypeId;
    SRWLOCK SubscriptionLock;
    LIST_ENTRY SubscriptionsListHead;
    ULONG NormalDeliverySubscriptions;
    ULONG NotificationTypeCount[5];
    PWNF_DELIVERY_DESCRIPTOR RetryDescriptor;
    ULONG DeliveryState;
    ULONG64 ReliableRetryTime;
} WNF_NAME_SUBSCRIPTION, * PWNF_NAME_SUBSCRIPTION;

typedef struct _WNF_SERIALIZATION_GROUP {
    WNF_CONTEXT_HEADER Header;
    ULONG GroupId;
    LIST_ENTRY SerializationGroupList;
    ULONG64 SerializationGroupValue;
    ULONG64 SerializationGroupMemberCount;
} WNF_SERIALIZATION_GROUP, * PWNF_SERIALIZATION_GROUP;

typedef NTSTATUS(*PWNF_USER_CALLBACK) (
    WNF_STATE_NAME                    StateName,
    WNF_CHANGE_STAMP                  ChangeStamp,
    PWNF_TYPE_ID                      TypeId,
    PVOID                             CallbackContext,
    PVOID                             Buffer,
    ULONG                             BufferSize);

typedef struct _WNF_USER_SUBSCRIPTION {
    WNF_CONTEXT_HEADER Header;
    LIST_ENTRY SubscriptionsListEntry;
    PWNF_NAME_SUBSCRIPTION NameSubscription;
    PWNF_USER_CALLBACK Callback;
    PVOID CallbackContext;
    ULONG64 SubProcessTag;
    ULONG CurrentChangeStamp;
    ULONG DeliveryOptions;
    ULONG SubscribedEventSet;
    PWNF_SERIALIZATION_GROUP SerializationGroup;
    ULONG UserSubscriptionCount;
    ULONG64 Unknown[10];
} WNF_USER_SUBSCRIPTION, * PWNF_USER_SUBSCRIPTION;



/*
typedef NTSTATUS(NTAPI* RtlSubscribeWnfStateChangeNotification_t)(
    _Outptr_ PVOID* SubscriptionHandle, // PWNF_USER_SUBSCRIPTION
    _In_ WNF_STATE_NAME StateName,
    _In_ WNF_CHANGE_STAMP ChangeStamp,
    _In_ PWNF_USER_CALLBACK Callback,
    _In_opt_ PVOID CallbackContext,
    _In_opt_ PCWNF_TYPE_ID TypeId,
    _In_opt_ ULONG SerializationGroup,
    _Reserved_ ULONG Flags
);*/
typedef NTSTATUS(WINAPI* RtlSubscribeWnfStateChangeNotification_t) (
    _Outptr_ WNF_USER_SUBSCRIPTION* Subscription,
    _In_ WNF_STATE_NAME StateName,
    _In_ WNF_CHANGE_STAMP ChangeStamp,
    _In_ PWNF_USER_CALLBACK Callback,
    _In_opt_ PVOID CallbackContext,
    _In_opt_ PCWNF_TYPE_ID TypeId,
    _In_opt_ ULONG SerializationGroup,
    _In_opt_ ULONG Unknown
    );
FILE* file;

NTSTATUS NTAPI WnfCallback(
    WNF_STATE_NAME StateName,
    WNF_CHANGE_STAMP                  ChangeStamp,
    PWNF_TYPE_ID                      TypeId,
    PVOID                             CallbackContext,
    PVOID                             Buffer,
    ULONG                             BufferSize)
{
    unsigned int v7; // ebx
    struct _WNF_STATE_NAME Source1;
    Source1 = StateName;
    //if (RtlCompareMemory(&Source1, &WNF_EMS_SACSVR_STATE_CHANGE, 8uLL) == 8)
    if (StateName.Data[0] == WNF_EMS_SACSVR_STATE_CHANGE.Data[0] &&
        StateName.Data[1] == WNF_EMS_SACSVR_STATE_CHANGE.Data[1]
        && Buffer)
    {
        v7 = 0;
        fprintf(file, "Callback executed\n");
        if (CallbackContext)
        {
            if (Buffer)
            {
                if (BufferSize == 4)
                {
                    //if (!*(_DWORD*)Buffer)
                    SetEvent(*((HANDLE*)CallbackContext + 5));
                }
                else
                {
                    return (unsigned int)-1073741580;
                }
            }
            else
            {
                return (unsigned int)-1073741581;
            }
        }
        else
        {
            return (unsigned int)-1073741582;
        }
    }
    else
    {
        return (unsigned int)-1073741585;
    }
    return v7;
}


int SACQueryCommand(HANDLE hDevice)
{

    BOOL bResult;
    DWORD bytesReturned;
    //char inputBuffer[100] = "Data to send to driver";
    char outputBuffer[0x4F] = { 0 };
    HMODULE hNtdll = LoadLibraryA("ntdll.dll");
    if (hNtdll == NULL) {
        fprintf(file, "LoadLibrary failed\n", GetLastError());
        return 1;
    }
    printf("We are here RtlPublishWnfStateData\n");
    // Resolve the address of RtlPublishWnfStateData
    RtlPublishWnfStateData_t RtlPublishWnfStateData = (RtlPublishWnfStateData_t)GetProcAddress(hNtdll, "RtlPublishWnfStateData");
    if (RtlPublishWnfStateData == NULL) {
        printf("Failed to resolve RtlPublishWnfStateData\n");
        FreeLibrary(hNtdll);
        return 1;
    }
    int Data = 0;
    //Sleep(100000);
    //RtlPublishWnfStateData(WNF_EMS_SACSVR_STATE_CHANGE, 0LL, &Data, 4LL, 0LL);
        bResult = DeviceIoControl(hDevice,
            IOCTL_CODE_QUERYSUBCOMMAND,
            NULL,
            NULL,
            outputBuffer,
            0x4f,
            &bytesReturned,
            NULL);
        if (!bResult) {
            fprintf(file, "DeviceIoControl failed. Error code: %lu\n", GetLastError());
            CloseHandle(hDevice);
            return 1;
        }
        else {
            fprintf(file, "DeviceIoControl succeeded. Bytes returned: %lu\n", bytesReturned);
            fprintf(file, "Output buffer: %s\n", outputBuffer);
            return 1;
        }

}


int SACRead(HANDLE hDevice)
{

    BOOL bResult;
    DWORD bytesReturned;
    char inputBuffer[0x18] = { 0xFF, 0x59, 0xF5, 0x4A, 0x57, 0x52, 0xEF, 0x11, 0xBD, 0x85, 0x00, 0x15, 0x5D, 0x1D, 0x05, 0x0A };
    char outputBuffer[100] = { 0 };

        bResult = DeviceIoControl(hDevice,
            IOCTL_CODE_READ,
            inputBuffer,
            0x18,
            outputBuffer,
            sizeof(outputBuffer),
            &bytesReturned,
            NULL);
        if (!bResult) {
            fprintf(file, "DeviceIoControl failed. Error code: %lu\n", GetLastError());
            // CloseHandle(hDevice);
            
        }
        else {
            fprintf(file, "DeviceIoControl succeeded. Bytes returned: %lu\n", bytesReturned);
            fprintf(file, "Output buffer: %x\n", outputBuffer);
            //return 1;


    }
    return 1;

}

int SACWrite(HANDLE hDevice)
{

    BOOL bResult;
    DWORD bytesReturned;
    char inputBuffer[0x21] = { 0x9E, 0x41, 0xB5, 0x7A, 0x07, 0x5C, 0xEF, 0x11, 0xBD, 0x8E, 0x00, 0x15, 0x5D, 0x1D, 0x05, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x20, 0x73, 0x64, 0x67, 0x00};
    char outputBuffer[32] = { 0 };

        bResult = DeviceIoControl(hDevice,
            IOCTL_CODE_WRITE,
            inputBuffer,
            sizeof(inputBuffer),
            NULL,
            NULL,
            &bytesReturned,
            NULL);
        if (!bResult) {
            fprintf(file, "DeviceIoControl  SACWrite failed. Error code: %lu\n", GetLastError());
            //CloseHandle(hDevice);
            //return 1;
        }
        else {
            fprintf(file, "DeviceIoControl SACWrite succeeded.\n");
        }

    fprintf(file, "DeviceIoControl succeeded. Bytes returned: %lu\n", bytesReturned);
    fprintf(file, "Output buffer: %s\n", outputBuffer);
}


int SubscribeEvent() {
    HANDLE hDevice;
    NTSTATUS ntStatus;
    WNF_USER_SUBSCRIPTION userSubscription{};
    const char* file_path = "C:\\SAC.log";
    // Replace "\\\\.\\DeviceName" with the actual device name.
    hDevice = CreateFile(L"\\\\.\\SAC",
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    // Open log file for writing
    file = fopen(file_path, "w");
    if (hDevice == INVALID_HANDLE_VALUE) {
        fprintf(file, "Failed to open device. Error code: %lu\n", GetLastError());
        return 1;
    }

    HINSTANCE hNtdll = LoadLibrary(TEXT("NtDll.dll"));
    if (hNtdll)
    {
        RtlSubscribeWnfStateChangeNotification_t RtlSubscribeWnfStateChangeNotification = (RtlSubscribeWnfStateChangeNotification_t)GetProcAddress(hNtdll, "RtlSubscribeWnfStateChangeNotification");
        if (RtlSubscribeWnfStateChangeNotification == NULL) {
            fprintf(file,"Failed to resolve RtlSubscribeWnfStateChangeNotification\n");
            FreeLibrary(hNtdll);
            return 1;
        }
        WNF_USER_SUBSCRIPTION userSubscription{0x288};
        NTSTATUS ntStatus = RtlSubscribeWnfStateChangeNotification(&userSubscription, WNF_EMS_SACSVR_STATE_CHANGE, 0, WnfCallback,
            0,
            0,
            0,
            0);
        if (ntStatus == 0)
        {
            fprintf(file, "Subscribe to notification succesfully\n");
        }
        else {
            fprintf(file, "RtlSubscribeWnfStateChangeNotification error : %08x", ntStatus);
        }
        
        //RtlSubscribeWnfStateChangeNotification pSubscribe = (RtlSubscribeWnfStateChangeNotification)GetProcAddress(hNtdll, "RtlSubscribeWnfStateChangeNotification");
        //RtlQueryWnfStateData pQuery = (RtlQueryWnfStateData)GetProcAddress(hNtdll, "RtlQueryWnfStateData");
    }

    // Close the file
    fclose(file);
    CloseHandle(hDevice);
    Sleep(100000);
    return 0;
}

Beside that, CShell::Read and CShell::Write are used to read and write data to pipe of command shell.

That's all the information releavant to SAC group working. In subsequent articles, we will delve into additional intricacies of the features within the Windows operating system.