From 9d11a0a81f60c315755d91adbc0c51dc6c002cf8 Mon Sep 17 00:00:00 2001 From: "iris.boeters" Date: Thu, 11 Jul 2024 16:46:12 +0200 Subject: [PATCH 1/3] add dissect msi --- dissect/executable/exception.py | 12 ++ dissect/executable/msi/__init__.py | 0 dissect/executable/msi/c_msi.py | 22 ++ dissect/executable/msi/msi.py | 329 +++++++++++++++++++++++++++++ 4 files changed, 363 insertions(+) create mode 100644 dissect/executable/msi/__init__.py create mode 100644 dissect/executable/msi/c_msi.py create mode 100644 dissect/executable/msi/msi.py diff --git a/dissect/executable/exception.py b/dissect/executable/exception.py index b0fb678..85cb915 100644 --- a/dissect/executable/exception.py +++ b/dissect/executable/exception.py @@ -4,3 +4,15 @@ class Error(Exception): class InvalidSignatureError(Error): """Exception that occurs if the magic in the header does not match.""" + +class InvalidDataType(Error): + """Exception that occurs if the datatype of a cell in an MSI table is not a valid type.""" + pass + +class InvalidStringData(Error): + """Exception that occurs if the information on strings from an MSI tables is incorrect.""" + pass + +class InvalidTable(Error): + """Exception that occurs if an MSI table is not restored correctly.""" + pass diff --git a/dissect/executable/msi/__init__.py b/dissect/executable/msi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dissect/executable/msi/c_msi.py b/dissect/executable/msi/c_msi.py new file mode 100644 index 0000000..88b221c --- /dev/null +++ b/dissect/executable/msi/c_msi.py @@ -0,0 +1,22 @@ +from dissect import cstruct + +msi_def = """ +#define MSI_DATASIZEMASK 0x00ff +#define MSITYPE_VALID 0x0100 +#define MSITYPE_LOCALIZABLE 0x0200 +#define MSITYPE_STRING 0x0800 +#define MSITYPE_NULLABLE 0x1000 +#define MSITYPE_KEY 0x2000 +#define MSITYPE_TEMPORARY 0x4000 +#define MSITYPE_UNKNOWN 0x8000 + +#define TABLENAME_COLUMNS "_Columns" +#define TABLENAME_TABLE "_Tables" +#define TABLENAME_STRINGPOOL "_StringPool" +#define TABLENAME_STRINGDATA "_StringData" + +#define CP_ACP 0x0 +""" + +c_msi = cstruct.cstruct() +c_msi.load(msi_def) diff --git a/dissect/executable/msi/msi.py b/dissect/executable/msi/msi.py new file mode 100644 index 0000000..56a71da --- /dev/null +++ b/dissect/executable/msi/msi.py @@ -0,0 +1,329 @@ +from __future__ import annotations +import struct +from dissect.ole import OLE +from dissect.ole.ole import DirectoryEntry +from dissect.ole.exceptions import NotFoundError +from dissect.executable.exception import InvalidDataType, InvalidStringData, InvalidTable +from dissect.executable.msi.c_msi import c_msi +from typing import BinaryIO, Generator +from dissect.ole.c_ole import STGTY +import os + +def to_str(bytes_content: bytes) -> str: + return str(bytes_content, 'utf-8') + +class MSI(OLE): + def __init__(self, fh: BinaryIO) -> None: + OLE.__init__(self, fh) + self.StringPool = None + self.StringData = None + self.codepage: int = 0 + self.strings: list[list[bytes, int, int]] = [[b'', 0, 0]] # the string at idx 0 is an empty string + self.n_strings: int = 0 + self._tablecache = {} + + # from https://github.com/ironfede/openmcdf/issues/11 + # MSI stores multiple characters in one unicode code point + # this function decodes all these directory names to get the actual directory names + # with the funcionts get, listdir and dirlist from OLE, it will parse the all directory entries from root and save it in self._dirlist + # it will always only fill _dirlist once, using listdir + # with these function overloads, first MSI.listdir will be called, and after the directory names will be decrypted + def listdir(self) -> dict[str, DirectoryEntry]: + if self._dirlist: + return OLE.listdir(self) + OLE.listdir(self) + self._decode_directory_entry_names() + return self._dirlist + + dirlist = listdir + + def _msi_base64_encode(self, byte: int) -> int: + # 0x00-0x3F (0-63) are converted to '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._' + # all other values higher than 0x3F are converted to '_' + base64_str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._" + if byte < len(base64_str): + return ord(base64_str[byte]) + return ord('_') + + def _decode_name(self, dir_entry: DirectoryEntry) -> tuple[str, int]: + len = 0 + output = "" + for char in dir_entry.entry._ab: + char = ord(char) + if ((char >= 0x3800) and (char <= 0x4840)) : + if (char >= 0x4800): # 0x4800 - 0x483F -> decode only one character + char = self._msi_base64_encode(char - 0x4800) + else: # 0x3800 - 0x383F -> decode two characters + char -= 0x3800 + output += chr(self._msi_base64_encode(char & 0x3f)) + len += 1 + char = self._msi_base64_encode((char >> 6) & 0x3f) + # characters < 0x3800 or > 0x4840 will be saved without any decoding + output += chr(char) + len += 1 + return output.rstrip('\x00'), len + + def _decode_directory_entry_names(self) -> None: + old_dir_list = self._dirlist.copy() + for old_name in old_dir_list: + dir_entry = self._dirlist[old_name] + decoded_name, len = self._decode_name(dir_entry) + # update directory entry + dir_entry.name = decoded_name + dir_entry.entry._ab = decoded_name + dir_entry.entry._cb = len + self._dirlist[old_name].entry = dir_entry + # update _dirlist: key (name) and del old key + self._dirlist[decoded_name] = self._dirlist.pop(old_name) + + def _valid_codepage(self, codepage: int) -> bool: + if codepage in [c_msi.CP_ACP, 37, 424, 437, 500, 737, 775, 850, 852, 855, 856, 857, 860, 861, 862, 863, 864, 865, 866, 869, 874, 875, + 878, 932, 936, 949, 950, 1006, 1026, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258, 1361, 10000, 10006, + 10007, 10029, 10079, 10081, 20127, 20866, 20932, 21866, 28591, 28592, 28593, 28594, 28595, 28596, 28597, 28598, + 28599, 28600, 28603, 28604, 28605, 28606, 65000, 65001]: + return True + return False + + def _read_content_from_stream(self, name: str) -> bytes: + # call OLE get function + dir = self.get(name) + # if the error is returned + if isinstance(dir, NotFoundError): + # try with extra _ in front + dir = self.get("_" + name) + if isinstance(dir, NotFoundError): + # assume the table is empty if dissect.OLE didn't parse directory entry, but it is in __Tables table + for row in self.get_table("_Tables").rows(): + if name == to_str(row._cells["Name"]): + return b'' + raise NotFoundError(f"directory entry not found: {name}") + stream = dir.open() + if stream is None: + InvalidTable(f"Cannot read from table: {name}") + content = stream.read() + return content + + def _load_string_table(self) -> None: + try: + self.StringPool = self._read_content_from_stream(c_msi.TABLENAME_STRINGPOOL) + self.StringData = self._read_content_from_stream(c_msi.TABLENAME_STRINGDATA) + except NotFoundError: + raise InvalidStringData("error reading string data") + size = len(self.StringPool) + if size <= 4: + raise InvalidStringData("no string data available") + # there is always 4 bytes per string needed + count = int(size / 4) + # make string pool from little to big endian + self.StringPool = struct.unpack(f"<{len(self.StringPool)//2}H", self.StringPool) + # the first 4 bytes are for defining the codepage + self.codepage = self.StringPool[0] | ((self.StringPool[1] & ~0x8000) << 16) + if self._valid_codepage(self.codepage) == False: + raise InvalidStringData("invalid codepage") + idx_strpool = 2 + count -= 1 + offset = 0 + extra_length = 0 + for _ in range(count): + length = self.StringPool[idx_strpool] + # if the string length is more than 65535, previous string is 0 and high word of length is in refs field + # empty strings, that do have an idx, have a length of 0 and a refs of 0 + if extra_length: + length = (extra_length << 16) + length + extra_length = 0 + if (offset + length) > len(self.StringData): + raise InvalidStringData("error reading string information, invalid data") + refs = self.StringPool[idx_strpool + 1] + idx_strpool += 2 + # only if length is 0 and refs contains information, it is not an empty string + if length == 0 and refs != 0: + extra_length = refs + continue + str = self.StringData[offset:offset + length] + offset += length + self.strings.append([str, length, refs]) + if (offset != len(self.StringData)): + raise InvalidStringData("error reading string information, invalid data (not everything is read)") + self.n_strings = len(self.strings) + + def _msitype_is_binary(self, datatype: int) -> bool: + if ((datatype) & ~c_msi.MSITYPE_NULLABLE) == (c_msi.MSITYPE_STRING | c_msi.MSITYPE_VALID): + return True + return False + + # see bytes_per_column() function here https://github.com/GNOME/msitools/blob/master/libmsi/table.c + def _bytes_per_cell(self, datatype: int) -> int: + if self._msitype_is_binary(datatype): + return 2 + if datatype & c_msi.MSITYPE_STRING: + return 2 + elif (datatype & 0xFF) <= 2: + return 2 + elif (datatype & 0xFF) != 4: + raise InvalidDataType("invalid datatype") + return 4 + + def _get_str(self, idx: int) -> bytes: + if idx < 0 or idx > self.n_strings: + IndexError(f"invalid index for strings table: {idx}") + # idx 0 is an empty string + # idx starts at 1 -> idx -1 to get the correct string and [0] to get the string value + return self.strings[idx][0] + + def _read_table(self, table: Table) -> None: + # size of row = n of cols * size of one cell + row_size = 0 + for col in range(table.n_cols): + row_size += self._bytes_per_cell(table.columns[col].type) + table.rawdata = self._read_content_from_stream(table.name) + table.rawsize = len(table.rawdata) + if table.rawsize % row_size: + raise InvalidTable("table size invalid") + table.n_rows = int(table.rawsize / row_size) + offset = 0 + for i in range(table.n_cols): + bytes_per_cell = self._bytes_per_cell(table.columns[i].type) + for _ in range(table.n_rows): + value = table.rawdata[offset : offset + bytes_per_cell] + if bytes_per_cell == 2: + value = struct.unpack(' Table: + # initiate the table if it is not already in the cache + try: + return self._tablecache[name] + except KeyError: + t = Table(name, self) + # in order load a table, init columns based on column information for the table + t._init_columns() + # if there are no columns, the table is invalid, assuming that also empty tables have columns + if t.n_cols == 0: + raise InvalidTable("table and columns not found") + # then read in all the cells + self._read_table(t) + self._tablecache[name] = t + return t + + # the required tables to build up all database tables + # they have double underscores, or single: + # __StringPool + # __StringData + # __Validation + # __Columns + # __Tables + def get_table(self, name: str) -> Table: + try: + return self._tablecache[name] + except KeyError: # if the table doesn't exist yet, load the table + # in order to load a table, first load the information on strings + if self.n_strings == 0: + self._load_string_table() + # next the __Columns table needs to be loaded + columns_table = self._load_table(c_msi.TABLENAME_COLUMNS) + # return if there was an error loading the table, or the columns table was asked for + if name == c_msi.TABLENAME_COLUMNS: + return columns_table + # then load the table asked for + table = self._load_table(name) + return table + + def get_tables(self) -> Generator[Table, None, None]: + tables_table = self.get_table("_Tables") + for row in tables_table.rows(): + yield(self.get_table(to_str(row._cells["Name"]))) + + def dump_stream(self, name: str) -> None: + # create output directory + output_dir = "./output" + # only create the output directory if it doesn't already exists + os.makedirs(output_dir, exist_ok=True) + dir_entry = self.get(name) + if isinstance(dir_entry, NotFoundError): + raise NotFoundError(f"directory entry not found: {name}") + # only read and write stream directory entries + if dir_entry.type == STGTY.STGTY_STREAM: + content = self._read_content_from_stream(name) + file_path = os.path.join(output_dir, f"{name}") + with open(file_path, "wb") as file: + file.write(content) + + def dump_streams(self) -> None: + # loop over directory entries + for dir_name in self.listdir(): + self.dump_stream(dir_name) + +class Table(): + # table only holds columns, rows can be created when looping over columns + def __init__(self, name: str, msi: MSI) -> None: + self.msi = msi + self.name = name + self.columns = {} + self.n_cols = 0 + self.n_rows = 0 + self._init_columns() + + # the __Columns and also the __Tables tables have pre-defined column information + # for the rest of the tables, column info is defined in the __Columns table + def _init_columns(self) -> None: + if (self.name == c_msi.TABLENAME_COLUMNS): + self.columns[0] = Column(self, 1, b'Table', c_msi.MSITYPE_VALID | c_msi.MSITYPE_STRING | c_msi.MSITYPE_KEY | 64) + self.columns[1] = Column(self, 2, b'Number', c_msi.MSITYPE_VALID | c_msi.MSITYPE_KEY | 2) + self.columns[2] = Column(self, 3, b'Name', c_msi.MSITYPE_VALID | c_msi.MSITYPE_STRING | 64) + self.columns[3] = Column(self, 4, b'Type', c_msi.MSITYPE_VALID | 2) + elif (self.name == c_msi.TABLENAME_TABLE): + self.columns[0] = Column(self, 1, b'Name', c_msi.MSITYPE_VALID | c_msi.MSITYPE_STRING | c_msi.MSITYPE_KEY | 64) + else: + columns_table = self.msi.get_table(c_msi.TABLENAME_COLUMNS) + i = 0 + for row in columns_table.rows(): + if to_str(row._cells["Table"]) == self.name: + self.columns[i] = Column(self, row._cells["Number"], row._cells["Name"], row._cells["Type"]) + i += 1 + self.n_cols = len(self.columns) + if self.n_cols == 0: + InvalidTable(f"no columns found, table not valid: {self.name}") + + def rows(self) -> Generator[Row, None, None]: + for row_number in range(self.n_rows): + yield Row(self, self.n_cols, row_number) + + def __repr__(self) -> str: + return f"" + +class Row(): + def __init__(self, table: Table, n_cols: int, row_number: int) -> None: + self._table = table + self._table_name = table.name + self._cells = {} + self._n_cols = n_cols + # yield row cells by going over columns + for key in table.columns: + col = table.columns[key] + if isinstance(col.name, str) is False: + col.name = to_str(col.name) + # rows are a dictionary of column names and values + self._cells.update({col.name: col.cells[row_number]}) + + def __repr__(self) -> str: + values = " ".join([f"{key}={value!r}" for key, value in self._cells.items()]) + return f"" + +class Column(): + def __init__(self, table: Table, number: int, name: bytes, datatype: int): + self._table = table + self._table_name = table.name + self.number = number + self.name = name + self.type = datatype + self.cells = [] + + def __repr__(self) -> str: + return f" Date: Fri, 19 Jul 2024 15:24:57 +0200 Subject: [PATCH 2/3] add tests MSI --- dissect/executable/__init__.py | 2 + dissect/executable/msi/__init__.py | 3 + tests/data/test_msi.msi | Bin 0 -> 27136 bytes tests/test_msi.py | 112 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 tests/data/test_msi.msi create mode 100644 tests/test_msi.py diff --git a/dissect/executable/__init__.py b/dissect/executable/__init__.py index 43d2a88..4d041c4 100644 --- a/dissect/executable/__init__.py +++ b/dissect/executable/__init__.py @@ -1,5 +1,7 @@ from dissect.executable.elf import ELF +from dissect.executable.msi import MSI __all__ = [ "ELF", + "MSI", ] diff --git a/dissect/executable/msi/__init__.py b/dissect/executable/msi/__init__.py index e69de29..747966d 100644 --- a/dissect/executable/msi/__init__.py +++ b/dissect/executable/msi/__init__.py @@ -0,0 +1,3 @@ +from .msi import MSI + +__all__ = ["MSI"] diff --git a/tests/data/test_msi.msi b/tests/data/test_msi.msi new file mode 100644 index 0000000000000000000000000000000000000000..b1bfd2f694ede84e80540ee8431cb7c83e20223c GIT binary patch literal 27136 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 literal 0 HcmV?d00001 diff --git a/tests/test_msi.py b/tests/test_msi.py new file mode 100644 index 0000000..bee572c --- /dev/null +++ b/tests/test_msi.py @@ -0,0 +1,112 @@ +import pytest +from dissect.executable import MSI +from dissect.executable.exception import InvalidTable +from dissect.ole.exceptions import InvalidFileError, NotFoundError +from io import BytesIO + +def test_msi_invalid_signature(): + with pytest.raises(InvalidFileError): + MSI(BytesIO(b"error" + b"\x00" * 0x200)) + +def test_msi_valid_signature(): + with open("./data/test_msi.msi", "rb") as fh: + MSI(fh) + +def test_msi_listdir(): + known_directory_entries = [ + '_Media', + '__Columns', + '__Tables', + '_Feature', + '_Registry', + '_Property', + 'required.cab', + '_Directory', + '_Component', + '__StringData', + '__StringPool', + '__Validation', + '_AdminUISequence', + '_FeatureComponents', + '_InstallUISequence', + '_AdminExecuteSequence', + '_AdvtExecuteSequence', + '_MsiPatchCertificate', + '_InstallExecuteSequence', + '_MsiDigitalCertificate', + '\x05DigitalSignature', + '\x05SummaryInformation', + 'MsiDigitalCertificate.CertificateForPatching' + ] + with open("./data/test_msi.msi", "rb") as fh: + msi = MSI(fh) + assert known_directory_entries == [dir_entry for dir_entry in msi.listdir()] + +def test_invalid_directory_entry(): + with open("./data/test_msi.msi", "rb") as fh: + msi = MSI(fh) + assert isinstance(msi.get("invalid name"), NotFoundError) # OLE.get doesn't raise the error, but returns it + +def test_valid_directory_entry(): + with open("./data/test_msi.msi", "rb") as fh: + msi = MSI(fh) + dir_entry = msi.get("_Registry") + assert dir_entry != None + +def test_msi_load_tables(): + known_table_names = [ + "_Validation", + "AdminExecuteSequence", + "AdminUISequence", + "AdvtExecuteSequence", + "Component", + "Directory", + "Feature", + "FeatureComponents", + "File", + "InstallExecuteSequence", + "InstallUISequence", + "Media", + "Property", + "MsiDigitalCertificate", + "MsiPatchCertificate", + "Registry" + ] + with open("./data/test_msi.msi", "rb") as fh: + msi = MSI(fh) + assert known_table_names == [table.name for table in msi.get_tables()] + +def test_invalid_table(): + with open("./data/test_msi.msi", "rb") as fh: + msi = MSI(fh) + with pytest.raises(InvalidTable): + msi.get_table("invalid name") + +def test_msi_rows(): + with open("./data/test_msi.msi", "rb") as fh: + msi = MSI(fh) + t = msi.get_table("Registry") + for i, row in enumerate(t.rows()): + continue + assert i == 0 + assert t.n_rows == i + 1 + assert row._table_name == "Registry" + assert row._cells['Registry'] == b'NonEmptyComponent' + assert row._cells['Key'] == b'SOFTWARE\\DropboxUpdate\\Update' + +def test_msi_columns(): + with open("./data/test_msi.msi", "rb") as fh: + msi = MSI(fh) + t = msi.get_table("Registry") + for i, col in enumerate(t.columns): + continue + assert i == 5 + assert t.n_cols == i + 1 + assert t.columns[i].name == b'Component_' + assert t.columns[i].cells[0] == b'MainComponent' + +def test_invalid_dump_stream(): + with open("./data/test_msi.msi", "rb") as fh: + msi = MSI(fh) + with pytest.raises(NotFoundError): + msi.dump_stream("invalid name") From 5ad380d2420585f303cc2b2de6953b5407a8ad25 Mon Sep 17 00:00:00 2001 From: "iris.boeters" Date: Mon, 22 Jul 2024 11:47:45 +0200 Subject: [PATCH 3/3] change testing function names to test_msi --- tests/test_msi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_msi.py b/tests/test_msi.py index bee572c..182eccf 100644 --- a/tests/test_msi.py +++ b/tests/test_msi.py @@ -42,12 +42,12 @@ def test_msi_listdir(): msi = MSI(fh) assert known_directory_entries == [dir_entry for dir_entry in msi.listdir()] -def test_invalid_directory_entry(): +def test_msi_invalid_directory_entry(): with open("./data/test_msi.msi", "rb") as fh: msi = MSI(fh) assert isinstance(msi.get("invalid name"), NotFoundError) # OLE.get doesn't raise the error, but returns it -def test_valid_directory_entry(): +def test_msi_valid_directory_entry(): with open("./data/test_msi.msi", "rb") as fh: msi = MSI(fh) dir_entry = msi.get("_Registry") @@ -76,7 +76,7 @@ def test_msi_load_tables(): msi = MSI(fh) assert known_table_names == [table.name for table in msi.get_tables()] -def test_invalid_table(): +def test_msi_invalid_table(): with open("./data/test_msi.msi", "rb") as fh: msi = MSI(fh) with pytest.raises(InvalidTable): @@ -105,7 +105,7 @@ def test_msi_columns(): assert t.columns[i].name == b'Component_' assert t.columns[i].cells[0] == b'MainComponent' -def test_invalid_dump_stream(): +def test_msi_invalid_dump_stream(): with open("./data/test_msi.msi", "rb") as fh: msi = MSI(fh) with pytest.raises(NotFoundError):