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 in 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

Using the COM interface of the Task Scheduler, the code listed below registers a scheduled task that runs notepad.exe every day at 3pm.

To call the fucntion, see the details in the first few lines of the code.

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;
}

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/

tags: #persistence