Skip to content

Minidump gives every module an abstract Section, so CFGFast raises NotImplementedError on any dump with a loaded module #749

Description

@zardus

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Minidump.__init__ builds its regions from the two base classes in cle/backends/region.py directly: one Segment per dumped memory range (cle/backends/minidump/__init__.py:62) and one Section per loaded module (:72).

Neither base class can describe permissions. Section.is_readable, .is_writable, .is_executable and .only_contains_uninitialized_data all raise NotImplementedError(), and Segment inherits Region, which answers True to all three unconditionally — the same defect reported in #743.

The dump loads fine. Anything that then asks where code lives does not, because CFGBase._executable_memory_regions prefers sections when an object has them and reads section.is_executable (angr/analyses/cfg/cfg_base.py:870). The fixture already in angr/binaries is enough:

import angr, cle

ld = cle.Loader("binaries/tests/x86/windows/jusched_x86.dmp", auto_load_libs=False)
ld.main_object.sections[0].is_executable   # NotImplementedError
ld.main_object.segments[0].is_executable   # True, for every one of the 173 segments

angr.Project("binaries/tests/x86/windows/jusched_x86.dmp", auto_load_libs=False).analyses.CFGFast()
# NotImplementedError, from cle/backends/region.py:190, before CFGFast does any work

So CFGFast cannot run on any minidump that has at least one loaded module. In a sweep of a large object corpus, 1,879 of 1,969 minidump observations end this way; the other 90 fail earlier for unrelated reasons. All 1,859 distinct dumps behind those observations reproduce it on current master.

Fixing this is not a matter of picking a subclass, because the dump does not always carry the answer.

The permission stream is optional. MINIDUMP_MEMORY_INFO_LIST holds a real PAGE_* value per range, but MiniDumpWriteDump writes it only when passed MiniDumpWithFullMemoryInfo. jusched_x86.dmp has it (251 entries: 85 PAGE_READONLY, 55 PAGE_READWRITE, 52 PAGE_NOACCESS, 33 PAGE_EXECUTE_READ), so a fix tested against the fixture alone will look complete. Of the 1,859 dumps above, 1,857 have no such stream. That number needs a caveat: 1,856 of them come from one generating harness, so it measures that harness more than it measures the world. The corpus holds only three dumps captured from real crashes, and one of those three also has no MemoryInfoListStream, which is enough to say the absent case is not hypothetical.

A Section here is a whole loaded module — a mapped PE image with r-x text beside r-- and rw- data. One permission triple cannot describe it honestly whichever value it takes.

The Segments are the regions that could carry per-range permissions, and they are the ones currently claiming rwx. Making the sections answer without touching the segments leaves that in place; removing the sections so the segments are consulted turns every dumped heap, stack and guard page into executable memory, which is #743 in a second backend.

Options, none of which is obviously the intended one:

  1. Give Segment real permissions from MemoryInfoListStream and drop the per-module Sections, accepting that dumps without that stream have no executable map at all. CFGFast then recovers nothing from them rather than crashing, which is quieter but not better.
  2. Keep the per-module Sections and read each module's own section table out of the dumped image. The module headers are mapped: MZ is present at module.baseaddress for every module of every dump checked, including the ones with no MemoryInfoListStream. This gives per-section permissions from the module itself rather than from the optional stream, at the cost of parsing a child PE per module.
  3. Let a Section report that it does not know, and have CFGBase._executable_memory_regions fall back to the segments in that case. This needs the "unknown" state that ELFCore drops the permissions of every core segment it turns into a Blob #743 also asks for.

#682 is open against this and takes option 1. As written it does not survive a dump with no MemoryInfoListStream: DumpSection is then constructed with protect = 0, and is_readable evaluates self.protect.value, so NotImplementedError becomes AttributeError: 'int' object has no attribute 'value'. Loading all 1,859 dumps on that branch and querying the permissions of every section, 1,857 raise AttributeError and the 2 with the stream answer. It also splits each module into one section per overlapping segment, taking the fixture from 30 sections to 127 and breaking tests/test_minidump.py, which is the objection already raised on it.

The TE and UEFI backends had the same crash from the same base class and were fixed in #713 by giving TESection permissions from the PE/COFF section characteristics. That worked because a TE image carries per-section permissions. A minidump often does not, which is why this one is a report rather than the same patch again.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions