From 057d4e5408ef521f581fbf3e65c2423fa3394955 Mon Sep 17 00:00:00 2001 From: hakril Date: Fri, 1 Jun 2018 12:20:13 +0200 Subject: [PATCH] Add scheduled_task code/doc-source/tests --- .../definitions/functions/accessibility.txt | 6 + .../definitions/functions/ntregistry.txt | 45 +++++++ .../definitions/functions/shlwapi.txt | 10 ++ .../definitions/simple_types.txt | 2 + docs/source/index.rst | 2 +- docs/source/sample.rst | 13 ++ .../scheduled_task_scheduled_task.txt | 32 +++++ docs/source/task_scheduler.rst | 108 ++++++++++++++++ docs/source/{various.rst => winobject.rst} | 3 +- tests/test_tasks.py | 94 ++++++++++++++ windows/winobject/system.py | 15 +++ .../winobject/{tasks.py => task_scheduler.py} | 118 +++++++++++++++--- 12 files changed, 432 insertions(+), 16 deletions(-) create mode 100644 ctypes_generation/definitions/functions/accessibility.txt create mode 100644 ctypes_generation/definitions/functions/ntregistry.txt create mode 100644 ctypes_generation/definitions/functions/shlwapi.txt create mode 100644 docs/source/samples_output/scheduled_task_scheduled_task.txt create mode 100644 docs/source/task_scheduler.rst rename docs/source/{various.rst => winobject.rst} (90%) create mode 100644 tests/test_tasks.py rename windows/winobject/{tasks.py => task_scheduler.py} (70%) diff --git a/ctypes_generation/definitions/functions/accessibility.txt b/ctypes_generation/definitions/functions/accessibility.txt new file mode 100644 index 0000000..9a6f698 --- /dev/null +++ b/ctypes_generation/definitions/functions/accessibility.txt @@ -0,0 +1,6 @@ +HRESULT WINAPI ObjectFromLresult( + _In_ LRESULT lResult, + _In_ REFIID riid, + _In_ WPARAM wParam, + _Out_ PVOID *ppvObject +); diff --git a/ctypes_generation/definitions/functions/ntregistry.txt b/ctypes_generation/definitions/functions/ntregistry.txt new file mode 100644 index 0000000..2ba548b --- /dev/null +++ b/ctypes_generation/definitions/functions/ntregistry.txt @@ -0,0 +1,45 @@ +NTSTATUS WINAPI NtOpenKey( + _Out_ PHANDLE KeyHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes +); + + +NTSTATUS WINAPI NtCreateKey( + _Out_ PHANDLE pKeyHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes, + _In_ ULONG TitleIndex, + _In_ PUNICODE_STRING Class, + _In_ ULONG CreateOptions, + _Out_ PULONG Disposition +); + +NTSTATUS WINAPI NtSetValueKey( + _In_ HANDLE KeyHandle, + _In_ PUNICODE_STRING ValueName, + _In_ ULONG TitleIndex, + _In_ ULONG Type, + _In_ PVOID Data, + _In_ ULONG DataSize +); + + +NTSTATUS NtQueryValueKey( + HANDLE KeyHandle, + PUNICODE_STRING ValueName, + KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass, + PVOID KeyValueInformation, + ULONG Length, + PULONG ResultLength +); + + +NTSTATUS NtEnumerateValueKey( + HANDLE KeyHandle, + ULONG Index, + KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass, + PVOID KeyValueInformation, + ULONG Length, + PULONG ResultLength +); \ No newline at end of file diff --git a/ctypes_generation/definitions/functions/shlwapi.txt b/ctypes_generation/definitions/functions/shlwapi.txt new file mode 100644 index 0000000..3455c60 --- /dev/null +++ b/ctypes_generation/definitions/functions/shlwapi.txt @@ -0,0 +1,10 @@ +PWSTR WINAPI StrStrIW( + _In_ PWSTR pszFirst, + _In_ PWSTR pszSrch +); + +PCSTR WINAPI StrStrIA( + _In_ PCSTR pszFirst, + _In_ PCSTR pszSrch +); + diff --git a/ctypes_generation/definitions/simple_types.txt b/ctypes_generation/definitions/simple_types.txt index 9c683f0..22a134b 100644 --- a/ctypes_generation/definitions/simple_types.txt +++ b/ctypes_generation/definitions/simple_types.txt @@ -13,6 +13,7 @@ PDWORD = POINTER(DWORD) LPDWORD = POINTER(DWORD) LPBYTE = POINTER(BYTE) ULONG_PTR = PVOID +LONG_PTR = PVOID DWORD_PTR = ULONG_PTR KAFFINITY = ULONG_PTR KPRIORITY = LONG @@ -59,6 +60,7 @@ HCRYPTHASH = PULONG ALG_ID = UINT DISPID = LONG MEMBERID = DISPID +LRESULT = LONG_PTR PSECURITY_DESCRIPTOR = PVOID LPUNKNOWN = POINTER(PVOID) diff --git a/docs/source/index.rst b/docs/source/index.rst index c478cfd..af761ca 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,7 +31,7 @@ Documentation :numbered: windows.rst - various.rst + winobject.rst native_exec.rst winproxy.rst pipe.rst diff --git a/docs/source/sample.rst b/docs/source/sample.rst index b5f6b08..0d4377c 100644 --- a/docs/source/sample.rst +++ b/docs/source/sample.rst @@ -104,6 +104,19 @@ Output .. literalinclude:: samples_output\registry_registry.txt +.. _sample_scheduled_task: + +Scheduled tasks +""""""""""""""" + +.. literalinclude:: ..\..\samples\scheduled_tasks\scheduled_task.py + +Output + +.. literalinclude:: samples_output\scheduled_task_scheduled_task.txt + + + .. _sample_wintrust: ``windows.wintrust`` diff --git a/docs/source/samples_output/scheduled_task_scheduled_task.txt b/docs/source/samples_output/scheduled_task_scheduled_task.txt new file mode 100644 index 0000000..8be31d6 --- /dev/null +++ b/docs/source/samples_output/scheduled_task_scheduled_task.txt @@ -0,0 +1,32 @@ +(cmd) python scheduled_tasks\scheduled_task.py +Task scheduler is +Root folder is +Listing sub folders + * + * + * +Manually opening subfolder <\Microsoft\Windows\AppID> +Working into + * PolicyConverter + * SmartScreenSpecific + * VerifiedPublisherCertStoreCheck + +Analysing task + * Name: + * Path: <\Microsoft\Windows\AppID\VerifiedPublisherCertStoreCheck> + * Definition: <> +Listing actions: + * Action: <> + * Type: + * path: <%windir%\system32\appidcertstorecheck.exe> + * arguments: +Listing triggers: + * Trigger type: + +Creating folder +Demo folder is +Creating Task definition +Registering task definition as in <> +Created task is +Deleting the demo task +Deleting the demo folder diff --git a/docs/source/task_scheduler.rst b/docs/source/task_scheduler.rst new file mode 100644 index 0000000..a097df4 --- /dev/null +++ b/docs/source/task_scheduler.rst @@ -0,0 +1,108 @@ +Task scheduler +============== + +.. module:: windows.winobject.task_scheduler + +.. note:: + + See sample :ref:`sample_scheduled_task` + + +TaskService +""""""""""" + +.. autoclass:: TaskService + :show-inheritance: + :special-members: __call__ + +TaskFolder +""""""""""" + +.. autoclass:: TaskFolder + :show-inheritance: + :special-members: __getitem__, __delitem__, __call__ + + +Task +"""" + +.. autoclass:: Task + :show-inheritance: + :special-members: __getitem__, __delitem__, __call__ + +TaskDefinition +"""""""""""""" + +.. autoclass:: TaskDefinition + :show-inheritance: + :special-members: __getitem__, __delitem__, __call__ + +Action +"""""" + +Action +'''''' + +.. autoclass:: Action + :show-inheritance: + :inherited-members: + :special-members: __getitem__, __delitem__, __call__ + + +ExecAction +'''''''''' + +.. autoclass:: ExecAction + :show-inheritance: + :inherited-members: + :special-members: __getitem__, __delitem__, __call__ + + +ComHandlerAction +'''''''''''''''' + +.. autoclass:: ComHandlerAction + :show-inheritance: + :inherited-members: + :special-members: __getitem__, __delitem__, __call__ + + +Trigger +""""""" + +.. autoclass:: Trigger + :show-inheritance: + :special-members: __getitem__, __delitem__, __call__ + + +Collections +""""""""""" + +TaskFolderCollection +'''''''''''''''''''' + +.. autoclass:: TaskFolderCollection + :show-inheritance: + :special-members: __getitem__, __delitem__, __call__ + +TaskCollection +'''''''''''''' + +.. autoclass:: TaskCollection + :show-inheritance: + :special-members: __getitem__, __delitem__, __call__ + +ActionCollection +'''''''''''''''' + +.. autoclass:: ActionCollection + :show-inheritance: + :special-members: __getitem__, __delitem__, __call__ + +TriggerCollection +''''''''''''''''' + +.. autoclass:: TriggerCollection + :show-inheritance: + :special-members: __getitem__, __delitem__, __call__ + diff --git a/docs/source/various.rst b/docs/source/winobject.rst similarity index 90% rename from docs/source/various.rst rename to docs/source/winobject.rst index f0c2e60..6387797 100644 --- a/docs/source/various.rst +++ b/docs/source/winobject.rst @@ -16,4 +16,5 @@ This sections describes them by group of relation. service.rst volume.rst wmi.rst - handle.rst \ No newline at end of file + handle.rst + task_scheduler.rst \ No newline at end of file diff --git a/tests/test_tasks.py b/tests/test_tasks.py new file mode 100644 index 0000000..e45b9fd --- /dev/null +++ b/tests/test_tasks.py @@ -0,0 +1,94 @@ +import pytest +import subprocess +import os.path + +import windows +import windows.generated_def as gdef + +from pfwtest import require_admin + +SCHTASKS = "c:\Windows\System32\schtasks.exe" + +task_scheduler = windows.system.task_scheduler + +def do_schtasks(*args): + return subprocess.check_output([SCHTASKS] + list(args)) + +# /SC MONTHLY allows to create the task without admin privileges +def schtasks_create_task(taskname, binary, sc="MONTHLY"): + return do_schtasks("/create", "/tn", taskname, "/tr", binary, "/sc", sc) + +def schtasks_delete_task(taskname): + return do_schtasks("/delete", "/tn", taskname, "/f") + +def schtasks_task_exists(taskname): + try: + return do_schtasks("/query", "/tn", taskname) + except subprocess.CalledProcessError as e: + if e.returncode == 1: + return False + raise + +SCHEDULED_TASK_PARAMS = [ + {"DIR": "", "NAME": "PFW_TEST1", "PATH": '"c:\windows\system32\notepad.exe"', "ARGS": "PFW_TEST_PARAM.txt"}, + # Test in subdir + {"DIR": "PFW_TEST_DIR", "NAME": "PFW_TEST1", "PATH": '"c:\windows\system32\notepad.exe"', "ARGS": "PFW_TEST_PARAM.txt"} +] + + +@pytest.fixture(params=SCHEDULED_TASK_PARAMS) +def sheduled_task(request): + descr = request.param + fullpath = os.path.join(descr["DIR"], descr["NAME"]) + full_binary = "{0} {1}".format(descr["PATH"], descr["ARGS"]) + schtasks_create_task(fullpath, full_binary) + yield descr + schtasks_delete_task(fullpath) + if descr["DIR"]: + schtasks_delete_task(descr["DIR"]) + + +def test_list_simple_task(sheduled_task): + # taskdir = os.path.dirname(sheduled_task) + # taskname = os.path.basename(sheduled_task) + print(sheduled_task) + if sheduled_task["DIR"]: + tdir = windows.system.task_scheduler(sheduled_task["DIR"]) + else: + tdir = windows.system.task_scheduler.root + task = tdir[sheduled_task["NAME"]] + assert task.name == sheduled_task["NAME"] + triggers = task.definition.triggers + assert len(list(triggers)) == 1 + assert triggers[1].type == gdef.TASK_TRIGGER_MONTHLY + actions = task.definition.actions + assert len(list(actions)) == 1 + assert actions[1].type == gdef.TASK_ACTION_EXEC + assert actions[1].path == sheduled_task["PATH"] + assert actions[1].arguments == sheduled_task["ARGS"] + + +@pytest.mark.parametrize("taskdict", SCHEDULED_TASK_PARAMS) +def test_create_delete_task(taskdict): + tsched = tdir = windows.system.task_scheduler + # Check task does not exists + assert not schtasks_task_exists(os.path.join(taskdict["DIR"], taskdict["NAME"])) + # Create Task + if taskdict["DIR"]: + tdir = windows.system.task_scheduler.root.create_folder(taskdict["DIR"]) + else: + tdir = windows.system.task_scheduler.root + ntd = tsched.create() + actions = ntd.actions + nea = actions.create(gdef.TASK_ACTION_EXEC) + nea.path = taskdict["PATH"] + nea.arguments = taskdict["ARGS"] + tdir.register(taskdict["NAME"], ntd) + # Check task presence + assert schtasks_task_exists(os.path.join(taskdict["DIR"], taskdict["NAME"])) + # Delete task + del tdir[taskdict["NAME"]] + if taskdict["DIR"]: + tdir = windows.system.task_scheduler.root.delete_folder(taskdict["DIR"]) + +# Test COM tasks ? \ No newline at end of file diff --git a/windows/winobject/system.py b/windows/winobject/system.py index 796a8b3..52a2709 100644 --- a/windows/winobject/system.py +++ b/windows/winobject/system.py @@ -18,6 +18,7 @@ from windows.winobject import volume from windows.winobject import wmi from windows.winobject import kernobj from windows.winobject import handle +from windows.winobject import task_scheduler from windows.generated_def.winstructs import * from windows.dbgprint import dbgprint @@ -87,6 +88,20 @@ class System(object): :type: :class:`~windows.winobject.wmi.WmiManager`""" return wmi.WmiManager() + + @utils.fixedpropety + def task_scheduler(self): + """An object able to manage scheduled tasks on the local system + + :type: :class:`~windows.winobject.task_scheduler.TaskService` + """ + windows.com.init() + clsid_task_scheduler = gdef.IID.from_string("0f87369f-a4e5-4cfc-bd3e-73e6154572dd") + task_service = task_scheduler.TaskService() + windows.com.create_instance(clsid_task_scheduler, task_service) + task_service.connect() + return task_service + #TODO: use GetComputerNameExA ? and recover other names ? @utils.fixedpropety def computer_name(self): diff --git a/windows/winobject/tasks.py b/windows/winobject/task_scheduler.py similarity index 70% rename from windows/winobject/tasks.py rename to windows/winobject/task_scheduler.py index a779edb..bbd50b3 100644 --- a/windows/winobject/tasks.py +++ b/windows/winobject/task_scheduler.py @@ -2,15 +2,14 @@ import windows.com import windows.generated_def as gdef -def generate_simple_getter(function, restype, extract_value=True): - @property +def generate_simple_getter(function, restype, extract_value=True, doc=None): def value_getter(self): res = restype() getattr(self, function)(res) if extract_value: return res.value return res - return value_getter + return property(value_getter, doc=doc) def add_simple_setter(getter, function, restype): @@ -30,6 +29,7 @@ class TaskCollectionType(object): return self.ITEM_TYPE def get_item(self, index): + """Return elements nb ``index``. Collection index starts at 1""" # TODO: ImprovedVariant.from_int() # vindex = windows.com.ImprovedVariant() # vindex.vt = gdef.VT_I4 @@ -44,7 +44,6 @@ class TaskCollectionType(object): def get_index(self, index): return index - def items_generator(self): for i in range(self.count): # Start index is 1 @@ -53,6 +52,10 @@ class TaskCollectionType(object): @property def items(self): + """Return the list of item in the collection + + :type: :class:`list` + """ return list(self.items_generator()) def __iter__(self): @@ -61,59 +64,92 @@ class TaskCollectionType(object): def __getitem__(self, index): # Allow subclasses to only overwrite 'get_item' to rewrite both behavior return self.get_item(index) - - - - +# Need to-do the doc=xx tricks to have the documentation in the 'AbstractAction' subclasses class AbstractAction(object): - type = generate_simple_getter("get_Type", gdef.TASK_ACTION_TYPE) - id = generate_simple_getter("get_Id", gdef.BSTR) + type_doc = """The type of action + + :type: :class:`~windows.generated_def.winstructs.TASK_ACTION_TYPE` + """ + type = generate_simple_getter("get_Type", gdef.TASK_ACTION_TYPE, doc=type_doc) + + id_doc = """The action id + + :type: :class:`~windows.generated_def.winstructs.BSTR` + """ + id = generate_simple_getter("get_Id", gdef.BSTR, doc=id_doc) + class Action(gdef.IAction, AbstractAction): + """Describe an action performed by a task""" ACTION_SUBTYPE = { gdef.TASK_ACTION_SEND_EMAIL: gdef.IEmailAction, gdef.TASK_ACTION_SHOW_MESSAGE: gdef.IShowMessageAction } - @property def subtype(self): + """Return the :class:`Action`-subtype according to :data:`AbstractAction.type`""" subinterface = self.ACTION_SUBTYPE[self.type] # KeyError ? return self.query(subinterface) class ExecAction(gdef.IExecAction, AbstractAction): + """Represent an action of type + :data:`~windows.generated_def.winstructs._TASK_ACTION_TYPE.TASK_ACTION_EXEC`""" path = generate_simple_getter("get_Path", gdef.BSTR) path = add_simple_setter(path, "put_Path", gdef.BSTR) + """[R-W] The path of the programm to execute""" arguments = generate_simple_getter("get_Arguments", gdef.BSTR) arguments = add_simple_setter(arguments, "put_Arguments", gdef.BSTR) + """[R-W] The arguments for the command to execute""" working_directory = generate_simple_getter("get_WorkingDirectory", gdef.BSTR) + """The working directory for the command to execute""" # Register action subtype Action.ACTION_SUBTYPE[gdef.TASK_ACTION_EXEC] = ExecAction class ComHandlerAction(gdef.IComHandlerAction, AbstractAction): + """Represent an action of type + :data:`~windows.generated_def.winstructs._TASK_ACTION_TYPE.TASK_ACTION_COM_HANDLER`""" + classid = generate_simple_getter("get_ClassId", gdef.BSTR) classid = add_simple_setter(classid, "put_ClassId", gdef.BSTR) + """The CLSID of the COM server executed + + :type: :class:`~windows.generated_def.winstructs.BSTR` + """ data = generate_simple_getter("get_Data", gdef.BSTR) data = add_simple_setter(data, "put_Data", gdef.BSTR) + """The DATA for the COM class + + :type: :class:`~windows.generated_def.winstructs.BSTR` + """ # Register action subtype Action.ACTION_SUBTYPE[gdef.TASK_ACTION_COM_HANDLER] = ComHandlerAction class Trigger(gdef.ITrigger): + """A task trigger""" type = generate_simple_getter("get_Type", gdef.TASK_TRIGGER_TYPE2) + """The type of trigger + + :type: :class:`~windows.generated_def.winstructs.TASK_TRIGGER_TYPE2` + """ class ActionCollection(gdef.IActionCollection, TaskCollectionType): ITEM_TYPE = Action def create(self, action_type): + """Create a new action of type ``action_type`` + + :rtype: A subclass of :class:`Action` + """ res = self.ITEM_TYPE() self.Create(action_type, res) return res.subtype @@ -128,15 +164,34 @@ class TriggerCollection(gdef.ITriggerCollection, TaskCollectionType): class TaskDefinition(gdef.ITaskDefinition): + """The definition of a task""" actions = generate_simple_getter("get_Actions", ActionCollection, extract_value=False) - triggers = generate_simple_getter("get_Triggers", TriggerCollection, extract_value=False) + """The list of actions of the task + :type: :class:`ActionCollection` + """ + triggers = generate_simple_getter("get_Triggers", TriggerCollection, extract_value=False) + """The list of triggers of the task + + :type: :class:`TriggerCollection` + """ class Task(gdef.IRegisteredTask): + """A scheduled task""" name = generate_simple_getter("get_Name", gdef.BSTR) + """The name of the task""" path = generate_simple_getter("get_Path", gdef.BSTR) + """The path of the task""" state = generate_simple_getter("get_State", gdef.TASK_STATE) + """The state of the task + + :type: :class:`~windows.generated_def.winstructs.TASK_STATE` + """ definition = generate_simple_getter("get_Definition", TaskDefinition, extract_value=False) + """The definition of the task + + :type: :class:`TaskDefinition` + """ def run(self, params=None, flags=gdef.TASK_RUN_NO_FLAGS, sessionid=0, user=None): if params is None: params = gdef.VARIANT() # Empty variant @@ -158,7 +213,12 @@ class TaskCollection(gdef.IRegisteredTaskCollection, TaskCollectionType): class TaskService(gdef.ITaskService): + """The task scheduler""" def create(self, flags=0): + """Create a new :class:`TaskDefinition` that can be used to create/register a new scheduled task + + :rtype: :class:`TaskDefinition` + """ res = TaskDefinition() self.NewTask(flags, res) return res @@ -171,69 +231,99 @@ class TaskService(gdef.ITaskService): self.Connect(server, user, domain, password) def folder(self, name): + """Return the :class:`TaskFolder` with ``name`` + + :rtype: :class:`TaskFolder` + """ folder = TaskFolder() self.GetFolder(name, folder) return folder __call__ = folder # use the same 'API' than the registry + """Alias for :func:`folder`""" @property def root(self): + r"""The root ``\`` :class:`TaskFolder`""" return self.folder("\\") class TaskFolder(gdef.ITaskFolder): + """A folder of tasks""" path = generate_simple_getter("get_Path", gdef.BSTR) name = generate_simple_getter("get_Name", gdef.BSTR) @property def folders(self): + """The list of sub-folders + + :type: :class:`TaskFolderCollection` + """ res = TaskFolderCollection() self.GetFolders(0, res) return res - def register(self, path, taskdef, flags=gdef.TASK_CREATE, userid=None, password=None, logonType=gdef.TASK_LOGON_NONE, ssid=None): + def register(self, name, taskdef, flags=gdef.TASK_CREATE, userid=None, password=None, logonType=gdef.TASK_LOGON_NONE, ssid=None): + """Register the task definition ``taskdef`` as a new task with ``name`` + + :rtype: :class:`Task` + """ new_task = Task() if userid is None: userid = gdef.VARIANT() # Empty variant if password is None: password = gdef.VARIANT() # Empty variant if ssid is None: ssid = gdef.VARIANT() # Empty variant - self.RegisterTaskDefinition(path, taskdef, flags, userid, password, logonType, ssid, new_task) + self.RegisterTaskDefinition(name, taskdef, flags, userid, password, logonType, ssid, new_task) return new_task @property def tasks(self, flags=gdef.TASK_ENUM_HIDDEN): + """The list of tasks in the folder + + :type: :class:`TaskCollection` + """ tasks = TaskCollection() self.GetTasks(flags, tasks) return tasks def get_task(self, name): + """Retrieve the task with ``name`` in the current folder + + :rtype: :class:`Task` + """ res = Task() self.GetTask(name, res) return res def delete_task(self, name): + """Delete the task with ``name`` in the current folder""" return self.DeleteTask(name, 0) def folder(self, name): + """Return the :class:`TaskFolder` with ``name``""" folder = TaskFolder() self.GetFolder(name, folder) return folder def create_folder(self, name): + """Create a new sub-:class:`TaskFolder` with ``name``""" folder = TaskFolder() self.CreateFolder(name, gdef.VARIANT(), folder) return folder def delete_folder(self, name): + """Delete the sub-folder with ``name`` in the current folder""" return self.DeleteFolder(name, 0) __getitem__ = get_task + """ Alias for :func:`get_task`""" __delitem__ = delete_task + """ Alias for :func:`delete_task`""" __call__ = folder # use the same 'API' than the registry + """ Alias for :func:`folder`"""