|
| 1 | +import logging |
| 2 | +from contextlib import suppress |
| 3 | + |
| 4 | +HAVE_PYMSI = False |
| 5 | +with suppress(ImportError): |
| 6 | + import pymsi |
| 7 | + HAVE_PYMSI = True |
| 8 | + |
| 9 | +log = logging.getLogger(__name__) |
| 10 | + |
| 11 | +type_dll = 0x00000001 # msidbCustomActionTypeDll |
| 12 | +type_exe = 0x00000002 # msidbCustomActionTypeExe |
| 13 | +type_text_data = 0x00000003 # msidbCustomActionTypeTextData |
| 14 | +type_jscript = 0x00000005 # msidbCustomActionTypeJScript |
| 15 | +type_vbscript = 0x00000006 # msidbCustomActionTypeVBScript |
| 16 | +type_install = 0x00000007 # msidbCustomActionTypeInstall |
| 17 | +type_binary_data = 0x00000000 # msidbCustomActionTypeBinaryData |
| 18 | +type_source_file = 0x00000010 # msidbCustomActionTypeSourceFile |
| 19 | +type_directory = 0x00000020 # msidbCustomActionTypeDirectory |
| 20 | +type_property = 0x00000030 # msidbCustomActionTypeProperty |
| 21 | +type_continue = 0x00000040 # msidbCustomActionTypeContinue (Async/Sync ignore return) |
| 22 | +type_async = 0x00000080 # msidbCustomActionTypeAsync |
| 23 | +type_first_sequence = 0x00000100 # msidbCustomActionTypeFirstSequence |
| 24 | +type_once_per_process = 0x00000200 # msidbCustomActionTypeOncePerProcess |
| 25 | +type_client_repeat = 0x00000300 # msidbCustomActionTypeClientRepeat |
| 26 | +type_in_script = 0x00000400 # msidbCustomActionTypeInScript (Deferred) |
| 27 | +type_rollback = 0x00000100 # msidbCustomActionTypeRollback |
| 28 | +type_commit = 0x00000200 # msidbCustomActionTypeCommit |
| 29 | +type_no_impersonate = 0x00000800 # msidbCustomActionTypeNoImpersonate |
| 30 | +type_64bit_script = 0x00001000 # msidbCustomActionType64BitScript |
| 31 | +type_hide_target = 0x00002000 # msidbCustomActionTypeHideTarget |
| 32 | +type_ts_aware = 0x00004000 # msidbCustomActionTypeTSAware |
| 33 | +type_patch_uninstall = 0x00008000 # msidbCustomActionTypePatchUninstall |
| 34 | + |
| 35 | +mask_basic_type = 0x7 |
| 36 | +mask_source_type = 0x30 |
| 37 | +mask_return_type = 0xC0 |
| 38 | +mask_execution = 0xF00 |
| 39 | + |
| 40 | +# Mask of all known bits to calculate remainder / check for bogus data |
| 41 | +mask_all_known = ( |
| 42 | + mask_basic_type | |
| 43 | + mask_source_type | |
| 44 | + mask_return_type | |
| 45 | + mask_execution | |
| 46 | + type_64bit_script | |
| 47 | + type_hide_target | |
| 48 | + type_ts_aware | |
| 49 | + type_patch_uninstall |
| 50 | +) |
| 51 | + |
| 52 | +basic_type_map = { |
| 53 | + type_dll: "DLL (msidbCustomActionTypeDll)", |
| 54 | + type_exe: "EXE (msidbCustomActionTypeExe)", |
| 55 | + type_text_data: "Text Data (msidbCustomActionTypeTextData)", |
| 56 | + type_jscript: "JScript (msidbCustomActionTypeJScript)", |
| 57 | + type_vbscript: "VBScript (msidbCustomActionTypeVBScript)", |
| 58 | + type_install: "Install (msidbCustomActionTypeInstall)", |
| 59 | + 0: "None/Error (0)", |
| 60 | +} |
| 61 | + |
| 62 | +def parse_msi_action_type(input_int): |
| 63 | + """ |
| 64 | + Parses an MSI Custom Action Type integer into a dictionary of properties. |
| 65 | + """ |
| 66 | + # ========================================================================= |
| 67 | + # MSI CONSTANTS (Based on C++ Header Definitions) |
| 68 | + # ========================================================================= |
| 69 | + if not input_int: |
| 70 | + return {} |
| 71 | + |
| 72 | + try: |
| 73 | + val = int(input_int) |
| 74 | + except ValueError: |
| 75 | + return {} |
| 76 | + |
| 77 | + result = { |
| 78 | + "basic_type": "", |
| 79 | + "source": "", |
| 80 | + "return_processing": "", |
| 81 | + "execution": "", |
| 82 | + "flags": [], |
| 83 | + "remainder": 0 |
| 84 | + } |
| 85 | + |
| 86 | + # Basic Type |
| 87 | + b_type = val & mask_basic_type |
| 88 | + result["basic_type"] = basic_type_map.get(b_type, "Unknown Type (%d)" % b_type) |
| 89 | + |
| 90 | + # Source Location |
| 91 | + s_loc = val & mask_source_type |
| 92 | + if s_loc == type_binary_data: |
| 93 | + result["source"] = "Binary Table (msidbCustomActionTypeBinaryData)" |
| 94 | + elif s_loc == type_source_file: |
| 95 | + result["source"] = "Source File (msidbCustomActionTypeSourceFile)" |
| 96 | + elif s_loc == type_directory: |
| 97 | + result["source"] = "Directory (msidbCustomActionTypeDirectory)" |
| 98 | + elif s_loc == type_property: |
| 99 | + result["source"] = "Property (msidbCustomActionTypeProperty)" |
| 100 | + |
| 101 | + # Return Processing (Bits 6-7) |
| 102 | + r_type = val & mask_return_type |
| 103 | + if r_type == 0: |
| 104 | + # No specific constant exists for 0 (default), labeled as (None) for consistency with MSDN |
| 105 | + result["return_processing"] = "Synchronous, Check Return Code (None)" |
| 106 | + elif r_type == type_continue: |
| 107 | + result["return_processing"] = "Synchronous, Ignore Return Code (msidbCustomActionTypeContinue)" |
| 108 | + elif r_type == type_async: |
| 109 | + result["return_processing"] = "Asynchronous, Wait for Exit (msidbCustomActionTypeAsync)" |
| 110 | + elif r_type == (type_async | type_continue): |
| 111 | + result["return_processing"] = ( |
| 112 | + "Asynchronous, Do Not Wait (msidbCustomActionTypeAsync | msidbCustomActionTypeContinue)") |
| 113 | + |
| 114 | + # Execution Scheduling |
| 115 | + exec_val = val & mask_execution |
| 116 | + exec_parts = [] |
| 117 | + is_deferred = (exec_val & type_in_script) == type_in_script |
| 118 | + if is_deferred: |
| 119 | + exec_parts.append("Deferred (msidbCustomActionTypeInScript)") |
| 120 | + |
| 121 | + # In Deferred execution, bits 0x100 and 0x200 are independent (Rollback/Commit) |
| 122 | + if exec_val & type_rollback: |
| 123 | + exec_parts.append("Rollback (msidbCustomActionTypeRollback)") |
| 124 | + |
| 125 | + if exec_val & type_commit: |
| 126 | + exec_parts.append("Commit (msidbCustomActionTypeCommit)") |
| 127 | + |
| 128 | + else: |
| 129 | + # In Immediate execution, 0x300 is a specific combination (msidbCustomActionTypeClientRepeat) |
| 130 | + # https://learn.microsoft.com/en-us/windows/win32/msi/custom-action-execution-scheduling-options |
| 131 | + sched_bits = exec_val & (type_first_sequence | type_once_per_process) |
| 132 | + |
| 133 | + if sched_bits == type_client_repeat: |
| 134 | + exec_parts.append("ClientRepeat (msidbCustomActionTypeClientRepeat)") |
| 135 | + else: |
| 136 | + # Handle individual bits if it's not the specific ClientRepeat combo |
| 137 | + if exec_val & type_first_sequence: |
| 138 | + exec_parts.append("FirstSequence (msidbCustomActionTypeFirstSequence)") |
| 139 | + |
| 140 | + if exec_val & type_once_per_process: |
| 141 | + exec_parts.append("OncePerProcess (msidbCustomActionTypeOncePerProcess)") |
| 142 | + |
| 143 | + # Check for NoImpersonate |
| 144 | + if exec_val & type_no_impersonate: |
| 145 | + exec_parts.append("NoImpersonate (msidbCustomActionTypeNoImpersonate)") |
| 146 | + |
| 147 | + if not exec_parts: |
| 148 | + result["execution"] = "Immediate (User Context)" |
| 149 | + else: |
| 150 | + result["execution"] = " + ".join(exec_parts) |
| 151 | + |
| 152 | + # Parse Global Flags |
| 153 | + if val & type_64bit_script: |
| 154 | + result["flags"].append("64-bit Script (msidbCustomActionType64BitScript)") |
| 155 | + if val & type_hide_target: |
| 156 | + result["flags"].append("Hide Target (msidbCustomActionTypeHideTarget)") |
| 157 | + if val & type_ts_aware: |
| 158 | + result["flags"].append("Terminal Server Aware (msidbCustomActionTypeTSAware)") |
| 159 | + if val & type_patch_uninstall: |
| 160 | + result["flags"].append("Patch Uninstall (msidbCustomActionTypePatchUninstall)") |
| 161 | + |
| 162 | + # Check for bogus / malformed stuffs; remainder is 0 in tested samples |
| 163 | + result["remainder"] = val & ~mask_all_known |
| 164 | + |
| 165 | + return result |
| 166 | + |
| 167 | +def parse_msi(msi_path: str): |
| 168 | + msi = {} |
| 169 | + if not HAVE_PYMSI: |
| 170 | + return msi |
| 171 | + try: |
| 172 | + with pymsi.Package(msi_path) as package: |
| 173 | + if "CustomAction" in package.tables: |
| 174 | + current_table_obj = package.get("CustomAction") |
| 175 | + msi = { |
| 176 | + "rows": [row for row in current_table_obj.rows], |
| 177 | + "columns": [column.name for column in current_table_obj.columns], |
| 178 | + } |
| 179 | + for row in msi["rows"]: |
| 180 | + row["Enrich"] = parse_msi_action_type(row["Type"]) |
| 181 | + except Exception as e: |
| 182 | + log.error("parse_msi: %s", e) |
| 183 | + return msi |
| 184 | + |
| 185 | + |
| 186 | +if __name__ == "__main__": |
| 187 | + import sys |
| 188 | + from pprint import pprint as pp |
| 189 | + pp(parse_msi(sys.argv[1])) |
| 190 | + # pymsi uses CamelCase for their dict keys, and my function does not. so if you want it to be clean feel free to make it CamelCase |
0 commit comments