Skip to content

v4.9.0

Choose a tag to compare

@github-actions github-actions released this 28 Sep 18:25
· 46 commits to master since this release
2247ee6

Added

  • smda.export: the engine that turns a disassembler frontend's analysis into a report, and
    Disassembler.setExporter(exporter) to pin one, which ida_domain_export.py and downstream
    callers used to do through a private flag. (#360)

  • (binja) Export Binary Ninja's analysis as a SMDA report, the way smda.ida does for IDA.
    smda.binja.BinjaInterface reads functions, blocks, edges, symbols and imports from a
    BinaryView and feeds smda.export.Exporter, smda.binja.BinjaExporter.exportBinaryView(bv)
    wraps the two, and the scripts match the IDA ones: binja_export.py exports the open view from
    Run Script... or a binary or .bndb headlessly, and binja_analyze.py adds SMDA's recovered
    functions and names to the open view as one undo step. Ported from the MCRIT plugin, where the
    exporter was versioned by the plugin's releases instead of the report format it produces. No new
    dependency: Binary Ninja's API comes with the application, and the module imports without it.
    Measured with Binary Ninja 6.0 on an x64 PE DLL, two x86_64 Mach-O binaries and /bin/ls:
    reports identical to the plugin's exporter except oep, which ELF and Mach-O reports now record
    relative to the base address as native SMDA reports and IDA exports do (the plugin recorded an
    absolute address; PE entry points were already RVAs). Not reproducible from the repository:
    none of the binaries is bundled and the comparison needs a licensed Binary Ninja. No cost to the
    disassembly backends, which it does not touch. (#357)

Changed

  • (cli) analyze.py detects container formats on its own, so -p/--parse_header is no longer
    needed to get a PE/ELF/Mach-O/Delphi-KB/DEX file mapped and its symbols parsed. Routing is decided
    by shouldParseHeader(), which asks every loader in FileLoader.file_loaders whether it claims
    the bytes -- the same list FileLoader dispatches on, not a second copy of the magic. An explicit
    -a/--base_addr or -i/--oep still selects raw buffer mode, because a dump of a mapped image
    begins with the header it was mapped from and the caller naming its base address is describing
    exactly that -- and so does a base address in the file name, the _0x<addr> form
    parseBaseAddrFromArgs() already read, since a memory-dumped PE still starts with MZ and would
    otherwise be mapped a second time; -p keeps working and overrides all of them. DEX is unchanged
    in output: routed through disassembleFile the blockblast fixture yields the same 2,219
    functions / 2,527 blocks / 9,824 instructions at base 0 as the buffer path. Detection needs the bytes, so disassembleFile() and
    FileLoader take an optional buffer the caller has already read and skip reading the file a
    second time; omitting it loads from the path as before.

  • (build) Correct the package summary and describe the package to PyPI properly. The summary
    shipped disassmbler in every release's metadata, which is also the string PyPI's search index
    matches on. The classifier list gained Typing :: Typed -- py.typed has shipped since the
    package-data entry was added, with nothing advertising it -- plus the two Intended Audience
    entries, Programming Language :: Python :: 3 :: Only, and a keywords list, none of which
    PyPI could infer. No code, no runtime dependency and no report field moves.

  • (build) Stop shipping tests/ in the sdist. setuptools' default rules picked up every
    .py file under the root but not the extension-less xored corpora they read, so the published
    sdist carried 1.6 MB of a test suite that could not run from it -- and the corpora are live
    malware, which is not something to publish to an index either. A MANIFEST.in prunes it; the
    sdist goes 3.12 MB -> 1.49 MB and the wheel is unaffected. Run the suite from a clone.

  • The export engine moved out of smda.ida into smda.export. Exporter(config, interface) builds
    the report from any BackendInterface, and BackendInterface now declares exactly the methods
    the engine reads: getApiMap and isExternalFunction added, the never-called getApiOffsets
    dropped. The engine was IDA-only in name: the interface it reads has had two IDA implementations
    since the IDA Domain backend, and nothing in it touches IDA, so a second frontend should not
    import it from the first. smda.ida.IdaExporter and smda.ida.BackendInterface remain as
    before, so no caller changes; the engine's code moved verbatim. (#360)

  • (ci) Pull requests only start the jobs their diff can move. A Changed paths job reads
    the PR file list and gates the rest: the MCRIT install matrix needs a packaging change, lint
    and tests skip a documentation-only PR, the two security audits need a dependency or .github/
    change, the benchmark workflow triggers only on files that can move output or timing and skips
    Malpedia for changes confined to the AArch64, CIL or Dalvik backends, and each fuzz target runs
    only when its region of the tree was touched. push, schedule and workflow_dispatch still
    run every job. (#359)

Fixed

  • (report) Refuse an ELF or Mach-O synthesis whose section span exceeds SmdaConfig.MAX_IMAGE_SIZE, instead
    of allocating it. _resolveFunctionOffsets bounded the span from the first function offset to the last
    function's extent end, but _syntheticSpan also stretches the section up to the highest function offset. A
    report whose function offset lies far above its own blocks therefore passed the check and asked _nopFill for
    gigabytes. fuzz_synthesis found it on master after #371 merged, though synthesis has not changed since 4.8.0.
    _syntheticSpan now raises ValueError over the limit, the same operational error the offset check raises. PE
    synthesis, which does not use it, still builds such a report. Reproduced in tests/testSynthesis.py: a report
    with one function 0x90000000 above its blocks raises MemoryError under a 4 GiB cap before this change, and
    ValueError without allocating after it. (#376)

  • (ci) Classify a large pull request's file list correctly. Every path filter piped the list into
    grep -q under set -o pipefail; grep -q exits on its first match, printf then dies of SIGPIPE once the
    list outgrows the 64 KB pipe buffer, and the pipeline's exit status reads as "no match". A PR of about 1,500
    paths or more could therefore skip lint, tests, the audits, the benchmark and fuzzing and still show green, and
    pass the changelog check without an entry. The filters now read the list from a here-string. On the same
    change, CHANGELOG.md no longer counts as documentation for the lint/test gate (the release guard tests parse
    it), data/ counts as benchmark input (run_perf_check.py loads the ApiScout database from it), and the MCRIT
    install matrix runs when ci.yml itself changes. Measured by running each filter's step script locally against
    a stubbed file list:
    at 3,000 paths the old filters gave the wrong verdict in every workflow and the new ones
    the right one; on small lists both agree except for the three filter changes above. (#370)

  • (binja) Cap the image BinjaInterface.getBinary() assembles at SmdaConfig.MAX_IMAGE_SIZE, as the IDA
    frontends already do. It allocated from the lowest segment to the highest segment end in one piece, so a view
    whose segments lie far apart raised MemoryError instead of exporting. Both frontends now share
    assembleSegmentBuffer, which moved from smda.ida.segment_mapping to smda.export.segment_mapping (the old
    module re-exports it); its truncation warning no longer names IDA. No other change to Binary Ninja output: a
    segment's unbacked tail past data_length is still zero-filled and still counts toward the image size. (#371)

  • (report) Apply SmdaConfig.MAX_IMAGE_SIZE when a report is written, not only when it is read. Since #350
    fromDict() refuses to inflate a stored buffer larger than the limit, but toDict() still packed any buffer, so
    a dump over 100 MiB analysed with STORE_BUFFER=True wrote a buffer that its own reload dropped. toDict() now
    leaves out a buffer over the limit and logs a warning, so a written report loads back with everything it
    contains; the in-memory report keeps its buffer. No cost for buffers within the limit, which serialize as
    before. Nothing in the MCRIT family reads a stored buffer (searched 2026-09-28). (#372)

  • (cli) Keep shellcode that starts with MZ in raw buffer mode. analyze.py's container detection took any
    loader's isCompatible() as its answer, and PeFileLoader's checks only the two MZ bytes, so MZ-prefixed
    shellcode went down the PE header path and could come back as an empty report. For PE, detection now also needs
    the PE\0\0 signature at e_lfanew; isCompatible() itself, which FileLoader dispatches on, is unchanged, and
    -p still forces header parsing. No cost on well-formed PE files, which carry the signature (cutwail fixture). (#373)

  • Disassembler(backend="IDA").disassembleFile() no longer fails on the unconditional
    addPdbFile call; the export engine now carries the same no-op the CIL and Dalvik backends have.
    (#360)

Security

  • (report) Bound stored report-buffer inflation by SmdaConfig.MAX_IMAGE_SIZE and reject non-native ZIP
    layouts before materializing their contents, so a crafted serialized report cannot trigger unbounded
    decompression while the remainder of a malformed report stays loadable. (#350)

What's Changed

Changes

Dependency and tooling updates

Full Changelog: v4.8.0...v4.9.0