ELF: repair only the header bytes that are loaded - #828
Conversation
When pyelftools cannot walk the section table, ELF.__init__ zeroes e_shoff, e_shentsize, e_shnum and e_shstrndx in a PatchedStream, rereads the file from the program headers alone, and puts the original bytes back into the loaded image at the end. It addressed them as min_addr + file_offset, which is the right address only when the lowest mapped segment sits at file offset 0. Nothing in ELF requires that, and nothing requires the header to be mapped at all. A linked image whose first PT_LOAD starts past the header maps none of those bytes, and the restore then writes the four fields over whatever really is mapped at that address. On tests/i386/bios.bin.truncated.elf, whose only PT_LOAD starts at file offset 0x70, six bytes of the loaded image differ from the file. These are file offsets, so offset_to_addr is what translates them, and a byte whose offset maps nowhere is not written at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Full loaded image of the only Before — four ELF header fields are written over the image, because the restore addresses them from angr/cle master 0e77adeAfter — the restore asks with this change |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head
Caveats, one line each:
|
|
Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_828 |
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
An ELF whose section header table pyelftools cannot walk, and whose lowest mapped segment does not start at file offset 0, loads with four fields of its own ELF header written over the image. On the new
tests/i386/bios.bin.truncated.elfsix bytes of the loaded image differ from the file:0x6474is that file's section header table offset, and40,3,2aree_shentsize,e_shnumande_shstrndxbeside it. They land at0xf9c30and0xf9c3ebecause the restore addresses them frommin_addr, and that image's onlyPT_LOADstarts at file offset0x70.Root cause
ELF.__init__probes the section table, and when the walk raises it zeroese_shoff,e_shentsize,e_shnumande_shstrndxin aPatchedStreamso pyelftools rereads the file from the program headers alone. Those zeroes reach memory with everything else the segments map, so the original bytes are stored back at the end of the constructor:offsetis a file offset into the ELF header --0x20and0x2efor a 32-bit ELF,0x28and0x3afor a 64-bit one.min_addr + offsetis that offset's address only when the lowest mapped segment sits at file offset 0. Nothing in ELF requires that, and nothing requires the header to be mapped at all.Fix
offset_to_addris what translates a file offset, so the restore asks it and skips a byte whose offset maps nowhere. Byte by byte, because the run can straddle a segment boundary.This also stops an object with no
PT_LOADat all from raising out of the constructor: on master such a file reachesClemory.storeon an empty memory and dies with aKeyErrornaming the patched offset, and it now loads with nothing mapped and nothing restored. No tracked object has that shape, so nothing here tests it.CGCsubclassesELF, so CGC binaries reach this loop too, and #726 repairs the same defect in the header that backend substitutes.NRFIN_00059from the DARPA CGC corpus is a public reproducer for the pair --lungetech/cgc-cfe-submission-corpusat6e28340, fileNRFIN_00059/2988916517-NRFIN_00059-5c96c811....rcb, whose firstPT_LOADstarts at file offset0x40. Master raises theTypeError#726 fixes and never loads it; this change alone leaves that sameTypeError; #726 alone loads it with nine bytes overwritten by this defect; with both, none. No tracked CGC fixture has that shape, and the case below needs neither #726 nor a download.Testing
tests/test_patched_stream.py::test_malformed_sections_unmapped_headerloads the new fixture.::test_malformed_sections, which already loadedtests/i386/oxfoo1m3, gains the same assertion for the case where the header is mapped. Both compare the whole loadedPT_LOADagainst the file's own bytes rather than the four fields, so they fail on a wrong-address write anywhere in the segment. The new test fails on the merge base and passes with the change; deleting the restore loop entirely makes theoxfoo1m3test fail with zeroes where the header bytes belong, so the loop is still load-bearing.The fixture is new because nothing already tracked reaches this path: of the 861 tracked
\x7fELFobjects inangr/binariesatfc07821, one takes the fallback --tests/i386/oxfoo1m3-- and it is unaffected, its onlyPT_LOADbeing at file offset 0. None of the 98 CGC fixtures takes it either. Merge angr/binaries#226 first. Validation: #828 (comment)sync: angr/binaries#226
🤖 Generated with Claude Code
session: sharpen