Files

74 lines
3.0 KiB
Python

"""Read a Volatility layer without filling missing evidence with zero bytes."""
from collections.abc import Iterator
from volatility3.framework import exceptions
from volatility3.framework.layers.intel import Intel
from mcptrace.core import ScanStats
def readable_chunks(
layer,
start: int,
size: int,
stats: ScanStats,
invalid_address: type[Exception],
chunk_bytes: int = 1024 * 1024,
) -> Iterator[tuple[int, bytes]]:
if size < 0 or start < 0 or chunk_bytes < 4096:
raise ValueError("Invalid virtual range or read chunk size")
end = start + size
offset = start
while offset < end:
amount = min(chunk_bytes, end - offset)
next_offset = offset + amount
try:
data = layer.read(offset, amount, pad=False)
if len(data) != amount:
raise invalid_address(layer.name, offset, "Short read")
except invalid_address:
# One absent page must not discard other resident pages in this chunk.
page = offset
run_start = page
run = bytearray()
while page < offset + amount:
page_amount = min(4096 - page % 4096, offset + amount - page)
advance = page_amount
try:
part = layer.read(page, page_amount, pad=False)
if len(part) != page_amount:
raise invalid_address(layer.name, page, "Short read")
except invalid_address as exc:
if run:
yield run_start, bytes(run)
run.clear()
# Intel faults identify the size of an absent page-table
# branch. Skip that branch, possibly beyond this read chunk,
# but never beyond the caller's VAD/VMA range or byte budget.
# Do not apply this to physical backing failures: part of a
# large mapped page can still be readable in a truncated dump.
if (
isinstance(layer, Intel)
and isinstance(exc, exceptions.PagedInvalidAddressException)
and exc.layer_name == layer.name
and page <= exc.invalid_address < page + page_amount
and 12 <= exc.invalid_bits <= 48
):
block_size = 1 << exc.invalid_bits
advance = min(end - page, block_size - page % block_size)
stats.unreadable_bytes += advance
else:
if not run:
run_start = page
run.extend(part)
stats.bytes_read += len(part)
page += advance
if run:
yield run_start, bytes(run)
next_offset = max(next_offset, page)
else:
stats.bytes_read += len(data)
yield offset, data
offset = next_offset