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 keyHKLM\System\CurrentControlSet\Services\sacdrv\DisableCmdSessions
. - Allocate memory to store serial port buffer.
- Assign the
sacsvr
service Start registry value inImposeSacCmdServiceStartTypePolicy
(Registry key\Registry\Machine\System\CurrentControlSet\Services\sacsvr\Start
toRegistryValueBuffer
. - Initialize channel manager (
ChanMgrInitialize
). Note: We will go into more details of channel later. - It creates an event
\SACEvent
usingIoCreateSynchronizationEvent()
. more on events object here. - Initialize machine information in
InitializeMachineInformation()
. The machine information are extracted from following registry keys and apis:
RtlGetVersion()
RtlGetProductInfo
HKLM\System\Setup\SystemSetupInProgress
HKLM\System\CurrentControlSet\Control\ComputerName\ComputerName
HKLM\System\CurrentControlSet\Control\Session Manager\Environment\PROCESSOR_ARCHITECTURE
HKLM\System\CurrentControlSet\Control\MiniNT
- If event creation succeed, it will register a callback named
\Callback\Phase1InitComplete
usingExRegisterCallback
. 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 usingZwAssignProcessToJobObject
. - 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 callsSacIppEnumerateAddresses
and thenRtlIpv6AddressToStringW
. - Convert string to ip using
RetrieveIpAddressFromString
- Calls
SacIpSetNetInfo
that usesNsiSetAllParameters
.
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
andsacsess
usingCreatePipe
. - Pass the pipes handle to
CreatePseudoConsoleAsUser
to create pseudo console. You can read more about pseudo console here. - Start the
cmd.exe
usingCreateProcessAsUserW
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.