PE: Map a section over its raw size when that is larger than its virtual size - #835
Conversation
PESection reported a section's mapped size as Misc_VirtualSize alone. Windows copies a section's raw data to its virtual address, so a section whose SizeOfRawData exceeds its Misc_VirtualSize -- or which declares a virtual size of zero, which the PE specification permits -- is mapped over the raw size. Take the larger of the two, bounding the raw size to the bytes the file actually holds and to the end of the image the optional header declares. Both bounds only ever hold the result down toward Misc_VirtualSize, so no section comes out smaller than it did. cle#285 fixed half of this in 6cf00b4, which gave PESection a filesize of its own and left the memsize argument alone. Where the shortfall covered the entry point, the binary analysed to nothing: an entry outside every section's memsize is inside no executable memory region, so CFGFast refused every address in the object.
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head Tests
The third row is the check that the new tests are regression tests: they fail without the change. Lint and type
Section geometry over every PE Both arms are separate nix store builds of the two trees; the dump records every section's The dump asserts every section's The newly executable bytes are the padding between a section's virtual size and its file-aligned CFG regression control over the same 106 files
The four gains are not recoveries and should not be read as a win. Each starts nine bytes before Corpus this fixes The corpus this fixes is private and cannot be named. Both arms,
Workspace gate Run after publication, on this exact head. It matters here because
Gaps
|
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Before and after on the reproducer, and the section geometry on a public file. Before — the section stops at its virtual size, the 14 MiB cle master 0e77adeThe image that appends 14 MiB to itself and counts it in On the three private binaries whose entry point falls in the shortfall: After — with this changeA fourth private sample already recovered 1 function and now recovers 4. Its
The 0x31 bytes |
|
Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_835 |
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
PESectionreports a section's mapped size asMisc_VirtualSizealone. Windows maps a sectionover its raw data as well, so a section whose
SizeOfRawDataexceeds itsMisc_VirtualSizeisshort in cle unless the file or the image runs out first. It is not a corner case: over the 106 PE
files
angr/binariestracks atfc07821c, this change grows 706 sections in 101 of them.tests/x86_64/windows/fauxware.exeis the plain version..textstatesVirtualSize 0x7dcfandSizeOfRawData 0x7e00, and cle stops it at0x7dcf:The 0x31 bytes it drops are in the file and already in cle's memory. Here they are zero padding,
which is the ordinary case.
Where the shortfall covers the entry point, the binary loads and analyses to nothing.
CFGBase._executable_memory_regionsbuilds its regions for PE and COFF fromsection.min_addrand
min(section.memsize, section.filesize), so an entry outside every section'smemsizeis in noexecutable region,
CFGFastrefuses every address, and the result is 0 functions and 0 nodes.A section that declares
VirtualSize0 — which the PE specification permits, and which Windowshandles by using the raw size — gets
memsize0 and loses its whole content this way.Root cause
cle/backends/pe/regions.py,PESection.__init__:Section.__init__uses that value formemsize, andRegion.contains_addrisvaddr <= addr < vaddr + memsize, which is whatfind_section_containingwalks.cle#285 reported the same geometry in 2021 and
6cf00b4bfixed half of it: it gavePESectionafilesizeof its own, taken fromSizeOfRawData, and left thememsizeargument passed toSection.__init__asMisc_VirtualSize. That half has not been revisited since.Fix
_mapped_sizetakes the larger of the virtual size and the raw size, and bounds the raw sizetwice: to the bytes the file actually holds, and to the end of the image the optional header
declares. Both bounds only ever hold the result down toward
Misc_VirtualSize, so no sectioncomes out smaller than it does today.
The image bound matters on real files:
tests/i386/windows/aa893de523f58ee14972b94fef7ecdbb930cbdc700d8be097eb8a6de2549ce73appends14 MiB to itself and counts it in
.reloc'sSizeOfRawData, which reaches0xe25000against aSizeOfImageof0x4f02e. Without the bound that object's span would grow from0x4f02eto0xe72000; with it,.relocstays at0x202e.max_addrfollowsmemsize, so the tailpe.pykeeps of the mapped image grows on 97 of the106: more bytes retained, never fewer, and the shortfall between an object's claimed span and its
backed bytes is unchanged object for object. Two of our open items are in the same code. cle#790
backs the last byte and the last section's virtual-size tail; with both changes applied, 105 of
the 106 tracked files come out with no shortfall at all, and the one that does not is cle#794's,
which is about a section whose raw data begins past the end of the file. The two compose and
neither needs the other.
Deliberately not done:
filesizestill comes fromSizeOfRawData; the section is not rounded upto
SectionAlignment; and a section is not clamped to the next section's virtual address. Thatlast is left out on a measurement: across both corpora no section reaches the clamp, so it would
be an untested branch.
Testing
tests/test_pe.py::TestPESectionMappedSize, two methods on filesangr/binariesalready tracks.The first asserts
fauxware.exe's.textmaps0x7e00while.data, which states the reverse(
VirtualSize 0xa19againstSizeOfRawData 0x400), keeps its virtual size. The second assertsboth halves of the rule on the 14 MiB object above. Both fail on master and pass here. The
pre-existing
test_loading_incomplete_pe_filecovers the file bound: an earlier version of thischange without that bound failed it.
CFGFast(normalize=False)over all 106 tracked PE files, both arms: nothing is lost — noobject loses a function or a node — and four gain one function each, 244,228 -> 244,232. Those
four are not recoveries. Each starts nine bytes before the old end of
.text, in bytes masteralready had, and runs into the zeros past it, so a block master could not finish now decodes to
its end: one
0xffand 197 zeros. The count that matters is the zero losses.The binaries whose CFG this recovers are in a private corpus and cannot be named here. Three go
from 0 functions to 125, 281 and 490. A fourth already recovered 1 function and now recovers 4,
and it answers whether this only ever maps padding: its
.textstatesVirtualSize 0xcb4againstSizeOfRawData 0xe00, and at0x401db0,0xfcbytes past the section as cle reports it, sitsmov eax, [esp+4]; sub esp, 0x12cwith four callee-saved pushes andpush 0x1f0fff(
PROCESS_ALL_ACCESS). That is a compiler-emitted prologue, and today it is outside everysection. No file in
angr/binariesshows the entry-point symptom — 105 of the 106 already havetheir entry inside a section — so the public evidence is the geometry and the control.
Validation: #835 (comment).
session: sharpen