Skip to content

feat: introduce \ooxml module for inspecting OOXML documents - #803

Open
RustyNoob-619 wants to merge 3 commits into
VirusTotal:mainfrom
RustyNoob-619:feat/ooxml-module
Open

RustyNoob-619 wants to merge 3 commits into
VirusTotal:mainfrom
RustyNoob-619:feat/ooxml-module

Conversation

@RustyNoob-619

Copy link
Copy Markdown

feat: introduce ooxml module for inspecting OOXML documents

This PR adds an ooxml module that parses the ZIP central directory of Office
Open XML documents (DOCX, XLSX, PPTX) and other ZIP archives, and exposes the
metadata of every entry. It's a port of an OOXML module I wrote for YARA 4.x
(RustyNoob-619/YARA — Modules/OOXML).

Like olecf, vba and msi, it is experimental and not in the default
modules
; it's enabled with the ooxml-module feature.

Why

Weaponised Office documents remain one of the most common initial-access
vectors. Besides content, the ZIP metadata of a document carries a fingerprint
of the tool that built it: MS-DOS timestamps, host OS and ZIP version in
"version made by", flags, CRC-32s, entry ordering. These are useful for
clustering documents produced by the same builder, and for spotting
inconsistencies (e.g. on-disk vs. total entry counts, odd compression
methods, archive comments).

What it exposes

  • is_ooxml (bool): true if one of the entries is [Content_Types].xml, false
    for other ZIP archives, undefined if the file isn't a ZIP archive.
  • number_of_on_disk_entries, number_of_total_entries, central_dir_size,
    central_dir_offset, zip_comment_len, zip_comment_str.
  • entries[]: name_string, name_length, compressed_size,
    uncompressed_size, crc32_checksum, compression_method_value,
    compression_method_name, mod_time_raw, mod_date_raw, flags,
    version_needed, version_made_by, os_name, spec_version.

Relationship with the zip module

The existing zip module exposes each entry's path, compression method and
sizes. ooxml focuses on the central directory metadata that zip doesn't
expose (CRC-32, timestamps, flags, ZIP versions, host OS, entry counts, central
directory location, archive comment) and on recognising OOXML documents. If
you'd prefer these fields to be added to zip instead of living in a separate
module, I'm happy to rework the PR that way.

Behaviour

  • Only files starting with a local file header (PK\x03\x04) are parsed, as
    in the original module.
  • The end of central directory record is searched backwards from the end of
    the file and accepted only if its comment ends exactly at the end of the
    file; comments up to the maximum 65,535 bytes are supported.
  • Parsing stops at the first malformed central directory header; entries
    parsed until then are kept, so number_of_total_entries may exceed the
    number of entries.
  • Entry names are exposed as raw bytes (they aren't guaranteed to be UTF-8).
  • Nothing is decompressed, and all reads are bounds-checked.

Files

Following the layout of the zip module commit (e050078):

  • lib/src/modules/protos/ooxml.proto (with field comments for
    field_docs.rs)
  • lib/src/modules/ooxml/mod.rs
  • lib/src/modules/ooxml/tests/mod.rs and tests/testdata/<sha256>.in.zip /
    <sha256>.out, prepared as described in the Module Developer's Guide
    (objcopy -I binary -O ihex, then zip); all inputs are synthetic and
    benign, and each is described next to its hash in tests/mod.rs
  • lib/Cargo.toml: ooxml-module feature, listed as experimental in
    default-modules
  • Generated: modules.rs, protos/generated/{mod.rs,ooxml.rs}, and the
    ooxml entries in field_docs.rs (only those; I didn't regenerate the
    whole file); one line in add_modules.rs, as done for msi.
  • Second commit: site/content/docs/modules/ooxml.md. Happy to drop it while
    the module is experimental.
  • Third commit: yr dump support (yr dump --module ooxml), mirroring the
    commit that added zip to dump (3bbe876): mods.proto,
    lib/src/modules/mod.rs (re-exports and invoke_all),
    cli/src/commands/dump.rs, and the regenerated protos/generated/mods.rs.
    Without the ooxml-module feature, dump --module ooxml prints nothing,
    as with the other optional modules.

Testing

  • cargo test -p yara-x --features ooxml-module --lib: 340 passed (6 new rule
    tests plus the golden-file test over the new testdata).
  • cargo test -p yara-x --lib (default features): 334 passed; default builds
    are unaffected.
  • cargo test -p yara-x-cli --bin yr --features yara-x/ooxml-module
    (with CARGO_BIN_EXE_yr set): 56 passed.
  • cargo fmt --all --check and
    cargo clippy -p yara-x -p yara-x-cli --features yara-x/ooxml-module --tests --no-deps -- --deny clippy::all:
    clean.
  • Parity with the original YARA 4.x module (with two bug fixes applied to it):
    identical module output on 50 files (real DOCX/XLSX/PPTX, plain and crafted
    ZIPs, truncated archives, ZIP64, non-ZIP files — 923,608 values) and
    identical rule results on 28 rules; the only difference is that this
    version also handles a maximum-length (65,535-byte) archive comment, which
    the original missed.

The new `ooxml` module parses the ZIP end of central directory record and
the central directory file headers of Office Open XML documents (DOCX, XLSX,
PPTX) and other ZIP archives. It exposes whether the file is an OOXML
document (it has a `[Content_Types].xml` entry), the entry counts, the
central directory size and offset, the archive comment, and for every entry
its name, sizes, CRC-32, compression method, MS-DOS timestamps, flags, the
ZIP version needed, and the host operating system and ZIP version that
created it. Entries are not decompressed.

The per-entry metadata reflects the tool that produced a document, which is
useful for clustering malicious documents created by the same builder.

The module is a port of an OOXML module for YARA 4.x. It is experimental and
not part of the default modules; enable it with the `ooxml-module` feature.
// ZIP file comment. Undefined if the archive has no comment.
optional bytes zip_comment_str = 7;
// Entries in the central directory.
repeated Entry entries = 8;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the fields in each Entry seem to belong to the ZIP format actually, and should be exposed by the zip module. In fact, the zip modules already exposes some information about each entry, like the compressed size and compression method. Duplicating that information in two modules doesn't make sense, let's put everyting belonging to the ZIP format in the zip module, and use the ooxml module for putting any information that can be obtained from the word/document.xml file or any other file containing metadata about the document.

In fact, it looks like all the information exposed by this module is actually ZIP-related stuff.

@RustyNoob-619 RustyNoob-619 Oct 9, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that does make a whole lot of sense, when I first started working on this module it was intended towards detecting malicious docs conforming to the ooxml format but at the end of the day, they are zip files. I did look into the zip module and it does seem to be limited, it would be nice to add the exposed fields from here and merge it in the modules, so totally agree with your thoughts.

For the ooxml, as you suggested we could rework it to expose document metadata only, using the existing ZIP cache such as docProps/core.xml and app.xml properties, the document type from [Content_Types].xml, and relationships with external targets (e.g. remote templates). To the best of my knowledge, the dependencies are already part of other modules and we could have something like ooxml-module = ["dep:tinyzip", "dep:flate2", "dep:roxmltree"]

So happy for this PR to be merged into the zip module (I can raise a new one for the zip module and conform with its structure/fields) and can create a new separate PR for the ooxml module, once it is ready

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants