diff --git a/Evtx/Nodes.py b/Evtx/Nodes.py index 1cc9bc5..475864d 100644 --- a/Evtx/Nodes.py +++ b/Evtx/Nodes.py @@ -467,9 +467,10 @@ def get_variant_value(buf, offset, chunk, parent, type_, length=None): NODE_TYPES.BXML: BXmlTypeNode, NODE_TYPES.WSTRINGARRAY: WstringArrayTypeNode, } + types.update(ARRAY_TYPES) try: TypeClass = types[type_] - except IndexError: + except KeyError: raise NotImplementedError("Type {} not implemented".format(type_)) return TypeClass(buf, offset, chunk, parent, length=length) @@ -1603,3 +1604,62 @@ def string(self): "Conditional Substitution", "Start of Stream", ] + + +def _fixed_array_type_node(element_class, element_size): + """ + Build a variant type for an array of fixed-width values: the elements are + stored one after another, and each is rendered by `element_class`. + The array renders as pieces, as WstringArrayTypeNode does. + """ + + class FixedArrayTypeNode(VariantTypeNode): + def __init__(self, buf, offset, chunk, parent, length=None): + super().__init__(buf, offset, chunk, parent, length=length) + if self._length is None: + raise NotImplementedError("an array value outside a substitution has no size") + if self._length % element_size != 0: + raise ParseException( + f"array of {self._length} bytes is not a whole number of {element_size}-byte values" + ) + + def tag_length(self): + return self._length + + def string(self): + acc = [] + for index in range(self._length // element_size): + element = element_class( + self._buf, self.offset() + index * element_size, self._chunk, self, length=element_size + ) + acc.append("") + acc.append(element.string()) + acc.append("\n") + return "".join(acc) + + FixedArrayTypeNode.__name__ = f"{element_class.__name__}Array" + return FixedArrayTypeNode + + +# Array value types whose element has a fixed size: the array flag (0x80) or'd +# with the element's value type. +ARRAY_TYPES = { + 0x80 | value_type: _fixed_array_type_node(element_class, element_size) + for value_type, element_class, element_size in ( + (NODE_TYPES.SIGNED_BYTE, SignedByteTypeNode, 1), + (NODE_TYPES.UNSIGNED_BYTE, UnsignedByteTypeNode, 1), + (NODE_TYPES.SIGNED_WORD, SignedWordTypeNode, 2), + (NODE_TYPES.UNSIGNED_WORD, UnsignedWordTypeNode, 2), + (NODE_TYPES.SIGNED_DWORD, SignedDwordTypeNode, 4), + (NODE_TYPES.UNSIGNED_DWORD, UnsignedDwordTypeNode, 4), + (NODE_TYPES.SIGNED_QWORD, SignedQwordTypeNode, 8), + (NODE_TYPES.UNSIGNED_QWORD, UnsignedQwordTypeNode, 8), + (NODE_TYPES.FLOAT, FloatTypeNode, 4), + (NODE_TYPES.DOUBLE, DoubleTypeNode, 8), + (NODE_TYPES.GUID, GuidTypeNode, 16), + (NODE_TYPES.FILETIME, FiletimeTypeNode, 8), + (NODE_TYPES.SYSTEMTIME, SystemtimeTypeNode, 16), + (NODE_TYPES.HEX32, Hex32TypeNode, 4), + (NODE_TYPES.HEX64, Hex64TypeNode, 8), + ) +} diff --git a/tests/test_array_types.py b/tests/test_array_types.py new file mode 100644 index 0000000..9a94a34 --- /dev/null +++ b/tests/test_array_types.py @@ -0,0 +1,36 @@ +import struct + +import pytest + +import Evtx.Nodes as e_nodes + + +def value(type_, data, length=None): + return e_nodes.get_variant_value(data, 0, None, None, type_, length=len(data) if length is None else length) + + +def test_unsigned_qword_array(): + node = value(0x8A, struct.pack("<3Q", 1, 2, 2**64 - 1)) + assert node.string() == "1\n2\n18446744073709551615\n" + assert node.length() == 24 + + +def test_other_fixed_width_arrays(): + assert value(0x84, bytes([0xFF, 0x01])).string() == "255\n1\n" + assert value(0x83, bytes([0xFF, 0x01])).string() == "-1\n1\n" + assert value(0x88, struct.pack("<2I", 9, 10)).string() == "9\n10\n" + assert value(0x95, struct.pack("0x0706050403020100\n" + + +def test_empty_array(): + assert value(0x8A, b"").string() == "" + + +def test_partial_element_raises(): + with pytest.raises(e_nodes.ParseException): + value(0x8A, bytes(12)) + + +def test_unknown_type_raises_not_implemented(): + with pytest.raises(NotImplementedError): + value(0x93, bytes(8))