From 8532eb03de132ce419d591eed582c158158b7165 Mon Sep 17 00:00:00 2001 From: Brigs Date: Sat, 3 Oct 2026 09:57:03 -0400 Subject: [PATCH] Render fixed-width array values get_variant_value only knows the UTF-16 string array (0x81). Any other array type raised KeyError, which the except IndexError around the lookup never caught, so the whole record failed to render. Add the arrays whose elements have a fixed size: 8 to 64-bit integers, float and double, GUID, FILETIME, SYSTEMTIME and hex 32/64. Each element is rendered by the existing node for its type, and the array comes out as pieces like WstringArrayTypeNode. The lookup now catches KeyError. --- Evtx/Nodes.py | 62 ++++++++++++++++++++++++++++++++++++++- tests/test_array_types.py | 36 +++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/test_array_types.py 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))