Skip to content

Commit 3ce5be7

Browse files
committed
fixes regarding the latest changes in the specification; add now the possibility to change the blake3 hash
1 parent f9e3dc1 commit 3ce5be7

14 files changed

Lines changed: 376 additions & 55 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# MemSlicer
22

33
[![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)](https://www.python.org/)
4-
[![Version](https://img.shields.io/badge/version-0.2.6-green)](pyproject.toml)
4+
[![Version](https://img.shields.io/badge/version-0.2.7-green)](pyproject.toml)
55

66
A memory acquisition tool that captures process memory snapshots into the MSL (Memory Slice) binary format. Supports multiple debugger backends (Frida, GDB, LLDB) and targets across Windows, Linux, macOS, Android, and iOS. Designed for forensic analysis, reverse engineering, and security research.
77

@@ -37,7 +37,7 @@ This installs memslicer with all backends (Frida, GDB, LLDB).
3737
### From Source
3838

3939
```bash
40-
git clone https://github.com/danielbaier/memslicer.git
40+
git clone git@github.com:MemorySlice/memslicer.git
4141
cd memslicer
4242
pip install -e .
4343
```
@@ -318,7 +318,7 @@ src/memslicer/
318318
### Setup
319319

320320
```bash
321-
git clone https://github.com/danielbaier/memslicer.git
321+
git clone git@github.com:MemorySlice/memslicer.git
322322
cd memslicer
323323
pip install -e ".[dev]"
324324
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "memslicer"
7-
version = "0.2.6"
7+
version = "0.2.7"
88
description = "MSL memory dump tool with pluggable debugger backends (Frida, GDB, LLDB)"
99
requires-python = ">= 3.10"
1010
license = "Apache-2.0"

src/memslicer/acquirer/build_id_post.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
"""
2222
from __future__ import annotations
2323

24-
import hashlib
2524
import logging
2625
from typing import Iterable, Protocol
2726

2827
from memslicer.acquirer.elf_notes import extract_build_id
29-
from memslicer.msl.constants import PageState
28+
from memslicer.msl.constants import HashAlgo, PageState
29+
from memslicer.msl.integrity import make_hasher
3030
from memslicer.msl.types import MemoryRegion, ModuleEntry
3131

3232

@@ -110,6 +110,7 @@ def populate_from_bridge(
110110
entries: list[ModuleEntry],
111111
bridge: BridgeProtocol,
112112
logger: logging.Logger | None = None,
113+
hash_algo: HashAlgo = HashAlgo.BLAKE3,
113114
) -> list[ModuleEntry]:
114115
"""Path A: live extraction via ``bridge.read_memory``.
115116
@@ -139,7 +140,7 @@ def populate_from_bridge(
139140
entry.path, entry.base_addr,
140141
)
141142
continue
142-
_apply_extraction(entry, data, source_id=SOURCE_BRIDGE, log=log)
143+
_apply_extraction(entry, data, source_id=SOURCE_BRIDGE, log=log, hash_algo=hash_algo)
143144
return entries
144145

145146

@@ -148,6 +149,7 @@ def populate_from_regions(
148149
captured_regions: Iterable[MemoryRegion],
149150
logger: logging.Logger | None = None,
150151
source_id: int = SOURCE_CAPTURED_REGION,
152+
hash_algo: HashAlgo = HashAlgo.BLAKE3,
151153
) -> list[ModuleEntry]:
152154
"""Paths B and C: extract from already-captured ``MemoryRegion`` data.
153155
@@ -180,7 +182,7 @@ def populate_from_regions(
180182
entry.path, entry.base_addr,
181183
)
182184
continue
183-
_apply_extraction(entry, data, source_id=source_id, log=log)
185+
_apply_extraction(entry, data, source_id=source_id, log=log, hash_algo=hash_algo)
184186
return entries
185187

186188

@@ -189,14 +191,16 @@ def _apply_extraction(
189191
data: bytes,
190192
source_id: int,
191193
log: logging.Logger,
194+
hash_algo: HashAlgo = HashAlgo.BLAKE3,
192195
) -> None:
193-
"""Run the ELF parser + SHA-256 and populate ``entry`` in place.
196+
"""Run the ELF parser and populate ``entry`` in place.
194197
195198
``native_blob`` always ends up set (possibly with a zero-length
196199
build-id when extraction failed); ``disk_hash`` is always the
197-
SHA-256 of the bytes we read, regardless of whether the parser
198-
recognised them. Both fields serve as collision-resistant anchors
199-
for downstream symbol lookup even when the build-id is missing.
200+
hash (using the file's selected ``HashAlgo``) of the bytes we
201+
read, regardless of whether the parser recognised them. Both
202+
fields serve as collision-resistant anchors for downstream symbol
203+
lookup even when the build-id is missing.
200204
"""
201205
if len(data) < 64:
202206
return
@@ -212,7 +216,9 @@ def _apply_extraction(
212216
build_id, _ = result
213217

214218
entry.native_blob = _encode_native_blob(build_id, source_id, flags)
215-
entry.disk_hash = hashlib.sha256(data).digest()
219+
h = make_hasher(hash_algo)
220+
h.update(data)
221+
entry.disk_hash = h.digest()
216222

217223

218224
__all__ = (

src/memslicer/acquirer/engine.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from memslicer.acquirer.os_detail import pack_os_detail, system_info_to_fields
1818
from memslicer.acquirer.region_filter import RegionFilter
1919
from memslicer.msl.constants import (
20-
CompAlgo, OSType, PageState, RegionType, CapBit,
20+
CompAlgo, HashAlgo, OSType, PageState, RegionType, CapBit,
2121
Endianness, VERSION, HASH_SIZE, FLAG_INVESTIGATION, FLAG_ENCRYPTED,
2222
)
2323
from memslicer.msl.types import (
@@ -245,9 +245,11 @@ def __init__(
245245
collector: InvestigationCollector | None = None,
246246
*,
247247
attribution: AttributionConfig | None = None,
248+
hash_algo: HashAlgo = HashAlgo.BLAKE3,
248249
) -> None:
249250
self._bridge = bridge
250251
self._comp_algo = comp_algo
252+
self._hash_algo = hash_algo
251253
self._filter = region_filter or RegionFilter()
252254
self._os_override = os_override
253255
self._abort = threading.Event()
@@ -349,13 +351,14 @@ def acquire(self, output_path: Path | str) -> AcquireResult:
349351
# ``include_module_build_ids`` — the default acquire path
350352
# produces lean ModuleEntry blocks without build-ids so
351353
# that a minimal process-centric slice does not pay for
352-
# per-module bridge reads and SHA-256 work. Operators who
354+
# per-module bridge reads and hash work. Operators who
353355
# need build-ids either pass the flag or run
354356
# ``memslicer-enrich`` on the finished slice (Path C).
355357
if module_entries and self._attribution.include_module_build_ids:
356358
try:
357359
populate_from_bridge(
358360
module_entries, self._bridge, logger=self._log,
361+
hash_algo=self._hash_algo,
359362
)
360363
except Exception as exc: # noqa: BLE001
361364
self._log.warning(
@@ -392,6 +395,7 @@ def acquire(self, output_path: Path | str) -> AcquireResult:
392395
os_type=os_type,
393396
arch_type=arch_type,
394397
pid=pid,
398+
hash_algo=self._hash_algo,
395399
)
396400

397401
with open(output_path, "wb") as f:

src/memslicer/cli.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
validate_attribution,
1616
)
1717
from memslicer.acquirer.region_filter import RegionFilter, SKIP_REASON_LABELS
18-
from memslicer.msl.constants import CompAlgo, OSType
18+
from memslicer.msl.constants import CompAlgo, HashAlgo, OSType
1919
from memslicer.utils.protection import parse_protection
2020

2121

@@ -161,6 +161,7 @@ def _create_acquirer(
161161
investigation: bool = False,
162162
passphrase: str | None = None,
163163
attribution=None,
164+
hash_algo: HashAlgo = HashAlgo.BLAKE3,
164165
):
165166
"""Factory to create the appropriate acquirer for the selected backend."""
166167
from memslicer.acquirer.engine import AcquisitionEngine
@@ -246,6 +247,7 @@ def _create_acquirer(
246247
passphrase=passphrase,
247248
collector=collector,
248249
attribution=attribution,
250+
hash_algo=hash_algo,
249251
)
250252

251253

@@ -267,10 +269,11 @@ def _create_acquirer(
267269
@click.option("--encrypt", "-E", is_flag=True, default=False, help="Enable AEAD encryption")
268270
@click.option("--no-encrypt", is_flag=True, default=False, help="Disable encryption (overrides investigation default)")
269271
@click.option("--passphrase", default=None, help="Encryption passphrase (prompted if --encrypt and not provided)")
272+
@click.option("--hash-algo", "hash_algo_str", type=click.Choice(["blake3", "sha256", "sha512-256"]), default="blake3", help="Integrity hash algorithm (default: blake3)")
270273
@attribution_options
271274
def cli(target, backend, output_path, comp, usb, remote_addr, os_override, filter_prot, filter_addr,
272275
verbose, read_timeout, include_unreadable, max_region_size, investigation,
273-
encrypt, no_encrypt, passphrase,
276+
encrypt, no_encrypt, passphrase, hash_algo_str,
274277
examiner, case_ref, hostname_override, domain_override,
275278
include_serials, include_network_identity, include_fingerprint,
276279
include_kernel_symbols, include_kernel_modules,
@@ -340,6 +343,10 @@ def cli(target, backend, output_path, comp, usb, remote_addr, os_override, filte
340343
comp_map = {"none": CompAlgo.NONE, "zstd": CompAlgo.ZSTD, "lz4": CompAlgo.LZ4}
341344
comp_algo = comp_map[comp]
342345

346+
# Parse hash algorithm
347+
hash_algo_map = {"blake3": HashAlgo.BLAKE3, "sha256": HashAlgo.SHA256, "sha512-256": HashAlgo.SHA512_256}
348+
hash_algo = hash_algo_map[hash_algo_str]
349+
343350
# Parse OS override
344351
os_map = {"windows": OSType.Windows, "linux": OSType.Linux, "macos": OSType.macOS, "android": OSType.Android, "ios": OSType.iOS}
345352
os_ovr = os_map.get(os_override) if os_override else None
@@ -414,6 +421,7 @@ def progress(regions_captured, total_ranges, bytes_cap, modules, regions_process
414421
investigation=investigation,
415422
passphrase=passphrase if use_encryption else None,
416423
attribution=attribution,
424+
hash_algo=hash_algo,
417425
)
418426
acquirer.set_progress_callback(progress)
419427

src/memslicer/cli_enrich.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@
5858
HEADER_SIZE,
5959
BlockType,
6060
CompAlgo,
61+
HashAlgo,
6162
PageState,
6263
)
63-
from memslicer.msl.iterator import iterate_blocks
64+
from memslicer.msl.iterator import iterate_blocks, read_hash_algo
6465
from memslicer.msl.types import (
6566
FileHeader,
6667
MemoryRegion,
@@ -215,10 +216,10 @@ def _extract_blob_fields(native_blob: bytes) -> tuple[int, int, int, bytes]:
215216

216217
def _load_slice(
217218
slice_path: Path,
218-
) -> tuple[list[ModuleEntry], list[MemoryRegion], int]:
219-
"""Read modules, regions, and the EoC start offset from a slice.
219+
) -> tuple[list[ModuleEntry], list[MemoryRegion], int, HashAlgo]:
220+
"""Read modules, regions, the EoC start offset, and hash algo from a slice.
220221
221-
Returns ``(modules, regions, eoc_start_offset)``. Raises
222+
Returns ``(modules, regions, eoc_start_offset, hash_algo)``. Raises
222223
:class:`FileNotFoundError` / :class:`ValueError` on missing or
223224
malformed files.
224225
"""
@@ -227,6 +228,7 @@ def _load_slice(
227228
eoc_start: int | None = None
228229

229230
with open(slice_path, "rb") as f:
231+
hash_algo = read_hash_algo(f)
230232
for block in iterate_blocks(f):
231233
if block.block_type == BlockType.ModuleEntry:
232234
modules.append(_parse_module_entry_minimal(block.payload))
@@ -241,7 +243,7 @@ def _load_slice(
241243
f"cannot determine where to append the manifest"
242244
)
243245

244-
return modules, regions, eoc_start
246+
return modules, regions, eoc_start, hash_algo
245247

246248

247249
def _build_manifest_rows(modules: list[ModuleEntry]) -> list[ModuleBuildIdRow]:
@@ -359,7 +361,7 @@ def main(slice_path: Path, output: Path | None, in_place: bool) -> None:
359361
output = slice_path.with_suffix(slice_path.suffix + ".enriched")
360362

361363
try:
362-
modules, regions, eoc_start = _load_slice(slice_path)
364+
modules, regions, eoc_start, hash_algo = _load_slice(slice_path)
363365
except ValueError as exc:
364366
click.echo(f"error: {exc}", err=True)
365367
sys.exit(2)
@@ -381,6 +383,7 @@ def main(slice_path: Path, output: Path | None, in_place: bool) -> None:
381383
regions,
382384
source_id=SOURCE_RETROACTIVE,
383385
logger=_log,
386+
hash_algo=hash_algo,
384387
)
385388

386389
rows = _build_manifest_rows(unpopulated)

src/memslicer/msl/constants.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,17 @@ class ClockSource(IntEnum):
173173
QueryPerformanceCounter = 0x03
174174
mach_absolute_time = 0x04
175175
Other = 0xFF
176+
177+
178+
class HashAlgo(IntEnum):
179+
"""Integrity hash algorithm selector (spec Section 4.4, Table 12).
180+
181+
Applies to PrevHash, EoC FileHash, and Module Entry DiskHash.
182+
All registered algorithms produce 32-byte output.
183+
KDF always uses HKDF-BLAKE3 regardless of this setting.
184+
"""
185+
186+
BLAKE3 = 0x00 # Default. High throughput via tree hashing and SIMD.
187+
SHA256 = 0x01 # FIPS 180-4. HW-accelerated on Intel SHA-NI / AMD Zen / ARMv8 CE.
188+
SHA512_256 = 0x02 # FIPS 180-4. Truncated SHA-512 with distinct IVs; fast on 64-bit w/o SHA-NI.
189+
OTHER = 0xFF # Non-standard; interpretation out of scope.

src/memslicer/msl/integrity.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,50 @@
1-
"""BLAKE3-based integrity chain for MSL files."""
1+
"""Integrity hash chain for MSL files.
2+
3+
Supports BLAKE3 (default), SHA-256, and SHA-512/256 as selected by
4+
the ``HashAlgo`` field in the file header (spec Section 4.4).
5+
"""
6+
from __future__ import annotations
7+
8+
import hashlib
9+
210
import blake3
311

12+
from memslicer.msl.constants import HashAlgo
13+
14+
15+
def make_hasher(algo: HashAlgo):
16+
"""Return a new hash object for the given algorithm.
17+
18+
The returned object exposes ``.update(data)`` and ``.digest()``
19+
(32-byte output for all registered algorithms).
20+
"""
21+
if algo == HashAlgo.BLAKE3:
22+
return blake3.blake3()
23+
if algo == HashAlgo.SHA256:
24+
return hashlib.sha256()
25+
if algo == HashAlgo.SHA512_256:
26+
return hashlib.new("sha512_256")
27+
raise ValueError(f"unsupported integrity hash algorithm: {algo!r}")
28+
429

530
class IntegrityChain:
6-
"""Maintains a rolling BLAKE3 hash chain across MSL blocks.
31+
"""Maintains a rolling hash chain across MSL blocks.
732
833
The chain works as follows:
934
- feed_header(header_bytes) hashes the file header, returns digest for Block 0's PrevHash
1035
- feed_block(block_bytes) hashes a complete block, returns digest for next block's PrevHash
1136
- prev_hash property returns current PrevHash value for next block
12-
- finalize() returns running BLAKE3 digest of entire file (for EoC FileHash)
37+
- finalize() returns running digest of entire file (for EoC FileHash)
1338
"""
1439

15-
def __init__(self) -> None:
40+
def __init__(self, hash_algo: HashAlgo = HashAlgo.BLAKE3) -> None:
41+
self._hash_algo = hash_algo
1642
self._prev_hash: bytes = b'\x00' * 32
17-
self._file_hasher = blake3.blake3()
43+
self._file_hasher = make_hasher(hash_algo)
44+
45+
def _new_hasher(self):
46+
"""Create a fresh per-block hasher."""
47+
return make_hasher(self._hash_algo)
1848

1949
@property
2050
def prev_hash(self) -> bytes:
@@ -24,24 +54,28 @@ def prev_hash(self) -> bytes:
2454
def feed_header(self, header_bytes: bytes) -> bytes:
2555
"""Hash file header bytes, update chain. Returns digest for Block 0's PrevHash."""
2656
self._file_hasher.update(header_bytes)
27-
self._prev_hash = blake3.blake3(header_bytes).digest()
57+
h = self._new_hasher()
58+
h.update(header_bytes)
59+
self._prev_hash = h.digest()
2860
return self._prev_hash
2961

3062
def feed_block(self, block_bytes: bytes) -> bytes:
3163
"""Hash a complete block, update chain. Returns digest for next block's PrevHash."""
3264
self._file_hasher.update(block_bytes)
33-
self._prev_hash = blake3.blake3(block_bytes).digest()
65+
h = self._new_hasher()
66+
h.update(block_bytes)
67+
self._prev_hash = h.digest()
3468
return self._prev_hash
3569

3670
def feed_block_parts(self, *parts: bytes) -> bytes:
3771
"""Hash a block from multiple parts without concatenation."""
38-
block_hasher = blake3.blake3()
72+
block_hasher = self._new_hasher()
3973
for part in parts:
4074
self._file_hasher.update(part)
4175
block_hasher.update(part)
4276
self._prev_hash = block_hasher.digest()
4377
return self._prev_hash
4478

4579
def finalize(self) -> bytes:
46-
"""Return BLAKE3 digest of the entire file so far (for EoC FileHash)."""
80+
"""Return digest of the entire file so far (for EoC FileHash)."""
4781
return self._file_hasher.digest()

0 commit comments

Comments
 (0)