Persistence 101: Looking at the Scheduled Tasks

This post discusses scheduled tasks on Windows as a mechanism for persistence. It 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 set 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 set 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 set 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"

To set up a scheduled task for a user account on a remote host, we can set the hostname and user credentials. 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 configuration file in the form of XML is created in the following directory:

C:\Windows\System32\Tasks

A scheduled task can also be created using an XML configuration file. The scheduled task configuration must be imported to the scheduled task manager first. Presuming the configuration of the scheduled task exists within the file config.xml, the scheduled task can then be imported using schtasks.exe with the following command line:

schtasks /create /xml “config.xml” /tn “task name”

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 triggers 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 additional required legwork to configure the COM server, 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]. Implementation of a COM Handler is detailed in [13]. Source code is available in C++ [14] and C# [15].

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.

Bonus: Hunting for Scheduled Task operational anomalies

Scheduled tasks - as many other system resources - generate a handful of event logs. These logs, if used properly, may decipher unexpected host activity or create leads for further investigation. Event logs for scheduled tasks are located in the following two areas:

The following event IDs, are indicative of scheduled task malfunctions:

A scheduled task may fail for various reasons, ranging from faulty software to malware. Although a scheduled task may have failed, this does not necessarily indicate no source code has run at all. For example, certain types of scheduled tasks such as COM handlers, must implement specific interfaces [8] with which the Task Scheduler then controls the scheduled task. If these interfaces are missing or are implemented incorrectly and ultimately fail, the Task Scheduler may not be able to control a task and as a result report back issues in the event log.

Let’s assume that a malware author may not has gone the extra mile to create a pristine, bug-free COM handler that runs with no error messages. Same author only does the bare minimum to just run malicious source code with no consideration of any error messages the Task Scheduler will generate. This creates a threat hunting opportunity which can then begin by reviewing the task scheduler event log and investigate any COM handlers that generate error logs.

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

[15] https://github.com/dahall/ITaskHandlerTemplate


tags: #persistence