fix(native): bound PE resource RVAs by section virtual size (#1678)
The PE resource reader treated each section as covering `max(VirtualSize, SizeOfRawData)` bytes, the mapping #1596 removed from managed RVA reads. A resource payload in the raw alignment padding past VirtualSize was reported with a hash of bytes the image loader never maps. A later section whose virtual range starts inside that padding was refused as an overlapping mapping. Containment and overlap now use the loader's virtual extent: VirtualSize, or SizeOfRawData when VirtualSize is zero. Ranges must still be file-backed. Refs #1596 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3749ccec47
commit
38221cb12e
|
|
@ -28,6 +28,11 @@ interface Section {
|
|||
readonly size: number;
|
||||
}
|
||||
|
||||
// The image loader maps VirtualSize bytes, using SizeOfRawData only when
|
||||
// VirtualSize is zero. Raw bytes past that extent are file-alignment padding.
|
||||
const virtualExtent = (section: Section): number =>
|
||||
section.virtualSize === 0 ? section.size : section.virtualSize;
|
||||
|
||||
/** Header admission and unambiguous file-backed RVA mapping, independent of section names. */
|
||||
export const readPeResourceLayout = (bytes: Buffer) => {
|
||||
requirePeRange(bytes, 0, 64);
|
||||
|
|
@ -64,7 +69,7 @@ export const readPeResourceLayout = (bytes: Buffer) => {
|
|||
offset: bytes.readUInt32LE(at + 20),
|
||||
size: bytes.readUInt32LE(at + 16),
|
||||
};
|
||||
if (section.rva + Math.max(section.virtualSize, section.size) > 0x100000000)
|
||||
if (section.rva + virtualExtent(section) > 0x100000000)
|
||||
peFailure("PE section RVA range overflows.");
|
||||
if (section.size > 0) {
|
||||
requirePeRange(bytes, section.offset, section.size);
|
||||
|
|
@ -90,15 +95,14 @@ export const readPeResourceLayout = (bytes: Buffer) => {
|
|||
if (rva + size > 0x100000000) peFailure("Resource RVA range overflows.");
|
||||
const candidates = sections.filter(
|
||||
(section) =>
|
||||
rva >= section.rva &&
|
||||
rva - section.rva < Math.max(section.size, section.virtualSize),
|
||||
rva >= section.rva && rva - section.rva < virtualExtent(section),
|
||||
);
|
||||
if (rva < headerSize) {
|
||||
if (
|
||||
sections.some(
|
||||
(section) =>
|
||||
section.rva < rva + size &&
|
||||
section.rva + Math.max(section.size, section.virtualSize) > rva,
|
||||
section.rva + virtualExtent(section) > rva,
|
||||
) ||
|
||||
size > headerSize - rva
|
||||
)
|
||||
|
|
@ -111,6 +115,8 @@ export const readPeResourceLayout = (bytes: Buffer) => {
|
|||
const section = candidates[0];
|
||||
if (section === undefined) return peFailure("Missing resource section.");
|
||||
const within = rva - section.rva;
|
||||
if (size > virtualExtent(section) - within)
|
||||
peFailure("Resource range leaves its section's virtual size.");
|
||||
if (within > section.size || size > section.size - within)
|
||||
peFailure("Resource range is not entirely file-backed in one section.");
|
||||
// A second section starting inside the requested interval is ambiguous too.
|
||||
|
|
@ -119,7 +125,7 @@ export const readPeResourceLayout = (bytes: Buffer) => {
|
|||
(other) =>
|
||||
other !== section &&
|
||||
other.rva < rva + size &&
|
||||
other.rva + Math.max(other.size, other.virtualSize) > rva,
|
||||
other.rva + virtualExtent(other) > rva,
|
||||
)
|
||||
)
|
||||
peFailure("Resource interval intersects overlapping virtual sections.");
|
||||
|
|
|
|||
|
|
@ -259,3 +259,62 @@ describe("PE header admission and bounded traversal", () => {
|
|||
await expect(parsing).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PE resource section virtual extent", () => {
|
||||
const sectionSizes = (fixture: ReturnType<typeof peResourceFixture>) => {
|
||||
const virtualSize = fixture.bytes.readUInt32LE(fixture.sectionAt + 8);
|
||||
const rawSize = fixture.bytes.readUInt32LE(fixture.sectionAt + 16);
|
||||
expect(rawSize - virtualSize).toBeGreaterThanOrEqual(8);
|
||||
return { virtualSize, rawSize };
|
||||
};
|
||||
|
||||
it.each([
|
||||
[0, "Resource RVA has missing or overlapping section mappings."],
|
||||
[-2, "Resource range leaves its section's virtual size."],
|
||||
])(
|
||||
"rejects a payload reaching raw alignment padding past VirtualSize (start %i)",
|
||||
async (start, message) => {
|
||||
const fixture = peResourceFixture();
|
||||
const { virtualSize } = sectionSizes(fixture);
|
||||
const rva = 0x2000 + virtualSize + start;
|
||||
fixture.bytes.writeUInt32LE(rva, fixture.dataOffsets[0]);
|
||||
fixture.bytes.writeUInt32LE(4, (fixture.dataOffsets[0] ?? 0) + 4);
|
||||
await expect(parse(fixture.bytes)).rejects.toMatchObject({
|
||||
reason: "format",
|
||||
message,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("maps raw bytes when VirtualSize is zero, as the image loader does", async () => {
|
||||
const fixture = peResourceFixture();
|
||||
fixture.bytes.writeUInt32LE(0, fixture.sectionAt + 8);
|
||||
expect(await parse(fixture.bytes)).toMatchObject({
|
||||
coverage: { status: "complete", resources: 5 },
|
||||
});
|
||||
});
|
||||
|
||||
it("selects a later section that starts inside earlier raw padding", async () => {
|
||||
const fixture = peResourceFixture();
|
||||
const { virtualSize } = sectionSizes(fixture);
|
||||
const laterRva = 0x2000 + Math.ceil(virtualSize / 4) * 4;
|
||||
const rawOffset = fixture.bytes.length;
|
||||
const payload = Buffer.from("LATE");
|
||||
const bytes = Buffer.concat([fixture.bytes, payload]);
|
||||
bytes.writeUInt16LE(2, 0x86);
|
||||
const later = fixture.sectionAt + 40;
|
||||
bytes.writeUInt32LE(payload.length, later + 8);
|
||||
bytes.writeUInt32LE(laterRva, later + 12);
|
||||
bytes.writeUInt32LE(payload.length, later + 16);
|
||||
bytes.writeUInt32LE(rawOffset, later + 20);
|
||||
bytes.writeUInt32LE(laterRva, fixture.dataOffsets[0]);
|
||||
bytes.writeUInt32LE(payload.length, (fixture.dataOffsets[0] ?? 0) + 4);
|
||||
|
||||
const result = peResourcesSchema.parse(await parse(bytes));
|
||||
expect(result.resources[0]?.payload).toMatchObject({
|
||||
rva: laterRva,
|
||||
location: { offset: rawOffset, bytes: payload.length },
|
||||
sha256: createHash("sha256").update(payload).digest("hex"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue