use std::io::{Read, Seek, SeekFrom};
use crate::crc32;
use crate::entry::{parse_entry_array, GptEntry};
use crate::findings::{Anomaly, AnomalyKind, GptAnalysis, Location};
use crate::header::GptHeader;
use crate::Error;
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct AnalyseOptions {
pub sector_size: Option<u64>,
}
pub fn analyse_with_options<R: Read + Seek>(
reader: &mut R,
disk_size_bytes: u64,
opts: AnalyseOptions,
) -> Result<GptAnalysis, Error> {
analyse_inner(reader, disk_size_bytes, opts)
}
fn detect_sector_size<R: Read + Seek>(reader: &mut R) -> Result<u64, Error> {
for size in [512u64, 4096] {
reader.seek(SeekFrom::Start(size))?;
let mut sig = [0u8; 8];
if reader.read_exact(&mut sig).is_ok() && &sig == crate::header::SIGNATURE {
return Ok(size);
}
}
Ok(512)
}
#[cfg_attr(feature = "trace", tracing::instrument(level = "debug", skip(reader)))]
pub fn analyse<R: Read + Seek>(reader: &mut R, disk_size_bytes: u64) -> Result<GptAnalysis, Error> {
analyse_inner(reader, disk_size_bytes, AnalyseOptions::default())
}
fn analyse_inner<R: Read + Seek>(
reader: &mut R,
disk_size_bytes: u64,
opts: AnalyseOptions,
) -> Result<GptAnalysis, Error> {
let mut anomalies = Vec::new();
let sector_size = match opts.sector_size {
Some(s) => s,
None => detect_sector_size(reader)?,
};
let primary_sector = read_sector(reader, 1, sector_size)?;
let primary = GptHeader::parse(&primary_sector)?;
if !primary.header_crc_valid {
record(
&mut anomalies,
AnomalyKind::HeaderCrcInvalid {
location: Location::Primary,
},
);
}
check_header_slack(
&primary_sector,
primary.header_size,
Location::Primary,
&mut anomalies,
);
if primary.my_lba != 1 {
record(
&mut anomalies,
AnomalyKind::HeaderLbaMismatch {
location: Location::Primary,
claimed: primary.my_lba,
actual: 1,
},
);
}
let primary_array = read_entry_array(reader, &primary, sector_size)?;
if crc32::checksum(&primary_array) != primary.partition_array_crc32 {
record(
&mut anomalies,
AnomalyKind::PartitionArrayCrcInvalid {
location: Location::Primary,
},
);
}
let partitions = parse_entry_array(
&primary_array,
primary.num_partition_entries,
primary.partition_entry_size,
);
let backup = read_backup(
reader,
&primary,
&primary_array,
sector_size,
&mut anomalies,
);
if disk_size_bytes > 0 {
let disk_last_lba = (disk_size_bytes / sector_size).saturating_sub(1);
if disk_last_lba > primary.alternate_lba {
record(
&mut anomalies,
AnomalyKind::BackupGptNotAtDiskEnd {
alternate_lba: primary.alternate_lba,
disk_last_lba,
},
);
}
}
check_overlaps(&partitions, &mut anomalies);
check_bounds(
&partitions,
primary.first_usable_lba,
primary.last_usable_lba,
&mut anomalies,
);
for (a, b) in crate::collision::find_duplicate_partition_guids(&partitions) {
record(&mut anomalies, AnomalyKind::DuplicatePartitionGuid { a, b });
}
check_encrypted_volumes(reader, &partitions, sector_size, &mut anomalies);
reconcile_mbr(
reader,
&partitions,
disk_size_bytes,
sector_size,
&mut anomalies,
);
let mut evidence = primary_sector.to_vec();
evidence.extend_from_slice(&primary_array);
let gpt_sha256 = crate::sha256::hex(&crate::sha256::digest(&evidence));
let disk_guid = primary.disk_guid;
Ok(GptAnalysis {
primary,
backup,
disk_guid,
partitions,
sector_size,
gpt_sha256,
anomalies,
})
}
fn record(anomalies: &mut Vec<Anomaly>, kind: AnomalyKind) {
anomalies.push(Anomaly::new(kind));
}
fn check_header_slack(
sector: &[u8; 512],
header_size: u32,
location: Location,
anomalies: &mut Vec<Anomaly>,
) {
let start = (header_size as usize).clamp(92, 512);
if sector[start..].iter().any(|&b| b != 0) {
record(anomalies, AnomalyKind::HeaderSlackData { location });
}
}
fn read_sector<R: Read + Seek>(
reader: &mut R,
lba: u64,
sector_size: u64,
) -> Result<[u8; 512], Error> {
reader.seek(SeekFrom::Start(lba * sector_size))?;
let mut buf = [0u8; 512];
reader.read_exact(&mut buf)?;
Ok(buf)
}
fn read_entry_array<R: Read + Seek>(
reader: &mut R,
h: &GptHeader,
sector_size: u64,
) -> Result<Vec<u8>, Error> {
let len = h.num_partition_entries as usize * h.partition_entry_size as usize;
reader.seek(SeekFrom::Start(h.partition_entry_lba * sector_size))?;
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf)?;
Ok(buf)
}
fn read_backup<R: Read + Seek>(
reader: &mut R,
primary: &GptHeader,
primary_array: &[u8],
sector_size: u64,
anomalies: &mut Vec<Anomaly>,
) -> Option<GptHeader> {
let Ok(backup_sector) = read_sector(reader, primary.alternate_lba, sector_size) else {
record(anomalies, AnomalyKind::BackupGptUnreadable);
return None;
};
let Ok(backup) = GptHeader::parse(&backup_sector) else {
record(anomalies, AnomalyKind::BackupGptUnreadable);
return None;
};
if !backup.header_crc_valid {
record(
anomalies,
AnomalyKind::HeaderCrcInvalid {
location: Location::Backup,
},
);
}
check_header_slack(
&backup_sector,
backup.header_size,
Location::Backup,
anomalies,
);
if backup.my_lba != primary.alternate_lba {
record(
anomalies,
AnomalyKind::HeaderLbaMismatch {
location: Location::Backup,
claimed: backup.my_lba,
actual: primary.alternate_lba,
},
);
}
if let Ok(arr) = read_entry_array(reader, &backup, sector_size) {
if crc32::checksum(&arr) != backup.partition_array_crc32 {
record(
anomalies,
AnomalyKind::PartitionArrayCrcInvalid {
location: Location::Backup,
},
);
}
if arr != primary_array {
record(
anomalies,
AnomalyKind::PrimaryBackupDivergence {
field: "entry array contents",
},
);
}
}
let checks: &[(&'static str, bool)] = &[
("revision", primary.revision == backup.revision),
("header_size", primary.header_size == backup.header_size),
("disk_guid", primary.disk_guid == backup.disk_guid),
(
"first_usable_lba",
primary.first_usable_lba == backup.first_usable_lba,
),
(
"last_usable_lba",
primary.last_usable_lba == backup.last_usable_lba,
),
(
"num_partition_entries",
primary.num_partition_entries == backup.num_partition_entries,
),
(
"partition_entry_size",
primary.partition_entry_size == backup.partition_entry_size,
),
(
"partition_array_crc32",
primary.partition_array_crc32 == backup.partition_array_crc32,
),
];
for &(field, ok) in checks {
if !ok {
record(anomalies, AnomalyKind::PrimaryBackupDivergence { field });
}
}
Some(backup)
}
const PROTECTIVE_UNDERSIZE_TOLERANCE: u64 = 2048;
fn reconcile_mbr<R: Read + Seek>(
reader: &mut R,
partitions: &[GptEntry],
disk_size_bytes: u64,
sector_size: u64,
anomalies: &mut Vec<Anomaly>,
) {
let Ok(sector) = read_sector(reader, 0, sector_size) else {
return; };
let mbr = crate::mbr::parse_mbr_entries(§or);
let active: Vec<_> = mbr.iter().filter(|e| !e.is_empty()).collect();
match active.iter().find(|e| e.is_protective()) {
None => record(anomalies, AnomalyKind::MissingProtectiveMbr),
Some(p) if disk_size_bytes > 0 && p.lba_count != u32::MAX => {
let disk_last_lba = (disk_size_bytes / sector_size).saturating_sub(1);
let covered_last_lba = p.lba_end();
if disk_last_lba.saturating_sub(covered_last_lba) > PROTECTIVE_UNDERSIZE_TOLERANCE {
record(
anomalies,
AnomalyKind::ProtectiveMbrUndersized {
covered_last_lba,
disk_last_lba,
},
);
}
}
Some(_) => {}
}
for e in active.iter().filter(|e| !e.is_protective()) {
let (start, end) = (u64::from(e.lba_start), e.lba_end());
let overlaps_gpt = partitions
.iter()
.any(|g| start <= g.last_lba && g.first_lba <= end);
if !overlaps_gpt {
record(
anomalies,
AnomalyKind::HybridMbrHiddenPartition {
mbr_index: e.index,
lba_start: e.lba_start,
lba_count: e.lba_count,
},
);
}
}
}
fn check_encrypted_volumes<R: Read + Seek>(
reader: &mut R,
partitions: &[GptEntry],
sector_size: u64,
anomalies: &mut Vec<Anomaly>,
) {
for (index, p) in partitions.iter().enumerate() {
if p.type_name() == Some("Linux LUKS") {
continue;
}
let Ok(sector) = read_sector(reader, p.first_lba, sector_size) else {
continue;
};
if forensicnomicon::filesystems::detect_name(§or).is_some() {
continue;
}
let entropy = crate::entropy::shannon(§or);
if entropy > crate::entropy::HIGH_ENTROPY_THRESHOLD {
record(
anomalies,
AnomalyKind::HiddenEncryptedVolume { index, entropy },
);
}
}
}
fn check_overlaps(partitions: &[GptEntry], anomalies: &mut Vec<Anomaly>) {
let mut idx: Vec<usize> = (0..partitions.len()).collect();
idx.sort_by_key(|&i| partitions[i].first_lba);
for pair in idx.windows(2) {
let (a, b) = (pair[0], pair[1]);
if partitions[b].first_lba <= partitions[a].last_lba {
record(anomalies, AnomalyKind::OverlappingPartitions { a, b });
}
}
}
fn check_bounds(
partitions: &[GptEntry],
first_usable: u64,
last_usable: u64,
anomalies: &mut Vec<Anomaly>,
) {
for (index, p) in partitions.iter().enumerate() {
if p.last_lba > last_usable {
record(
anomalies,
AnomalyKind::PartitionOutOfBounds {
index,
last_lba: p.last_lba,
last_usable,
},
);
}
if p.first_lba < first_usable {
record(
anomalies,
AnomalyKind::PartitionOverlapsGptArea {
index,
first_lba: p.first_lba,
first_usable,
},
);
}
}
}