APFS: contêiner, mapa de objetos, árvore de arquivos, instantâneos e nós antigos
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
465bb672bd
commit
6fcbe773a1
5 files changed
+762
-1
No files matched your search
@@ -0,0 +1,531 @@
|
|||||||
|
//! APFS (volumes sem cifragem): lê o contêiner, o mapa de objetos e a árvore de arquivos de cada
|
||||||
|
//! volume; arquivos apagados vêm de duas fontes: as árvores dos instantâneos (snapshots) e os nós
|
||||||
|
//! antigos da árvore (copy-on-write) que ainda estão no disco. Volumes cifrados (FileVault) são
|
||||||
|
//! detectados e deixados para a varredura por assinatura.
|
||||||
|
|
||||||
|
use super::{make_found, Ctx, NewFound, VolumeScan};
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use crate::types::{Confidence, DataLoc, Extent, Origin};
|
||||||
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
|
fn le16(b: &[u8], o: usize) -> u16 {
|
||||||
|
b.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).unwrap_or(0)
|
||||||
|
}
|
||||||
|
fn le32(b: &[u8], o: usize) -> u32 {
|
||||||
|
b.get(o..o + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap())).unwrap_or(0)
|
||||||
|
}
|
||||||
|
fn le64(b: &[u8], o: usize) -> u64 {
|
||||||
|
b.get(o..o + 8).map(|s| u64::from_le_bytes(s.try_into().unwrap())).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OBJ_TYPE_MASK: u32 = 0xFFFF;
|
||||||
|
const T_BTREE_ROOT: u32 = 2;
|
||||||
|
const T_BTREE_NODE: u32 = 3;
|
||||||
|
const SUBTYPE_FSTREE: u32 = 0x0E;
|
||||||
|
|
||||||
|
// tipos de registro da árvore de arquivos
|
||||||
|
const R_SNAP_METADATA: u8 = 1;
|
||||||
|
const R_INODE: u8 = 3;
|
||||||
|
const R_FILE_EXTENT: u8 = 8;
|
||||||
|
const R_DIR_REC: u8 = 9;
|
||||||
|
|
||||||
|
/// Soma de verificação Fletcher-64 do APFS (cobre o bloco menos os 8 bytes iniciais).
|
||||||
|
pub fn fletcher64(data: &[u8]) -> u64 {
|
||||||
|
let (mut s1, mut s2) = (0u64, 0u64);
|
||||||
|
for w in data.chunks_exact(4) {
|
||||||
|
s1 = (s1 + u32::from_le_bytes(w.try_into().unwrap()) as u64) % 0xFFFF_FFFF;
|
||||||
|
s2 = (s2 + s1) % 0xFFFF_FFFF;
|
||||||
|
}
|
||||||
|
let c1 = 0xFFFF_FFFF - ((s1 + s2) % 0xFFFF_FFFF);
|
||||||
|
let c2 = 0xFFFF_FFFF - ((s1 + c1) % 0xFFFF_FFFF);
|
||||||
|
(c2 << 32) | c1
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Apfs<'a> {
|
||||||
|
src: &'a dyn Source,
|
||||||
|
base: u64,
|
||||||
|
bs: u64,
|
||||||
|
blocks: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BNode {
|
||||||
|
flags: u16,
|
||||||
|
level: u16,
|
||||||
|
/// (chave, valor) por entrada
|
||||||
|
entries: Vec<(Vec<u8>, Vec<u8>)>,
|
||||||
|
oid: u64,
|
||||||
|
xid: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Apfs<'a> {
|
||||||
|
fn block(&self, paddr: u64) -> Option<Vec<u8>> {
|
||||||
|
if paddr >= self.blocks {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let b = read_vec(self.src, self.base + paddr * self.bs, self.bs as usize).ok()?;
|
||||||
|
(b.len() as u64 == self.bs).then_some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checked_block(&self, paddr: u64) -> Option<Vec<u8>> {
|
||||||
|
let b = self.block(paddr)?;
|
||||||
|
(fletcher64(&b[8..]) == le64(&b, 0)).then_some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_node(&self, b: &[u8], fixed_k: usize, fixed_v: usize) -> Option<BNode> {
|
||||||
|
let flags = le16(b, 32);
|
||||||
|
let level = le16(b, 34);
|
||||||
|
let n = le32(b, 36) as usize;
|
||||||
|
let toc_off = le16(b, 40) as usize;
|
||||||
|
let toc_len = le16(b, 42) as usize;
|
||||||
|
let is_fixed = flags & 4 != 0;
|
||||||
|
let is_root = flags & 1 != 0;
|
||||||
|
let key_base = 56 + toc_off + toc_len;
|
||||||
|
let val_end = b.len() - if is_root { 40 } else { 0 };
|
||||||
|
let mut entries = Vec::with_capacity(n);
|
||||||
|
for i in 0..n {
|
||||||
|
let (koff, klen, voff, vlen);
|
||||||
|
if is_fixed {
|
||||||
|
let o = 56 + toc_off + i * 4;
|
||||||
|
koff = le16(b, o) as usize;
|
||||||
|
voff = le16(b, o + 2) as usize;
|
||||||
|
klen = fixed_k;
|
||||||
|
vlen = if level == 0 { fixed_v } else { 8 };
|
||||||
|
} else {
|
||||||
|
let o = 56 + toc_off + i * 8;
|
||||||
|
koff = le16(b, o) as usize;
|
||||||
|
klen = le16(b, o + 2) as usize;
|
||||||
|
voff = le16(b, o + 4) as usize;
|
||||||
|
vlen = le16(b, o + 6) as usize;
|
||||||
|
}
|
||||||
|
let ks = key_base + koff;
|
||||||
|
let k = b.get(ks..ks + klen)?.to_vec();
|
||||||
|
let v = if voff == 0xFFFF { vec![] } else { b.get(val_end.checked_sub(voff)?..)?.get(..vlen)?.to_vec() };
|
||||||
|
entries.push((k, v));
|
||||||
|
}
|
||||||
|
Some(BNode { flags, level, entries, oid: le64(b, 8), xid: le64(b, 16) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Todas as entradas de uma árvore física (mapa de objetos): filhos são endereços físicos.
|
||||||
|
fn walk_physical(&self, root: u64, fixed_k: usize, fixed_v: usize, f: &mut dyn FnMut(&[u8], &[u8])) {
|
||||||
|
let mut stack = vec![(root, 0u32)];
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
while let Some((p, depth)) = stack.pop() {
|
||||||
|
if depth > 8 || !seen.insert(p) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(b) = self.checked_block(p) else { continue };
|
||||||
|
let Some(n) = self.parse_node(&b, fixed_k, fixed_v) else { continue };
|
||||||
|
if n.level == 0 {
|
||||||
|
for (k, v) in &n.entries {
|
||||||
|
f(k, v);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (_, v) in n.entries.iter().rev() {
|
||||||
|
stack.push((le64(v, 0), depth + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mapa de objetos: oid -> [(xid, paddr)] ordenado por xid.
|
||||||
|
fn omap(&self, omap_paddr: u64) -> HashMap<u64, Vec<(u64, u64)>> {
|
||||||
|
let mut m: HashMap<u64, Vec<(u64, u64)>> = HashMap::new();
|
||||||
|
let Some(ob) = self.checked_block(omap_paddr) else { return m };
|
||||||
|
let tree = le64(&ob, 48);
|
||||||
|
self.walk_physical(tree, 16, 16, &mut |k, v| {
|
||||||
|
m.entry(le64(k, 0)).or_default().push((le64(k, 8), le64(v, 8)));
|
||||||
|
});
|
||||||
|
for v in m.values_mut() {
|
||||||
|
v.sort();
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup(m: &HashMap<u64, Vec<(u64, u64)>>, oid: u64, max_xid: u64) -> Option<u64> {
|
||||||
|
m.get(&oid)?.iter().rev().find(|(x, _)| *x <= max_xid).map(|(_, p)| *p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- registros
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct Inode {
|
||||||
|
parent: u64,
|
||||||
|
private_id: u64,
|
||||||
|
mtime: i64,
|
||||||
|
ctime: i64,
|
||||||
|
atime: i64,
|
||||||
|
mode: u16,
|
||||||
|
size: u64,
|
||||||
|
name: Option<String>,
|
||||||
|
has: bool,
|
||||||
|
compressed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct FileRec {
|
||||||
|
inode: Inode,
|
||||||
|
names: BTreeMap<u64, String>, // pai -> nome
|
||||||
|
}
|
||||||
|
|
||||||
|
type Files = HashMap<u64, FileRec>;
|
||||||
|
type Extents = HashMap<u64, BTreeMap<u64, (u64, u64, u64)>>; // private_id -> logical -> (len, paddr, crypto)
|
||||||
|
|
||||||
|
fn ns(t: u64) -> i64 {
|
||||||
|
(t / 1_000_000_000) as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_record(files: &mut Files, extents: &mut Extents, k: &[u8], v: &[u8]) {
|
||||||
|
let hdr = le64(k, 0);
|
||||||
|
let (id, ty) = (hdr & 0x0FFF_FFFF_FFFF_FFFF, (hdr >> 60) as u8);
|
||||||
|
match ty {
|
||||||
|
R_INODE if v.len() >= 92 => {
|
||||||
|
let e = files.entry(id).or_default();
|
||||||
|
let mut ino = Inode {
|
||||||
|
parent: le64(v, 0),
|
||||||
|
private_id: le64(v, 8),
|
||||||
|
ctime: ns(le64(v, 16)), // criação
|
||||||
|
mtime: ns(le64(v, 24)),
|
||||||
|
atime: ns(le64(v, 40)),
|
||||||
|
mode: le16(v, 80),
|
||||||
|
size: le64(v, 84).max(0),
|
||||||
|
has: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let flags = le64(v, 48);
|
||||||
|
ino.compressed = le32(v, 68) & 0x20 != 0;
|
||||||
|
let _ = flags;
|
||||||
|
// campos estendidos
|
||||||
|
let xf = &v[92..];
|
||||||
|
if xf.len() >= 4 {
|
||||||
|
let n = le16(xf, 0) as usize;
|
||||||
|
let mut p = 4 + n * 4;
|
||||||
|
for i in 0..n {
|
||||||
|
let (xt, xs) = (xf[4 + i * 4], le16(xf, 6 + i * 4) as usize);
|
||||||
|
let data = xf.get(p..p + xs).unwrap_or(&[]);
|
||||||
|
match xt {
|
||||||
|
4 => ino.name = Some(String::from_utf8_lossy(data.split(|&c| c == 0).next().unwrap_or(&[])).to_string()),
|
||||||
|
8 if data.len() >= 8 => ino.size = le64(data, 0),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
p += xs.div_ceil(8) * 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.inode = ino;
|
||||||
|
}
|
||||||
|
R_DIR_REC if k.len() > 12 && v.len() >= 18 => {
|
||||||
|
let nl = (le32(k, 8) & 0x3FF) as usize;
|
||||||
|
if let Some(name) = k.get(12..12 + nl) {
|
||||||
|
let name = String::from_utf8_lossy(name.split(|&c| c == 0).next().unwrap_or(&[])).to_string();
|
||||||
|
files.entry(le64(v, 0)).or_default().names.insert(id, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R_FILE_EXTENT if k.len() >= 16 && v.len() >= 24 => {
|
||||||
|
let len = le64(v, 0) & 0x00FF_FFFF_FFFF_FFFF;
|
||||||
|
extents.entry(id).or_default().insert(le64(k, 8), (len, le64(v, 8), le64(v, 16)));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apfs<'_> {
|
||||||
|
/// Percorre a árvore de arquivos a partir de uma raiz virtual.
|
||||||
|
fn walk_fstree(&self, root_oid: u64, omap: &HashMap<u64, Vec<(u64, u64)>>, xid: u64, f: &mut dyn FnMut(&[u8], &[u8], u64)) {
|
||||||
|
let Some(rp) = lookup(omap, root_oid, xid) else { return };
|
||||||
|
let mut stack = vec![(rp, 0u32)];
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
while let Some((p, depth)) = stack.pop() {
|
||||||
|
if depth > 10 || !seen.insert(p) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(b) = self.checked_block(p) else { continue };
|
||||||
|
let Some(n) = self.parse_node(&b, 16, 16) else { continue };
|
||||||
|
if n.level == 0 {
|
||||||
|
for (k, v) in &n.entries {
|
||||||
|
f(k, v, p);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (_, v) in n.entries.iter().rev() {
|
||||||
|
if let Some(cp) = lookup(omap, le64(v, 0), xid) {
|
||||||
|
stack.push((cp, depth + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_of(files: &Files, mut id: u64) -> String {
|
||||||
|
let mut parts = vec![];
|
||||||
|
for _ in 0..64 {
|
||||||
|
if id == 2 || id == 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match files.get(&id).and_then(|f| f.names.iter().next().map(|(p, n)| (*p, n.clone()))) {
|
||||||
|
Some((p, n)) => {
|
||||||
|
parts.push(n);
|
||||||
|
id = p;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.reverse();
|
||||||
|
parts.join("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Volume {
|
||||||
|
name: String,
|
||||||
|
omap_paddr: u64,
|
||||||
|
root_oid: u64,
|
||||||
|
snap_tree_oid: u64,
|
||||||
|
encrypted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_volume(b: &[u8]) -> Option<Volume> {
|
||||||
|
if &b[32..36] != b"APSB" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = String::from_utf8_lossy(&b[704..960]).split('\0').next().unwrap_or("").to_string();
|
||||||
|
Some(Volume {
|
||||||
|
name,
|
||||||
|
omap_paddr: le64(b, 128),
|
||||||
|
root_oid: le64(b, 136),
|
||||||
|
snap_tree_oid: le64(b, 152),
|
||||||
|
encrypted: le64(b, 264) & 1 == 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_container<'a>(src: &'a dyn Source, base: u64) -> Option<(Apfs<'a>, Vec<u8>)> {
|
||||||
|
let b0 = read_vec(src, base, 4096).ok()?;
|
||||||
|
if b0.len() < 4096 || &b0[32..36] != b"NXSB" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bs = le32(&b0, 36) as u64;
|
||||||
|
let blocks = le64(&b0, 40);
|
||||||
|
if !(4096..=65536).contains(&bs) || !bs.is_power_of_two() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let a = Apfs { src, base, bs, blocks };
|
||||||
|
// O superbloco mais recente está na área de descritores de checkpoint.
|
||||||
|
let desc_blocks = le32(&b0, 104) as u64;
|
||||||
|
let desc_base = le64(&b0, 112);
|
||||||
|
let mut best = b0.clone();
|
||||||
|
for i in 0..desc_blocks {
|
||||||
|
if let Some(b) = a.checked_block(desc_base as u64 + i) {
|
||||||
|
if &b[32..36] == b"NXSB" && le64(&b, 16) > le64(&best, 16) {
|
||||||
|
best = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((a, best))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan(ctx: &mut Ctx, base: u64, part_len: u64) -> VolumeScan {
|
||||||
|
let _ = part_len;
|
||||||
|
let mut res = VolumeScan::default();
|
||||||
|
let Some((a, nx)) = open_container(ctx.src, base) else { return res };
|
||||||
|
let Some(cont_omap_block) = a.checked_block(le64(&nx, 160)) else { return res };
|
||||||
|
let _ = cont_omap_block;
|
||||||
|
let container_omap = a.omap(le64(&nx, 160));
|
||||||
|
let max_fs = le32(&nx, 180).min(100) as usize;
|
||||||
|
let mut vols: Vec<Volume> = vec![];
|
||||||
|
for i in 0..max_fs {
|
||||||
|
let oid = le64(&nx, 184 + i * 8);
|
||||||
|
if oid == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(p) = lookup(&container_omap, oid, u64::MAX) {
|
||||||
|
if let Some(b) = a.checked_block(p) {
|
||||||
|
if let Some(v) = parse_volume(&b) {
|
||||||
|
vols.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let nvols = vols.len();
|
||||||
|
for v in &vols {
|
||||||
|
if v.encrypted {
|
||||||
|
res.warnings.push(format!("APFS: o volume \"{}\" está cifrado (FileVault); os arquivos só são legíveis com a chave, então use a varredura por assinatura apenas se o volume já estiver aberto.", v.name));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
scan_volume(ctx, &a, v, nvols == 1, &mut res);
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_volume(ctx: &mut Ctx, a: &Apfs, v: &Volume, allow_stale_scan: bool, res: &mut VolumeScan) {
|
||||||
|
let omap = a.omap(v.omap_paddr);
|
||||||
|
let current_xid = u64::MAX;
|
||||||
|
// Árvore atual.
|
||||||
|
let mut live: Files = HashMap::new();
|
||||||
|
let mut live_ext: Extents = HashMap::new();
|
||||||
|
let mut live_nodes: HashSet<u64> = HashSet::new();
|
||||||
|
a.walk_fstree(v.root_oid, &omap, current_xid, &mut |k, val, node| {
|
||||||
|
live_nodes.insert(node);
|
||||||
|
add_record(&mut live, &mut live_ext, k, val);
|
||||||
|
});
|
||||||
|
// Instantâneos.
|
||||||
|
let mut stale: Files = HashMap::new();
|
||||||
|
let mut stale_ext: Extents = HashMap::new();
|
||||||
|
if v.snap_tree_oid != 0 {
|
||||||
|
if let Some(sp) = lookup(&omap, v.snap_tree_oid, current_xid).or(Some(v.snap_tree_oid)) {
|
||||||
|
let mut snaps: Vec<(u64, u64)> = vec![]; // (xid, sblock paddr)
|
||||||
|
let mut stack = vec![sp];
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
while let Some(p) = stack.pop() {
|
||||||
|
if !seen.insert(p) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(b) = a.checked_block(p) else { continue };
|
||||||
|
let Some(n) = a.parse_node(&b, 0, 0) else { continue };
|
||||||
|
if n.level == 0 {
|
||||||
|
for (k, val) in &n.entries {
|
||||||
|
let hdr = le64(k, 0);
|
||||||
|
if (hdr >> 60) as u8 == R_SNAP_METADATA && val.len() >= 40 {
|
||||||
|
snaps.push((hdr & 0x0FFF_FFFF_FFFF_FFFF, le64(val, 8)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (_, val) in &n.entries {
|
||||||
|
// árvore de metadados de snapshot: filhos físicos ou virtuais
|
||||||
|
let c = le64(val, 0);
|
||||||
|
stack.push(lookup(&omap, c, current_xid).unwrap_or(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (xid, sblock) in snaps {
|
||||||
|
let Some(sb) = a.checked_block(sblock) else { continue };
|
||||||
|
let Some(sv) = parse_volume(&sb) else { continue };
|
||||||
|
a.walk_fstree(sv.root_oid, &omap, xid, &mut |k, val, _| {
|
||||||
|
add_record(&mut stale, &mut stale_ext, k, val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nós antigos da árvore de arquivos.
|
||||||
|
if allow_stale_scan {
|
||||||
|
let total = a.blocks;
|
||||||
|
for p in 0..total {
|
||||||
|
if p % 4096 == 0 {
|
||||||
|
if (ctx.cancelled)() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(ctx.progress)(p, total);
|
||||||
|
}
|
||||||
|
let Some(b) = a.block(p) else { continue };
|
||||||
|
let ty = le32(&b, 24) & OBJ_TYPE_MASK;
|
||||||
|
if (ty != T_BTREE_NODE && ty != T_BTREE_ROOT) || le32(&b, 28) != SUBTYPE_FSTREE || le16(&b, 34) != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if live_nodes.contains(&p) || fletcher64(&b[8..]) != le64(&b, 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(n) = a.parse_node(&b, 16, 16) {
|
||||||
|
let _ = (n.oid, n.xid, n.flags);
|
||||||
|
for (k, val) in &n.entries {
|
||||||
|
add_record(&mut stale, &mut stale_ext, k, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Arquivos que só existem nas versões antigas.
|
||||||
|
let mut ids: Vec<&u64> = stale.keys().collect();
|
||||||
|
ids.sort();
|
||||||
|
let mut seen_first: HashSet<u64> = HashSet::new();
|
||||||
|
for id in ids {
|
||||||
|
if live.get(id).map_or(false, |f| f.inode.has) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let rec = &stale[id];
|
||||||
|
let ino = &rec.inode;
|
||||||
|
if !ino.has || ino.mode & 0o170000 != 0o100000 || ino.size == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(ex) = stale_ext.get(&ino.private_id) else { continue };
|
||||||
|
if ex.values().any(|e| e.2 != 0) {
|
||||||
|
continue; // extensão cifrada
|
||||||
|
}
|
||||||
|
// nome e pasta: da entrada de diretório, senão do próprio inode
|
||||||
|
let (name, parent) = match rec.names.iter().next() {
|
||||||
|
Some((p, n)) => (n.clone(), *p),
|
||||||
|
None => match &ino.name {
|
||||||
|
Some(n) => (n.clone(), ino.parent),
|
||||||
|
None => (format!("arquivo_{id}"), ino.parent),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let merged: Files = stale.iter().map(|(k, v)| (*k, v.clone())).chain(live.iter().map(|(k, v)| (*k, v.clone()))).collect();
|
||||||
|
let dir = Some(format!("/{}", path_of(&merged, parent)).replace("//", "/"));
|
||||||
|
let mut extents: Vec<Extent> = vec![];
|
||||||
|
let mut pos = 0u64;
|
||||||
|
let mut ok = true;
|
||||||
|
for (&lo, &(len, paddr, _)) in ex {
|
||||||
|
if lo > pos {
|
||||||
|
extents.push(Extent { offset: 0, len: lo - pos, sparse: true });
|
||||||
|
}
|
||||||
|
if paddr == 0 {
|
||||||
|
extents.push(Extent { offset: 0, len, sparse: true });
|
||||||
|
} else if paddr < a.blocks {
|
||||||
|
extents.push(Extent::new(a.base + paddr * a.bs, len));
|
||||||
|
} else {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
pos = lo + len;
|
||||||
|
}
|
||||||
|
if !ok || pos < ino.size.min(1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let extents = super::fat::trim_extents(extents, ino.size);
|
||||||
|
let Some(first) = extents.iter().find(|e| !e.sparse) else { continue };
|
||||||
|
if !seen_first.insert(first.offset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let note = if ino.compressed { "arquivo comprimido pelo macOS (decmpfs): o conteúdo pode estar incompleto" } else { "versão antiga da árvore ou instantâneo do APFS" };
|
||||||
|
let ts = |t: i64| (t > 0).then_some(t);
|
||||||
|
let nf = NewFound {
|
||||||
|
name,
|
||||||
|
dir,
|
||||||
|
size: ino.size,
|
||||||
|
origin: Origin::Apfs,
|
||||||
|
data: DataLoc::Extents { extents },
|
||||||
|
modified: ts(ino.mtime),
|
||||||
|
created: ts(ino.ctime),
|
||||||
|
accessed: ts(ino.atime),
|
||||||
|
base: if ino.compressed { Confidence::Low } else { Confidence::Medium },
|
||||||
|
note: Some(note),
|
||||||
|
};
|
||||||
|
let f = make_found(ctx, nf);
|
||||||
|
(ctx.emit)(f);
|
||||||
|
}
|
||||||
|
let _ = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arquivos e pastas atuais do primeiro volume sem cifragem (caminho, tamanho, é pasta).
|
||||||
|
pub fn live_paths(src: &dyn Source, base: u64) -> Vec<(String, u64, bool)> {
|
||||||
|
let Some((a, nx)) = open_container(src, base) else { return vec![] };
|
||||||
|
let cont = a.omap(le64(&nx, 160));
|
||||||
|
let mut out = vec![];
|
||||||
|
for i in 0..le32(&nx, 180).min(100) as usize {
|
||||||
|
let oid = le64(&nx, 184 + i * 8);
|
||||||
|
let Some(p) = lookup(&cont, oid, u64::MAX) else { continue };
|
||||||
|
let Some(vb) = a.checked_block(p) else { continue };
|
||||||
|
let Some(v) = parse_volume(&vb) else { continue };
|
||||||
|
if v.encrypted {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let omap = a.omap(v.omap_paddr);
|
||||||
|
let mut files: Files = HashMap::new();
|
||||||
|
let mut ext: Extents = HashMap::new();
|
||||||
|
a.walk_fstree(v.root_oid, &omap, u64::MAX, &mut |k, val, _| add_record(&mut files, &mut ext, k, val));
|
||||||
|
for (&id, f) in &files {
|
||||||
|
if !f.inode.has || id <= 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push((path_of(&files, id), f.inode.size, f.inode.mode & 0o170000 == 0o040000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort();
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
|
//! devolve os extents ainda alocados ("live") para o carving pular e, quando
|
||||||
//! possível, os trechos livres.
|
//! possível, os trechos livres.
|
||||||
|
|
||||||
|
pub mod apfs;
|
||||||
pub mod btrfs;
|
pub mod btrfs;
|
||||||
pub mod exfat;
|
pub mod exfat;
|
||||||
pub mod ext;
|
pub mod ext;
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ pub fn run(
|
|||||||
FsKind::HfsPlus => Some(fs::hfsplus::scan(&mut ctx, p.start, p.len)),
|
FsKind::HfsPlus => Some(fs::hfsplus::scan(&mut ctx, p.start, p.len)),
|
||||||
FsKind::Btrfs => Some(fs::btrfs::scan(&mut ctx, p.start, p.len)),
|
FsKind::Btrfs => Some(fs::btrfs::scan(&mut ctx, p.start, p.len)),
|
||||||
FsKind::Xfs => Some(fs::xfs::scan(&mut ctx, p.start, p.len)),
|
FsKind::Xfs => Some(fs::xfs::scan(&mut ctx, p.start, p.len)),
|
||||||
|
FsKind::Apfs => Some(fs::apfs::scan(&mut ctx, p.start, p.len)),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let _ = progress_phase;
|
let _ = progress_phase;
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ impl FsKind {
|
|||||||
pub fn has_metadata_scanner(self) -> bool {
|
pub fn has_metadata_scanner(self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
FsKind::Ntfs | FsKind::Fat12 | FsKind::Fat16 | FsKind::Fat32 | FsKind::Exfat | FsKind::Ext | FsKind::HfsPlus | FsKind::Btrfs | FsKind::Xfs
|
FsKind::Ntfs | FsKind::Fat12 | FsKind::Fat16 | FsKind::Fat32 | FsKind::Exfat | FsKind::Ext | FsKind::HfsPlus | FsKind::Btrfs | FsKind::Xfs | FsKind::Apfs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
//! APFS: imagem real do dfvfs (4 checkpoints) e exclusão simulada pela troca da raiz da árvore.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use guara_core::fs::apfs::{fletcher64, live_paths};
|
||||||
|
use guara_core::source::FileSource;
|
||||||
|
use guara_core::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn img() -> Option<PathBuf> {
|
||||||
|
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/dfvfs/apfs.raw");
|
||||||
|
p.exists().then_some(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reads_the_live_tree_of_the_reference_image() {
|
||||||
|
let Some(p) = img() else { return };
|
||||||
|
let src = FileSource::open(&p).unwrap();
|
||||||
|
let files = live_paths(&src, 0);
|
||||||
|
eprintln!("{files:#?}");
|
||||||
|
assert!(!files.is_empty());
|
||||||
|
let names: Vec<&str> = files.iter().map(|f| f.0.as_str()).collect();
|
||||||
|
assert!(names.iter().any(|n| n.ends_with("passwords.txt")), "{names:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checksum_matches_real_blocks() {
|
||||||
|
let Some(p) = img() else { return };
|
||||||
|
let d = std::fs::read(p).unwrap();
|
||||||
|
let b = &d[0..4096];
|
||||||
|
assert_eq!(fletcher64(&b[8..]), u64::from_le_bytes(b[0..8].try_into().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quick_scan_is_clean_on_pristine_volume() {
|
||||||
|
let Some(p) = img() else { return };
|
||||||
|
init();
|
||||||
|
let c = scan_path(&p, &ScanOptions { mode: ScanMode::Quick, ..Default::default() });
|
||||||
|
assert_eq!(c.layout.as_ref().unwrap().partitions[0].fs, FsKind::Apfs);
|
||||||
|
eprintln!("{:?}", c.found.iter().map(|f| (&f.name, &f.dir, f.size)).collect::<Vec<_>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- manipulação bruta de nós (independente do módulo) ---
|
||||||
|
|
||||||
|
fn le16(b: &[u8], o: usize) -> usize {
|
||||||
|
u16::from_le_bytes([b[o], b[o + 1]]) as usize
|
||||||
|
}
|
||||||
|
fn le32(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn le64(b: &[u8], o: usize) -> u64 {
|
||||||
|
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entry = (Vec<u8>, Vec<u8>);
|
||||||
|
|
||||||
|
fn read_node(b: &[u8]) -> (u16, Vec<Entry>) {
|
||||||
|
let flags = le16(b, 32) as u16;
|
||||||
|
let n = le32(b, 36) as usize;
|
||||||
|
let (toc_off, toc_len) = (le16(b, 40), le16(b, 42));
|
||||||
|
let root = flags & 1 != 0;
|
||||||
|
let key_base = 56 + toc_off + toc_len;
|
||||||
|
let val_end = b.len() - if root { 40 } else { 0 };
|
||||||
|
let mut out = vec![];
|
||||||
|
for i in 0..n {
|
||||||
|
let o = 56 + toc_off + i * 8;
|
||||||
|
let (ko, kl, vo, vl) = (le16(b, o), le16(b, o + 2), le16(b, o + 4), le16(b, o + 6));
|
||||||
|
out.push((b[key_base + ko..key_base + ko + kl].to_vec(), b[val_end - vo..val_end - vo + vl].to_vec()));
|
||||||
|
}
|
||||||
|
(flags, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_node(old: &[u8], entries: &[Entry]) -> Vec<u8> {
|
||||||
|
let mut b = vec![0u8; old.len()];
|
||||||
|
b[..56].copy_from_slice(&old[..56]);
|
||||||
|
let flags = le16(old, 32) as u16;
|
||||||
|
let root = flags & 1 != 0;
|
||||||
|
if root {
|
||||||
|
let l = old.len();
|
||||||
|
b[l - 40..].copy_from_slice(&old[l - 40..]);
|
||||||
|
}
|
||||||
|
let n = entries.len();
|
||||||
|
b[36..40].copy_from_slice(&(n as u32).to_le_bytes());
|
||||||
|
b[40..42].copy_from_slice(&0u16.to_le_bytes());
|
||||||
|
b[42..44].copy_from_slice(&((n * 8) as u16).to_le_bytes());
|
||||||
|
let key_base = 56 + n * 8;
|
||||||
|
let val_end = old.len() - if root { 40 } else { 0 };
|
||||||
|
let (mut kpos, mut vpos) = (0usize, 0usize);
|
||||||
|
for (i, (k, v)) in entries.iter().enumerate() {
|
||||||
|
vpos += v.len();
|
||||||
|
let o = 56 + i * 8;
|
||||||
|
b[o..o + 2].copy_from_slice(&(kpos as u16).to_le_bytes());
|
||||||
|
b[o + 2..o + 4].copy_from_slice(&(k.len() as u16).to_le_bytes());
|
||||||
|
b[o + 4..o + 6].copy_from_slice(&(vpos as u16).to_le_bytes());
|
||||||
|
b[o + 6..o + 8].copy_from_slice(&(v.len() as u16).to_le_bytes());
|
||||||
|
b[key_base + kpos..key_base + kpos + k.len()].copy_from_slice(k);
|
||||||
|
b[val_end - vpos..val_end - vpos + v.len()].copy_from_slice(v);
|
||||||
|
kpos += k.len();
|
||||||
|
}
|
||||||
|
seal(&mut b);
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seal(b: &mut [u8]) {
|
||||||
|
let c = fletcher64(&b[8..]);
|
||||||
|
b[0..8].copy_from_slice(&c.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_removed_from_the_current_tree_survives_in_the_old_node() {
|
||||||
|
let Some(p) = img() else { return };
|
||||||
|
let mut d = std::fs::read(&p).unwrap();
|
||||||
|
let bs = 4096usize;
|
||||||
|
let blk = |d: &[u8], n: u64| d[n as usize * bs..(n as usize + 1) * bs].to_vec();
|
||||||
|
// Superbloco mais recente do contêiner.
|
||||||
|
let (desc_blocks, desc_base) = (le32(&d, 104) as u64, le64(&d, 112));
|
||||||
|
let mut nx = d[..bs].to_vec();
|
||||||
|
for i in 0..desc_blocks {
|
||||||
|
let b = blk(&d, desc_base + i);
|
||||||
|
if &b[32..36] == b"NXSB" && le64(&b, 16) > le64(&nx, 16) {
|
||||||
|
nx = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// omap do contêiner -> volume
|
||||||
|
let cont_omap = blk(&d, le64(&nx, 160));
|
||||||
|
let cont_tree = blk(&d, le64(&cont_omap, 48));
|
||||||
|
let (_, cont_entries) = read_node(&{
|
||||||
|
// omap usa chaves fixas (16/16): reaproveita o leitor variável convertendo a tabela
|
||||||
|
let mut c = cont_tree.clone();
|
||||||
|
fixed_to_var(&mut c);
|
||||||
|
c
|
||||||
|
});
|
||||||
|
let vol_oid = le64(&nx, 184);
|
||||||
|
let vol_paddr = cont_entries.iter().filter(|(k, _)| le64(k, 0) == vol_oid).map(|(_, v)| le64(v, 8)).last().unwrap();
|
||||||
|
let vsb = blk(&d, vol_paddr);
|
||||||
|
let (vol_omap_paddr, root_oid) = (le64(&vsb, 128), le64(&vsb, 136));
|
||||||
|
let vol_omap = blk(&d, vol_omap_paddr);
|
||||||
|
let vol_tree_paddr = le64(&vol_omap, 48);
|
||||||
|
let mut vt = blk(&d, vol_tree_paddr);
|
||||||
|
let vt_orig = vt.clone();
|
||||||
|
fixed_to_var(&mut vt);
|
||||||
|
let (_, omap_entries) = read_node(&vt);
|
||||||
|
let (root_idx, root_paddr) = omap_entries.iter().enumerate().filter(|(_, (k, _))| le64(k, 0) == root_oid).map(|(i, (_, v))| (i, le64(v, 8))).last().unwrap();
|
||||||
|
let root = blk(&d, root_paddr);
|
||||||
|
assert_eq!(le16(&root, 34), 0, "raiz deveria ser folha nesta imagem");
|
||||||
|
let (_, recs) = read_node(&root);
|
||||||
|
// inode do arquivo
|
||||||
|
let victim = b"passwords.txt";
|
||||||
|
let ino = recs.iter().find_map(|(k, v)| {
|
||||||
|
let hdr = le64(k, 0);
|
||||||
|
((hdr >> 60) == 9 && k.len() > 12 && k[12..].starts_with(victim)).then(|| le64(v, 0))
|
||||||
|
}).expect("entrada de diretório");
|
||||||
|
let private_id = recs.iter().find_map(|(k, v)| ((le64(k, 0) >> 60) == 3 && (le64(k, 0) & 0x0FFF_FFFF_FFFF_FFFF) == ino).then(|| le64(v, 8))).unwrap();
|
||||||
|
let kept: Vec<Entry> = recs
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, v)| {
|
||||||
|
let hdr = le64(k, 0);
|
||||||
|
let (id, ty) = (hdr & 0x0FFF_FFFF_FFFF_FFFF, hdr >> 60);
|
||||||
|
let drec_of_victim = ty == 9 && k.len() > 12 && k[12..].starts_with(victim) && le64(v, 0) == ino;
|
||||||
|
!(id == ino && ty != 9 || id == private_id && ty == 8 || drec_of_victim)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
assert!(kept.len() < recs.len());
|
||||||
|
let newnode = write_node(&root, &kept);
|
||||||
|
// bloco livre para a nova versão
|
||||||
|
let free = (10..d.len() / bs).find(|&n| d[n * bs..(n + 1) * bs].iter().all(|&x| x == 0) && n != vol_tree_paddr as usize).expect("bloco livre");
|
||||||
|
d[free * bs..(free + 1) * bs].copy_from_slice(&newnode);
|
||||||
|
// aponta o mapa de objetos para a nova versão (mesmo xid), editando o valor da entrada
|
||||||
|
let toc_off = le16(&vt_orig, 40);
|
||||||
|
let key_base = 56 + toc_off + le16(&vt_orig, 42);
|
||||||
|
let _ = key_base;
|
||||||
|
let val_end = bs - 40;
|
||||||
|
let vo = le16(&vt_orig, 56 + toc_off + root_idx * 4 + 2);
|
||||||
|
let at = vol_tree_paddr as usize * bs + val_end - vo + 8;
|
||||||
|
d[at..at + 8].copy_from_slice(&(free as u64).to_le_bytes());
|
||||||
|
let mut ob = d[vol_tree_paddr as usize * bs..(vol_tree_paddr as usize + 1) * bs].to_vec();
|
||||||
|
seal(&mut ob);
|
||||||
|
d[vol_tree_paddr as usize * bs..(vol_tree_paddr as usize + 1) * bs].copy_from_slice(&ob);
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let p2 = dir.path().join("a.img");
|
||||||
|
std::fs::write(&p2, &d).unwrap();
|
||||||
|
// O arquivo sumiu da árvore atual...
|
||||||
|
let src = FileSource::open(&p2).unwrap();
|
||||||
|
assert!(!live_paths(&src, 0).iter().any(|f| f.0 == "passwords.txt"));
|
||||||
|
init();
|
||||||
|
let c = scan_path(&p2, &ScanOptions { mode: ScanMode::Deep, ..Default::default() });
|
||||||
|
let f = c.found.iter().find(|f| f.origin == Origin::Apfs && f.name == "passwords.txt").unwrap_or_else(|| panic!("{:?}", c.found.iter().map(|f| (&f.name, f.origin)).collect::<Vec<_>>()));
|
||||||
|
assert_eq!(f.size, 116);
|
||||||
|
let out = dir.path().join("out");
|
||||||
|
let rep = recover_all(&p2, std::slice::from_ref(f), &out);
|
||||||
|
assert!(rep.failed.is_empty());
|
||||||
|
let rp = PathBuf::from(&rep.recovered[0].path);
|
||||||
|
let got = std::fs::read(if rp.is_absolute() { rp } else { out.join(rp) }).unwrap();
|
||||||
|
assert_eq!(got.len(), 116);
|
||||||
|
assert!(String::from_utf8_lossy(&got).chars().all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t'), "{got:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converte a tabela de entradas fixas (4 bytes) de um nó de omap para o formato variável (8 bytes) em memória.
|
||||||
|
fn fixed_to_var(b: &mut Vec<u8>) {
|
||||||
|
let flags = le16(b, 32) as u16;
|
||||||
|
if flags & 4 == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let n = le32(b, 36) as usize;
|
||||||
|
let (toc_off, toc_len) = (le16(b, 40), le16(b, 42));
|
||||||
|
let key_base = 56 + toc_off + toc_len;
|
||||||
|
let root = flags & 1 != 0;
|
||||||
|
let val_end = b.len() - if root { 40 } else { 0 };
|
||||||
|
let mut entries: Vec<(Vec<u8>, Vec<u8>)> = vec![];
|
||||||
|
for i in 0..n {
|
||||||
|
let o = 56 + toc_off + i * 4;
|
||||||
|
let (ko, vo) = (le16(b, o), le16(b, o + 2));
|
||||||
|
entries.push((b[key_base + ko..key_base + ko + 16].to_vec(), b[val_end - vo..val_end - vo + 16].to_vec()));
|
||||||
|
}
|
||||||
|
let mut nb = b.clone();
|
||||||
|
nb[32..34].copy_from_slice(&(flags & !4).to_le_bytes());
|
||||||
|
*b = write_node_flagsless(&nb, &entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_node_flagsless(old: &[u8], entries: &[Entry]) -> Vec<u8> {
|
||||||
|
let mut b = write_node(old, entries);
|
||||||
|
// write_node usa as flags de `old`
|
||||||
|
let fl = le16(&b, 32) as u16;
|
||||||
|
b[32..34].copy_from_slice(&fl.to_le_bytes());
|
||||||
|
b
|
||||||
|
}
|
||||||
Reference in new issue
Block a user