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
- Programmatically register a scheduled task using COM (IExecAction)
- Programmatically register a scheduled task using COM (IComHandlerAction)
- Activity traces
- Hiding scheduled tasks
- Bonus: Hunting for malware in Tasks directories
- References
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:
- Task registered - Event ID 106
- Task registration updated - Event ID 140
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:
- Adobe Acrobat Update Task
- GoogleUpdateTaskMachineCore{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}
- GoogleUpdateTaskMachineUA{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}
- MicrosoftEdgeUpdateTaskMachineCore
- MicrosoftEdgeUpdateTaskMachineUA
- OneDrive Standalone Update Task-S-1-5-21-1111111111-1111111111-111111111-500
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