From 3a6b0c7b748c228b99e7302c4ef4a309707c0ef1 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Fri, 23 Oct 2026 07:55:18 -0300 Subject: [PATCH 1/2] =?UTF-8?q?Depend=C3=AAncias:=20ctutils=200.4.5=20(RUS?= =?UTF-8?q?TSEC-2026-0385)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comparação em tempo constante corrigida na 0.4.4; o job de auditoria falhava. Co-Authored-By: Claude Sonnet 5.5 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0725c0..0f6513e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,9 +309,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", ] From 4bec3e971a6929ddb8b0336176de7c19f598cdd3 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Fri, 23 Oct 2026 07:55:58 -0300 Subject: [PATCH 2/2] Discos virtuais: arquivo de apoio do QCOW2, snapshots de VMDK e DMG com LZFSE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QCOW2 e VMDK filhos leem o pai no que não alocaram (o QCOW2 respeita o formato do apoio; o VMDK acha o pai por nome ou, no VirtualBox, por UUID, e recusa o filho se o CID do pai mudou). Chunks LZFSE do DMG usam lzfse_rust, conferidos com dados do compressor de referência da Apple. Snapshot real do VirtualBox entra como fixture. Co-Authored-By: Claude Sonnet 5.5 --- Cargo.lock | 7 + ROADMAP.md | 3 +- ROADMAP.pt-BR.md | 3 +- crates/guara-core/Cargo.toml | 1 + crates/guara-core/src/vdisk/dmg.rs | 7 +- crates/guara-core/src/vdisk/mod.rs | 22 +- crates/guara-core/src/vdisk/qcow2.rs | 87 ++++++- crates/guara-core/src/vdisk/vhd.rs | 2 +- crates/guara-core/src/vdisk/vhdx.rs | 2 +- crates/guara-core/src/vdisk/vmdk.rs | 187 +++++++++++---- .../tests/fixtures/vdisk/lzfse-64k.bin | Bin 0 -> 65536 bytes .../tests/fixtures/vdisk/lzfse-64k.bin.lzfse | Bin 0 -> 22091 bytes .../tests/fixtures/vdisk/lzfse-small.bin | Bin 0 -> 2900 bytes .../fixtures/vdisk/lzfse-small.bin.lzfse | Bin 0 -> 2056 bytes .../tests/fixtures/vdisk/vbox-snapshot.vmdk | Bin 0 -> 65536 bytes crates/guara-core/tests/vdisk.rs | 218 +++++++++++++++++- docs/pt-BR/suporte.md | 8 +- docs/support.md | 8 +- 18 files changed, 469 insertions(+), 86 deletions(-) create mode 100644 crates/guara-core/tests/fixtures/vdisk/lzfse-64k.bin create mode 100644 crates/guara-core/tests/fixtures/vdisk/lzfse-64k.bin.lzfse create mode 100644 crates/guara-core/tests/fixtures/vdisk/lzfse-small.bin create mode 100644 crates/guara-core/tests/fixtures/vdisk/lzfse-small.bin.lzfse create mode 100644 crates/guara-core/tests/fixtures/vdisk/vbox-snapshot.vmdk diff --git a/Cargo.lock b/Cargo.lock index 0f6513e..b0be34d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -430,6 +430,7 @@ dependencies = [ "jpeg-encoder", "libc", "lz4_flex", + "lzfse_rust", "md-5", "mp4", "pbkdf2", @@ -582,6 +583,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226" +[[package]] +name = "lzfse_rust" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5edeac3ec657925dececc672bdd58fa2212291886ddcb3e08aa46924c78340c6" + [[package]] name = "md-5" version = "0.11.0" diff --git a/ROADMAP.md b/ROADMAP.md index 5c24832..4824956 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -68,7 +68,8 @@ Today Guará opens volumes when you **already have** the password or key. Passwo | E01 and Ex01: read, write and `guara verify` | ✅ | | AFF4: read and write | ✅ | | Virtual disk containers: QCOW2, VMDK, VHD, VHDX and DMG (read) | ✅ | -| QCOW2 with a backing file, VMDK snapshots, DMG with LZFSE or a password | 🔜 | +| QCOW2 with a backing file, VMDK snapshots (by name or VirtualBox UUID), DMG with LZFSE | ✅ | +| Password-protected DMG, parent identity check for VHD, VHDX and QCOW2 | 🔜 | | Mobile device image (see item 7) | 🔭 | ### 4. Assembling loose disks diff --git a/ROADMAP.pt-BR.md b/ROADMAP.pt-BR.md index 88e6fa7..7899fba 100644 --- a/ROADMAP.pt-BR.md +++ b/ROADMAP.pt-BR.md @@ -68,7 +68,8 @@ Hoje o Guará abre volumes quando você **já tem** a senha ou a chave. Recupera | E01 e Ex01: leitura, gravação e `guara verify` | ✅ | | AFF4: leitura e gravação | ✅ | | Contêineres de disco virtual: QCOW2, VMDK, VHD, VHDX e DMG (leitura) | ✅ | -| QCOW2 com arquivo de apoio, snapshots de VMDK, DMG com LZFSE ou com senha | 🔜 | +| QCOW2 com arquivo de apoio, snapshots de VMDK (por nome ou UUID do VirtualBox), DMG com LZFSE | ✅ | +| DMG com senha, conferência da identidade do pai em VHD, VHDX e QCOW2 | 🔜 | | Imagem de dispositivo móvel (ver item 7) | 🔭 | ### 4. Montagem de discos soltos diff --git a/crates/guara-core/Cargo.toml b/crates/guara-core/Cargo.toml index 5aa5bc1..e3b6b12 100644 --- a/crates/guara-core/Cargo.toml +++ b/crates/guara-core/Cargo.toml @@ -29,6 +29,7 @@ hmac = "0.13" pbkdf2 = { version = "0.13", default-features = false, features = ["hmac"] } ripemd = "0.2" argon2 = "0.6" +lzfse_rust = "0.2.1" [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/crates/guara-core/src/vdisk/dmg.rs b/crates/guara-core/src/vdisk/dmg.rs index baad42d..664f9c0 100644 --- a/crates/guara-core/src/vdisk/dmg.rs +++ b/crates/guara-core/src/vdisk/dmg.rs @@ -1,6 +1,6 @@ //! DMG (Apple UDIF): rodapé `koly`, tabelas `mish` do plist e chunks zerados, brutos, ADC, -//! zlib e bzip2. Imagens cifradas (`encrcdsa`) e chunks LZFSE (`ULFO`) são recusados com -//! mensagem clara. +//! zlib, bzip2 e LZFSE (`ULFO`). Imagens cifradas (`encrcdsa`) são recusadas com mensagem +//! clara. use super::{bad, be32, be64, read_exact, read_vec, Backend}; use crate::source::{FileSource, Source}; @@ -187,7 +187,8 @@ impl Dmg { bzip2::read::BzDecoder::new(&raw[..]).read_to_end(&mut out)?; } 0x8000_0007 => { - return Err(bad("chunk LZFSE (formato ULFO) ainda não é suportado")); + lzfse_rust::decode_bytes(&raw, &mut out) + .map_err(|e| bad(format!("chunk LZFSE inválido: {e}")))?; } t => return Err(bad(format!("tipo de chunk DMG 0x{t:08x} desconhecido"))), } diff --git a/crates/guara-core/src/vdisk/mod.rs b/crates/guara-core/src/vdisk/mod.rs index 000d253..eed5bc9 100644 --- a/crates/guara-core/src/vdisk/mod.rs +++ b/crates/guara-core/src/vdisk/mod.rs @@ -2,9 +2,9 @@ //! //! Cada formato é aberto como o disco que contém, somente para leitura, e entra na cadeia //! normal (partições, sistemas de arquivos, cifras) por meio de [`Source`]. Discos -//! diferenciais (VHD e VHDX com pai) seguem a cadeia até o disco base; QCOW2 e VMDK com -//! arquivo de apoio são recusados com mensagem clara, porque ler só a camada filha -//! devolveria dados errados. +//! diferenciais (VHD e VHDX com pai, QCOW2 com arquivo de apoio, VMDK com pai) seguem a +//! cadeia até o disco base. A identidade do pai (UUID, CID, data) não é conferida: se o pai foi +//! alterado depois do filho, os dados lidos refletem o pai atual. mod dmg; mod qcow2; @@ -130,8 +130,8 @@ pub(crate) fn open_depth(path: &Path, depth: u32) -> Result> { }; let name = format!("{format} {}", path.display()); let inner: Box = match format { - "QCOW2" => Box::new(qcow2::Qcow2::open(file).map_err(|e| io_err(e, &name))?), - "VMDK" => Box::new(vmdk::Vmdk::open(file, path).map_err(|e| io_err(e, &name))?), + "QCOW2" => Box::new(qcow2::Qcow2::open(file, path, depth).map_err(|e| io_err(e, &name))?), + "VMDK" => Box::new(vmdk::Vmdk::open(file, path, depth).map_err(|e| io_err(e, &name))?), "VHD" => Box::new(vhd::Vhd::open(file, path, depth).map_err(|e| io_err(e, &name))?), "VHDX" => Box::new(vhdx::Vhdx::open(file, path, depth).map_err(|e| io_err(e, &name))?), _ => Box::new(dmg::Dmg::open(file).map_err(|e| io_err(e, &name))?), @@ -139,17 +139,23 @@ pub(crate) fn open_depth(path: &Path, depth: u32) -> Result> { Ok(Some(VDisk { inner, format })) } -/// Abre o disco pai de um VHD/VHDX diferencial. +/// Abre o disco pai (VHD/VHDX diferencial, QCOW2 com apoio, VMDK com pai). Com `raw`, o +/// arquivo é tratado como disco bruto sem tentar reconhecer o formato. pub(crate) fn open_parent( candidates: &[std::path::PathBuf], depth: u32, + raw: bool, ) -> io::Result> { for p in candidates { if !p.is_file() { continue; } - if let Some(v) = open_depth(p, depth + 1).map_err(|e| bad(e.to_string()))? { - return Ok(Box::new(v)); + if !raw { + if let Some(v) = open_depth(p, depth + 1).map_err(|e| bad(e.to_string()))? { + return Ok(Box::new(v)); + } + } else if depth > MAX_DEPTH { + return Err(bad("cadeia de discos pai longa demais ou circular")); } let f = FileSource::open(p)?; return Ok(Box::new(f)); diff --git a/crates/guara-core/src/vdisk/qcow2.rs b/crates/guara-core/src/vdisk/qcow2.rs index b84b31a..5e446f1 100644 --- a/crates/guara-core/src/vdisk/qcow2.rs +++ b/crates/guara-core/src/vdisk/qcow2.rs @@ -1,10 +1,15 @@ -//! QCOW2 (QEMU), versões 2 e 3: tabelas L1/L2, clusters comprimidos (deflate e zstd) e -//! clusters zerados. Imagens cifradas, com arquivo de apoio, com arquivo de dados externo ou -//! com L2 estendido são recusadas. +//! QCOW2 (QEMU), versões 2 e 3: tabelas L1/L2, clusters comprimidos (deflate e zstd), +//! clusters zerados e arquivo de apoio (backing file): o que o filho não alocou vem do pai. +//! Imagens cifradas, com arquivo de dados externo, com L2 estendido ou com apoio que não é um +//! arquivo local (`json:`, `nbd:`, URLs) são recusadas. -use super::{bad, be32, be64, check_table, read_exact, read_vec, Backend}; +use super::{ + bad, be32, be64, candidate_paths, check_table, open_parent, read_exact, read_parent, read_vec, + Backend, +}; use crate::source::{FileSource, Source}; use std::io::{self, Read}; +use std::path::Path; use std::sync::Mutex; const OFFSET_MASK: u64 = 0x00ff_ffff_ffff_fe00; @@ -16,12 +21,13 @@ pub(super) struct Qcow2 { cluster_bits: u32, l1: Vec, zstd: bool, + parent: Option>, /// Último cluster comprimido decodificado: (deslocamento do cluster virtual, dados). cache: Mutex)>>, } impl Qcow2 { - pub(super) fn open(file: FileSource) -> io::Result { + pub(super) fn open(file: FileSource, path: &Path, depth: u32) -> io::Result { let h = read_vec(&file, 0, 112.min(file.size() as usize))?; if h.len() < 72 { return Err(bad("cabeçalho QCOW2 truncado")); @@ -30,11 +36,6 @@ impl Qcow2 { if version != 2 && version != 3 { return Err(bad(format!("versão QCOW {version} não suportada"))); } - if be64(&h, 8) != 0 { - return Err(bad( - "a imagem QCOW2 usa arquivo de apoio (backing file); abrir só a camada filha daria dados errados", - )); - } let cluster_bits = be32(&h, 20); if !(9..=21).contains(&cluster_bits) { return Err(bad(format!( @@ -63,6 +64,11 @@ impl Qcow2 { } } } + let parent = if be64(&h, 8) != 0 { + Some(Self::open_backing(&file, &h, version, path, depth)?) + } else { + None + }; let l1_size = be32(&h, 36) as u64; let l1_off = be64(&h, 40); let n = check_table(l1_size, 8, "tabela L1")?; @@ -74,10 +80,65 @@ impl Qcow2 { cluster_bits, l1, zstd, + parent, cache: Mutex::new(None), }) } + /// Abre o arquivo de apoio. O formato vem da extensão de cabeçalho `0xE2792ACA`; `raw` + /// (ou ausente em v2) é lido sem reconhecer formato, como o QEMU faz. + fn open_backing( + file: &FileSource, + h: &[u8], + version: u32, + path: &Path, + depth: u32, + ) -> io::Result> { + let (off, len) = (be64(h, 8), be32(h, 16) as usize); + if len == 0 || len > 1023 { + return Err(bad("nome do arquivo de apoio QCOW2 inválido")); + } + let name = String::from_utf8_lossy(&read_vec(file, off, len)?).into_owned(); + if name.starts_with("json:") + || name.contains("://") + || name.split(':').next().is_some_and(|p| { + matches!(p, "nbd" | "ssh" | "rbd" | "sheepdog" | "gluster" | "iscsi") + }) + { + return Err(bad(format!( + "o arquivo de apoio {name} não é um arquivo local; só caminhos de arquivo são suportados" + ))); + } + // extensões de cabeçalho: depois do cabeçalho, alinhadas a 8 bytes + let hlen = if version == 3 && h.len() >= 104 { + be32(h, 100) as u64 + } else { + 72 + }; + let mut raw = false; + let mut pos = hlen.div_ceil(8) * 8; + for _ in 0..64 { + let mut eh = [0u8; 8]; + if read_exact(file, pos, &mut eh).is_err() { + break; + } + let (kind, elen) = (be32(&eh, 0), be32(&eh, 4) as u64); + if kind == 0 || elen > 1 << 20 { + break; + } + if kind == 0xE279_2ACA { + let fmt = read_vec(file, pos + 8, elen as usize)?; + raw = fmt == b"raw"; + } + pos += 8 + elen.div_ceil(8) * 8; + } + if version == 2 { + raw = false; + } + let dir = path.parent().unwrap_or(Path::new(".")); + open_parent(&candidate_paths(dir, &[name]), depth, raw) + } + fn cluster_size(&self) -> u64 { 1 << self.cluster_bits } @@ -139,8 +200,10 @@ impl Backend for Qcow2 { return Ok(n); } let off = entry & OFFSET_MASK; - if off == 0 || entry & 1 != 0 { - buf[..n].fill(0); + if entry & 1 != 0 { + buf[..n].fill(0); // zerado de propósito: não consulta o pai + } else if off == 0 { + read_parent(&self.parent, voff, &mut buf[..n])?; } else { read_exact(&self.file, off + within, &mut buf[..n])?; } diff --git a/crates/guara-core/src/vdisk/vhd.rs b/crates/guara-core/src/vdisk/vhd.rs index 204cb43..c25cb0a 100644 --- a/crates/guara-core/src/vdisk/vhd.rs +++ b/crates/guara-core/src/vdisk/vhd.rs @@ -112,7 +112,7 @@ impl Vhd { } } names.push(utf16(&dh[64..64 + 512], true)); - Some(open_parent(&candidate_paths(dir, &names), depth)?) + Some(open_parent(&candidate_paths(dir, &names), depth, false)?) } else { None }; diff --git a/crates/guara-core/src/vdisk/vhdx.rs b/crates/guara-core/src/vdisk/vhdx.rs index ff8063b..75b801c 100644 --- a/crates/guara-core/src/vdisk/vhdx.rs +++ b/crates/guara-core/src/vdisk/vhdx.rs @@ -130,7 +130,7 @@ impl Vhdx { } } let dir = path.parent().unwrap_or(Path::new(".")); - Some(open_parent(&candidate_paths(dir, &names), depth)?) + Some(open_parent(&candidate_paths(dir, &names), depth, false)?) } else { None }; diff --git a/crates/guara-core/src/vdisk/vmdk.rs b/crates/guara-core/src/vdisk/vmdk.rs index c6baf22..b7078c8 100644 --- a/crates/guara-core/src/vdisk/vmdk.rs +++ b/crates/guara-core/src/vdisk/vmdk.rs @@ -1,9 +1,12 @@ //! VMDK (VMware, VirtualBox): extensões esparsas (inclusive `streamOptimized` com grãos //! comprimidos) e descritores de texto com extensões `FLAT`, `SPARSE` e `ZERO`, como -//! `monolithicFlat` e `twoGbMaxExtentSparse`. Discos com pai (snapshots) e o formato SEsparse -//! são recusados. +//! `monolithicFlat` e `twoGbMaxExtentSparse`. Discos com pai (snapshots) leem o pai nos grãos +//! que o filho não alocou; o formato SEsparse é recusado. -use super::{bad, check_table, le16, le32, le64, read_exact, read_vec, Backend}; +use super::{ + bad, candidate_paths, check_table, le16, le32, le64, open_parent, read_exact, read_parent, + read_vec, Backend, +}; use crate::source::{FileSource, Source}; use std::io::{self, Read}; use std::path::Path; @@ -36,20 +39,73 @@ struct Extent { pub(super) struct Vmdk { extents: Vec, size: u64, + parent: Option>, } -fn parent_in(descriptor: &str) -> bool { - descriptor.lines().any(|l| { - let l = l.trim(); - l.strip_prefix("parentCID") +/// Valor de uma chave `chave=valor` do descritor, sem aspas. +fn desc_value(descriptor: &str, key: &str) -> Option { + descriptor.lines().find_map(|l| { + l.trim() + .strip_prefix(key) .and_then(|r| r.trim_start().strip_prefix('=')) - .is_some_and(|v| { - let v = v.trim().trim_matches('"'); - !v.is_empty() && !v.eq_ignore_ascii_case("ffffffff") - }) + .map(|v| v.trim().trim_matches('"').to_string()) }) } +/// Descritor (texto) de um arquivo VMDK: o próprio arquivo, se for descritor, ou o embutido +/// logo depois do cabeçalho de uma extensão esparsa. +fn descriptor_of(path: &Path) -> Option { + let f = FileSource::open(path).ok()?; + let head = read_vec(&f, 0, (f.size() as usize).min(24 * 512)).ok()?; + if head.starts_with(b"KDMV") { + let off = (le64(&head, 28) * SECTOR) as usize; + let len = (le64(&head, 36) * SECTOR) as usize; + let d = head.get(off..(off + len).min(head.len()))?; + let end = d.iter().position(|&b| b == 0).unwrap_or(d.len()); + return Some(String::from_utf8_lossy(&d[..end]).into_owned()); + } + head.starts_with(b"# Disk Desc") + .then(|| String::from_utf8_lossy(&head).into_owned()) +} + +/// Como o descritor identifica o pai: pelo nome (`parentFileNameHint`) e/ou, no VirtualBox, só +/// pelo UUID (`ddb.uuid.parent`); `cid` é o `parentCID` que o pai precisa ter. +struct ParentRef { + name: Option, + uuid: Option, + cid: Option, +} + +fn parent_ref(descriptor: &str) -> Option { + let cid = desc_value(descriptor, "parentCID") + .filter(|c| !c.is_empty() && !c.eq_ignore_ascii_case("ffffffff")); + let uuid = desc_value(descriptor, "ddb.uuid.parent") + .filter(|u| !u.is_empty() && u.chars().any(|c| c != '0' && c != '-')); + let name = desc_value(descriptor, "parentFileNameHint").filter(|n| !n.is_empty()); + (cid.is_some() || uuid.is_some()).then_some(ParentRef { name, uuid, cid }) +} + +/// Procura, na pasta do filho e nas duas acima, um VMDK cujo `ddb.uuid.image` seja `uuid`. +fn find_by_uuid(dir: &Path, uuid: &str) -> Option { + let mut d = Some(dir); + for _ in 0..3 { + let cur = d?; + for e in std::fs::read_dir(cur).ok()?.flatten() { + let p = e.path(); + if p.extension() + .is_some_and(|x| x.eq_ignore_ascii_case("vmdk")) + && descriptor_of(&p) + .and_then(|ds| desc_value(&ds, "ddb.uuid.image")) + .is_some_and(|u| u.eq_ignore_ascii_case(uuid)) + { + return Some(p); + } + } + d = cur.parent(); + } + None +} + impl Sparse { fn open(file: FileSource) -> io::Result<(Self, u64, Option)> { let mut h = read_vec(&file, 0, SECTOR as usize)?; @@ -95,11 +151,6 @@ impl Sparse { } else { None }; - if descriptor.as_deref().is_some_and(parent_in) { - return Err(bad( - "VMDK com disco pai (snapshot); abrir só a camada filha daria dados errados", - )); - } let gde = capacity.div_ceil(gtes * grain); let n = check_table(gde, 4, "diretório de grãos")?; let raw = read_vec(&file, gd_off * SECTOR, n * 4)?; @@ -118,7 +169,9 @@ impl Sparse { )) } - fn read(&self, off: u64, buf: &mut [u8]) -> io::Result { + /// Lê dentro de um grão. O `bool` é `false` quando o grão não está alocado neste arquivo + /// (o chamador completa com o pai); grão zerado de propósito devolve zeros. + fn read(&self, off: u64, buf: &mut [u8]) -> io::Result<(usize, bool)> { let g = off / self.grain; let within = off % self.grain; let n = buf.len().min((self.grain - within) as usize); @@ -129,13 +182,16 @@ impl Sparse { read_exact(&self.file, gt_sector * SECTOR + (g % self.gtes) * 4, &mut b)?; sector = u32::from_le_bytes(b) as u64; } - if sector <= 1 { + if sector == 0 { + return Ok((n, false)); + } + if sector == 1 { buf[..n].fill(0); - return Ok(n); + return Ok((n, true)); } if !self.compressed { read_exact(&self.file, sector * SECTOR + within, &mut buf[..n])?; - return Ok(n); + return Ok((n, true)); } let mut cache = self.cache.lock().unwrap(); if cache.as_ref().map(|c| c.0) != Some(g) { @@ -153,17 +209,12 @@ impl Sparse { } let data = &cache.as_ref().unwrap().1; buf[..n].copy_from_slice(&data[within as usize..within as usize + n]); - Ok(n) + Ok((n, true)) } } /// Extensões de um descritor de texto: `RW 2048 FLAT "disco-f001.vmdk" 0`. fn parse_descriptor(text: &str, dir: &Path) -> io::Result> { - if parent_in(text) { - return Err(bad( - "VMDK com disco pai (snapshot); abrir só a camada filha daria dados errados", - )); - } let mut out = vec![]; let mut start = 0u64; for line in text.lines() { @@ -216,27 +267,79 @@ fn parse_descriptor(text: &str, dir: &Path) -> io::Result> { } impl Vmdk { - pub(super) fn open(file: FileSource, path: &Path) -> io::Result { + fn open_parent_of(r: &ParentRef, dir: &Path, depth: u32) -> io::Result> { + let mut found = r + .name + .as_ref() + .map(|n| candidate_paths(dir, std::slice::from_ref(n))) + .unwrap_or_default(); + let by_name = found.iter().any(|p| p.is_file()); + if !by_name { + if let Some(p) = r.uuid.as_deref().and_then(|u| find_by_uuid(dir, u)) { + found = vec![p]; + } + } + if r.name.is_none() && r.uuid.is_none() { + return Err(bad( + "VMDK com pai, mas o descritor não diz qual (sem parentFileNameHint nem ddb.uuid.parent)", + )); + } + if r.name.is_none() && found.is_empty() { + return Err(bad( + "VMDK com pai identificado só por UUID (VirtualBox) e o pai não está na pasta do filho nem nas duas acima", + )); + } + if let Some(cid) = &r.cid { + // o pai precisa ser o mesmo de quando o filho foi criado + if let Some(p) = found.iter().find(|p| p.is_file()) { + let have = descriptor_of(p).and_then(|d| desc_value(&d, "CID")); + if have + .as_deref() + .is_some_and(|h| !h.eq_ignore_ascii_case(cid)) + { + return Err(bad(format!( + "o pai {} foi alterado depois do filho (CID {} em vez de {cid}); os dados do filho não valem mais", + p.display(), + have.unwrap_or_default() + ))); + } + } + } + open_parent(&found, depth, false) + } + + pub(super) fn open(file: FileSource, path: &Path, depth: u32) -> io::Result { let mut head = [0u8; 4]; let _ = file.read_at(0, &mut head); - let extents = if &head == b"KDMV" { - let (sp, size, _) = Sparse::open(file)?; - vec![Extent { - start: 0, - len: size, - kind: Kind::Sparse(sp), - }] + let dir = path.parent().unwrap_or(Path::new(".")); + let (extents, descriptor) = if &head == b"KDMV" { + let (sp, size, descriptor) = Sparse::open(file)?; + ( + vec![Extent { + start: 0, + len: size, + kind: Kind::Sparse(sp), + }], + descriptor, + ) } else { if file.size() > 1 << 20 { return Err(bad("descritor VMDK grande demais")); } let raw = read_vec(&file, 0, file.size() as usize)?; - let text = String::from_utf8_lossy(&raw); - let dir = path.parent().unwrap_or(Path::new(".")); - parse_descriptor(&text, dir)? + let text = String::from_utf8_lossy(&raw).into_owned(); + (parse_descriptor(&text, dir)?, Some(text)) + }; + let parent = match descriptor.as_deref().and_then(parent_ref) { + Some(r) => Some(Self::open_parent_of(&r, dir, depth)?), + None => None, }; let size = extents.iter().map(|e| e.len).sum(); - Ok(Self { extents, size }) + Ok(Self { + extents, + size, + parent, + }) } } @@ -261,7 +364,13 @@ impl Backend for Vmdk { read_exact(file, offset + rel, &mut buf[..n])?; Ok(n) } - Kind::Sparse(s) => s.read(rel, &mut buf[..n]), + Kind::Sparse(s) => { + let (got, allocated) = s.read(rel, &mut buf[..n])?; + if !allocated { + read_parent(&self.parent, voff, &mut buf[..got])?; + } + Ok(got) + } } } } diff --git a/crates/guara-core/tests/fixtures/vdisk/lzfse-64k.bin b/crates/guara-core/tests/fixtures/vdisk/lzfse-64k.bin new file mode 100644 index 0000000000000000000000000000000000000000..bcd1dfaf8884265d758de61463f4a806337b9d4c GIT binary patch literal 65536 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 literal 0 HcmV?d00001 diff --git a/crates/guara-core/tests/fixtures/vdisk/lzfse-64k.bin.lzfse b/crates/guara-core/tests/fixtures/vdisk/lzfse-64k.bin.lzfse new file mode 100644 index 0000000000000000000000000000000000000000..2a774e29ba06eb0b52a608abe82b9ff074975d18 GIT binary patch literal 22091 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 ic@R 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 Q0@44MU_1m^L%iAk2l7Px# literal 0 HcmV?d00001 diff --git a/crates/guara-core/tests/fixtures/vdisk/lzfse-small.bin b/crates/guara-core/tests/fixtures/vdisk/lzfse-small.bin new file mode 100644 index 0000000000000000000000000000000000000000..ed605f91cf0fcf1055bf18baa7fbf2ce7a661620 GIT binary patch literal 2900 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 literal 0 HcmV?d00001 diff --git a/crates/guara-core/tests/fixtures/vdisk/vbox-snapshot.vmdk b/crates/guara-core/tests/fixtures/vdisk/vbox-snapshot.vmdk new file mode 100644 index 0000000000000000000000000000000000000000..c8f22786984d0eab9be7757d2ba4dbb56dcf4bd8 GIT binary patch literal 65536 zcmeIx&uiR96aZkgxfDTfX%6j0$Y&YtTD!InduX~TDWy>2kep|wQ7q#1nqAq%ltTXW zYQ1Z4O%A!_knjy?M!cstKRz*k&i{JzU6LeEh9~)9+a<}zyFN*_@ojs$88Fo z{_}qPc%MtNAJhAH<5weakj=Zz`)nRJ^}4(2SL@%qiv-93L+!q$^*V+=zP`PRvwXR_TwQeic~_qeUv6TarU%)J5B*S+HTwmIS<_c< zvvFCSWM_Z>`s(aOmjBaE#!X!ww?$l(v8YCERfO`i9=g$ZTE&xi6ej;3-YlEJT zk6ClK>(GbndDz4(J($m*r)kqH4sW`3e;qDfzJ0ShL^jLvD!)sGCS3J#z1wZq3>(}Q zyo;-4?AN!4_3cG>*$h>)eEcYN9z!#PN7F~~O{@pAAu~PNWv;Kg=CE6acZ0|09P2^V zPIs2qb*N72vMDB^IV!4gT^5T;oD`vr;kYa(E_~p}K zQ33=A5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N a0t5&UAV7cs0RjXF5FkK+009DDMBqQnC9}c+ literal 0 HcmV?d00001 diff --git a/crates/guara-core/tests/vdisk.rs b/crates/guara-core/tests/vdisk.rs index 211254b..7aebafc 100644 --- a/crates/guara-core/tests/vdisk.rs +++ b/crates/guara-core/tests/vdisk.rs @@ -281,22 +281,82 @@ fn qcow2_with_zero_raw_and_compressed_clusters() { } #[test] -fn qcow2_with_backing_file_or_encryption_is_refused() { +fn qcow2_with_encryption_or_a_remote_backing_file_is_refused() { let dir = tempfile::tempdir().unwrap(); - let mut img = build_qcow2(&raw()); - let p = dir.path().join("b.qcow2"); - be(&mut img, 8, 4096, 8); - be(&mut img, 16, 3, 4); - std::fs::write(&p, &img).unwrap(); - let e = open_path(&p).err().expect("deveria recusar").to_string(); - assert!(e.contains("arquivo de apoio"), "{e}"); - let mut img = build_qcow2(&raw()); be(&mut img, 32, 2, 4); let p = dir.path().join("e.qcow2"); std::fs::write(&p, &img).unwrap(); let e = open_path(&p).err().expect("deveria recusar").to_string(); assert!(e.contains("LUKS"), "{e}"); + + let mut img = build_qcow2(&raw()); + let name = b"nbd://servidor/disco"; + img[512..512 + name.len()].copy_from_slice(name); + be(&mut img, 8, 512, 8); + be(&mut img, 16, name.len() as u64, 4); + let p = dir.path().join("r.qcow2"); + std::fs::write(&p, &img).unwrap(); + let e = open_path(&p).err().expect("deveria recusar").to_string(); + assert!(e.contains("não é um arquivo local"), "{e}"); +} + +/// Filho QCOW2 que só aloca o cluster 1 (dados), marca o 2 como zerado e deixa o resto para o pai. +fn build_qcow2_child(backing: &str, fmt: Option<&str>, own: &[u8]) -> Vec { + const CS: usize = 1 << 16; + let mut img = vec![0u8; 3 * CS]; + img[..4].copy_from_slice(b"QFI\xfb"); + be(&mut img, 4, 3, 4); + be(&mut img, 16, backing.len() as u64, 4); + be(&mut img, 8, 1024, 8); + img[1024..1024 + backing.len()].copy_from_slice(backing.as_bytes()); + be(&mut img, 20, 16, 4); + be(&mut img, 24, 16 * CS as u64, 8); + be(&mut img, 36, 1, 4); + be(&mut img, 40, CS as u64, 8); + be(&mut img, 100, 112, 4); + if let Some(f) = fmt { + // extensão 0xE2792ACA com o nome do formato do apoio, depois o fim das extensões + be(&mut img, 112, 0xE279_2ACA, 4); + be(&mut img, 116, f.len() as u64, 4); + img[120..120 + f.len()].copy_from_slice(f.as_bytes()); + } + be(&mut img, CS, (2 * CS as u64) | (1 << 63), 8); + let host = img.len() as u64; + img.extend_from_slice(own); + img.resize(img.len().div_ceil(CS) * CS, 0); + be(&mut img, 2 * CS + 8, host | (1 << 63), 8); // cluster 1 + be(&mut img, 2 * CS + 16, 1, 8); // cluster 2: zerado de propósito + img +} + +#[test] +fn qcow2_backing_file_supplies_what_the_child_did_not_allocate() { + const CS: usize = 1 << 16; + let base = raw(); + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("base.raw"), &base).unwrap(); + let own = vec![0x42u8; CS]; + let mut expect = base.clone(); + expect[CS..2 * CS].copy_from_slice(&own); + expect[2 * CS..3 * CS].fill(0); + for (name, fmt) in [("a.qcow2", Some("raw")), ("b.qcow2", None)] { + let p = dir.path().join(name); + std::fs::write(&p, build_qcow2_child("base.raw", fmt, &own)).unwrap(); + check_content(&p, &expect); + } + // com o pai em outro formato: o filho aponta para um QCOW2 + std::fs::write(dir.path().join("base.qcow2"), build_qcow2(&base)).unwrap(); + let p = dir.path().join("c.qcow2"); + std::fs::write(&p, build_qcow2_child("base.qcow2", Some("qcow2"), &own)).unwrap(); + check_content(&p, &expect); + // sem o pai, a abertura diz o que falta + std::fs::remove_file(dir.path().join("base.raw")).unwrap(); + let e = open_path(&dir.path().join("a.qcow2")) + .err() + .unwrap() + .to_string(); + assert!(e.contains("disco pai não encontrado"), "{e}"); } // ---------- VMDK ---------- @@ -324,8 +384,114 @@ fn vmdk_descriptor_with_flat_and_zero_extents() { check_content(&p, &expect); } +/// VMDK esparso feito à mão: 16 grãos de 64 KiB; `grains[i]` = `None` (não alocado, vem do pai), +/// `Some(None)` (zerado de propósito) ou `Some(Some(dados))`. +fn build_vmdk_sparse(descriptor: &str, grains: &[Option>>]) -> Vec { + const GRAIN: usize = 128; // setores + let mut img = vec![0u8; 26 * 512]; + img[..4].copy_from_slice(b"KDMV"); + le(&mut img, 4, 1, 4); + le(&mut img, 8, 3, 4); + le(&mut img, 12, (grains.len() * GRAIN) as u64, 8); + le(&mut img, 20, GRAIN as u64, 8); + le(&mut img, 28, 1, 8); + le(&mut img, 36, 20, 8); + le(&mut img, 44, 512, 4); + le(&mut img, 56, 21, 8); + le(&mut img, 64, 26, 8); + img[512..512 + descriptor.len()].copy_from_slice(descriptor.as_bytes()); + le(&mut img, 21 * 512, 22, 4); // diretório de grãos -> tabela no setor 22 + for (i, g) in grains.iter().enumerate() { + let entry = match g { + None => 0, + Some(None) => 1, + Some(Some(d)) => { + let sector = (img.len() / 512) as u64; + let mut d = d.clone(); + d.resize(GRAIN * 512, 0); + img.extend_from_slice(&d); + sector + } + }; + le(&mut img, 22 * 512 + i * 4, entry, 4); + } + img +} + #[test] -fn vmdk_with_a_parent_is_refused() { +fn vmdk_child_reads_the_parent_for_unallocated_grains() { + let base = raw(); + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("base.vmdk"), build_vmdk_sparse( + "# Disk DescriptorFile\nversion=1\nCID=11111111\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n", + &(0..16) + .map(|i| Some(Some(base[i * 65536..(i + 1) * 65536].to_vec()))) + .collect::>(), + )) + .unwrap(); + let own = vec![0x37u8; 65536]; + let mut grains: Vec>>> = vec![None; 16]; + grains[3] = Some(Some(own.clone())); + grains[5] = Some(None); // zerado de propósito: não consulta o pai + let child = build_vmdk_sparse( + "# Disk DescriptorFile\nversion=1\nCID=22222222\nparentCID=11111111\ncreateType=\"monolithicSparse\"\nparentFileNameHint=\"base.vmdk\"\n", + &grains, + ); + let p = dir.path().join("child.vmdk"); + std::fs::write(&p, child).unwrap(); + let mut expect = base.clone(); + expect[3 * 65536..4 * 65536].copy_from_slice(&own); + expect[5 * 65536..6 * 65536].fill(0); + check_content(&p, &expect); +} + +/// Snapshot real do VirtualBox: o filho só diz o UUID do pai (`ddb.uuid.parent`), sem nome. +#[test] +fn virtualbox_snapshot_vmdk_finds_its_parent_by_uuid() { + let dir = tempfile::tempdir().unwrap(); + let sub = dir.path().join("Snapshots"); + std::fs::create_dir(&sub).unwrap(); + // o pai fica na pasta de cima, como o VirtualBox organiza a máquina + std::fs::copy(fx("disk.vmdk"), dir.path().join("disco-base.vmdk")).unwrap(); + std::fs::copy(fx("vbox-snapshot.vmdk"), sub.join("filho.vmdk")).unwrap(); + check_content(&sub.join("filho.vmdk"), &raw()); + // sem o pai por perto, a abertura explica + std::fs::remove_file(dir.path().join("disco-base.vmdk")).unwrap(); + let e = open_path(&sub.join("filho.vmdk")) + .err() + .unwrap() + .to_string(); + assert!(e.contains("identificado só por UUID"), "{e}"); +} + +#[test] +fn vmdk_child_is_refused_when_the_parent_changed() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("base.vmdk"), + build_vmdk_sparse( + "# Disk DescriptorFile\nversion=1\nCID=aaaaaaaa\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n", + &vec![None; 16], + ), + ) + .unwrap(); + std::fs::write( + dir.path().join("filho.vmdk"), + build_vmdk_sparse( + "# Disk DescriptorFile\nversion=1\nCID=bbbbbbbb\nparentCID=11111111\ncreateType=\"monolithicSparse\"\nparentFileNameHint=\"base.vmdk\"\n", + &vec![None; 16], + ), + ) + .unwrap(); + let e = open_path(&dir.path().join("filho.vmdk")) + .err() + .unwrap() + .to_string(); + assert!(e.contains("foi alterado depois do filho"), "{e}"); +} + +#[test] +fn vmdk_with_a_parent_but_no_name_is_refused() { let dir = tempfile::tempdir().unwrap(); let p = dir.path().join("c.vmdk"); std::fs::write( @@ -334,7 +500,7 @@ fn vmdk_with_a_parent_is_refused() { ) .unwrap(); let e = open_path(&p).err().expect("deveria recusar").to_string(); - assert!(e.contains("disco pai"), "{e}"); + assert!(e.contains("não diz qual"), "{e}"); } // ---------- VHD ---------- @@ -619,8 +785,36 @@ fn dmg_with_every_chunk_type() { check_content(&p, &expect); } +/// Chunks `ULFO` com dados compactados pelo compressor de referência da Apple (`lzfse`): +/// um bloco `bvx2` (FSE, 64 KiB) e um pequeno (`bvxn`/LZVN). #[test] -fn dmg_lzfse_and_encrypted_are_refused_with_a_reason() { +fn dmg_lzfse_chunks_from_the_reference_compressor() { + let big = std::fs::read(fx("lzfse-64k.bin")).unwrap(); + let small = std::fs::read(fx("lzfse-small.bin")).unwrap(); + let mut small_padded = small.clone(); + small_padded.resize(small.len().div_ceil(512) * 512, 0); + let chunks = [ + DmgChunk { + ty: 0x8000_0007, + sectors: (big.len() / 512) as u64, + payload: std::fs::read(fx("lzfse-64k.bin.lzfse")).unwrap(), + }, + DmgChunk { + ty: 0x8000_0007, + sectors: (small_padded.len() / 512) as u64, + payload: std::fs::read(fx("lzfse-small.bin.lzfse")).unwrap(), + }, + ]; + let mut expect = big.clone(); + expect.extend_from_slice(&small_padded); + let dir = tempfile::tempdir().unwrap(); + let p = dir.path().join("l.dmg"); + std::fs::write(&p, build_dmg(&chunks, expect.len() as u64 / 512)).unwrap(); + check_content(&p, &expect); +} + +#[test] +fn dmg_with_a_corrupt_lzfse_chunk_and_encrypted_are_refused_with_a_reason() { let dir = tempfile::tempdir().unwrap(); let p = dir.path().join("l.dmg"); let chunks = [DmgChunk { diff --git a/docs/pt-BR/suporte.md b/docs/pt-BR/suporte.md index b2f9a2d..6b048e5 100644 --- a/docs/pt-BR/suporte.md +++ b/docs/pt-BR/suporte.md @@ -55,12 +55,12 @@ As senhas **nunca** vão para o arquivo de sessão. Com `--remember-keys` a sess | Bruta (dd, `.img`) | ✅ | ✅ com mapa de erros de setores ruins | | E01 e Ex01 (Expert Witness) | ✅ | ✅ com metadados do caso e segmentação | | AFF4 | ✅ | ✅ | -| QCOW2 (QEMU), versões 2 e 3 | ✅ inclusive clusters comprimidos (deflate, zstd) | ❌ | -| VMDK (VMware, VirtualBox) | ✅ esparso, `streamOptimized` e descritores com extensões flat/zero | ❌ | +| QCOW2 (QEMU), versões 2 e 3 | ✅ inclusive clusters comprimidos (deflate, zstd) e arquivo de apoio | ❌ | +| VMDK (VMware, VirtualBox) | ✅ esparso, `streamOptimized`, descritores com extensões flat/zero e snapshots (pai por nome ou, no VirtualBox, por UUID) | ❌ | | VHD e VHDX (Hyper-V) | ✅ fixo, dinâmico e diferencial (segue a cadeia de pais) | ❌ | -| DMG (Apple UDIF) | ✅ chunks zerados, brutos, ADC, zlib e bzip2 | ❌ | +| DMG (Apple UDIF) | ✅ chunks zerados, brutos, ADC, zlib, bzip2 e LZFSE | ❌ | -Os contêineres de disco virtual são abertos como o disco que contêm, sem conversão. Imagens que não podem ser lidas corretamente são recusadas com o motivo: QCOW2 com arquivo de apoio ou cifrado, VMDK com pai (snapshot) ou em SEsparse, DMG protegido por senha e DMG com chunks LZFSE. O log de transações do VHDX não é reproduzido (nada é gravado na imagem). +Os contêineres de disco virtual são abertos como o disco que contêm, sem conversão. Imagens que não podem ser lidas corretamente são recusadas com o motivo: QCOW2 cifrado, QCOW2 cujo apoio não é um arquivo local, VMDK em SEsparse e DMG protegido por senha. Discos diferenciais (VHD, VHDX, QCOW2 com apoio, snapshot de VMDK) leem o pai no que o filho não gravou; o pai é procurado ao lado do filho (VMDK: por nome ou UUID) e, no VMDK, o CID dele precisa bater, senão a imagem é recusada porque o pai mudou. A identidade dos pais de VHD, VHDX e QCOW2 não é conferida. O log de transações do VHDX não é reproduzido (nada é gravado na imagem). `guara verify` recalcula o MD5 de uma imagem E01 ou Ex01 e compara com o gravado nela. diff --git a/docs/support.md b/docs/support.md index 5b61db7..d7e7459 100644 --- a/docs/support.md +++ b/docs/support.md @@ -55,12 +55,12 @@ Passwords are **never** written to the session file. With `--remember-keys` the | Raw (dd, `.img`) | ✅ | ✅ with a bad sector error map | | E01 and Ex01 (Expert Witness) | ✅ | ✅ with case metadata and segmentation | | AFF4 | ✅ | ✅ | -| QCOW2 (QEMU), versions 2 and 3 | ✅ including compressed clusters (deflate, zstd) | ❌ | -| VMDK (VMware, VirtualBox) | ✅ sparse, `streamOptimized` and descriptors with flat/zero extents | ❌ | +| QCOW2 (QEMU), versions 2 and 3 | ✅ including compressed clusters (deflate, zstd) and backing files | ❌ | +| VMDK (VMware, VirtualBox) | ✅ sparse, `streamOptimized`, descriptors with flat/zero extents and snapshots (parent by name or, for VirtualBox, by UUID) | ❌ | | VHD and VHDX (Hyper-V) | ✅ fixed, dynamic and differencing (follows the parent chain) | ❌ | -| DMG (Apple UDIF) | ✅ zero, raw, ADC, zlib and bzip2 chunks | ❌ | +| DMG (Apple UDIF) | ✅ zero, raw, ADC, zlib, bzip2 and LZFSE chunks | ❌ | -Virtual disk containers are opened as the disk they hold, with no conversion. Images that cannot be read correctly are refused with the reason: QCOW2 with a backing file or encryption, VMDK with a parent (snapshot) or in SEsparse, password-protected DMG and DMG with LZFSE chunks. The VHDX transaction log is not replayed (nothing is ever written to the image). +Virtual disk containers are opened as the disk they hold, with no conversion. Images that cannot be read correctly are refused with the reason: encrypted QCOW2, QCOW2 whose backing file is not a local file, VMDK in SEsparse, and password-protected DMG. Differencing disks (VHD, VHDX, QCOW2 backing file, VMDK snapshot) read the parent for what the child did not write; the parent is found next to the child (VMDK: by name or UUID) and, for VMDK, its CID must match, otherwise the image is refused because the parent changed. The identity of VHD, VHDX and QCOW2 parents is not checked. The VHDX transaction log is not replayed (nothing is ever written to the image). `guara verify` recomputes the MD5 of an E01 or Ex01 image and compares it with the one stored in it.