Skip to content

feat: Add support for QEMU QCOW images #833

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/magic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,17 @@ pub fn patterns() -> Vec<signatures::common::Signature> {
description: signatures::dpapi::DESCRIPTION.to_string(),
extractor: None,
},
// QEMU QCOW image
signatures::common::Signature {
name: "qcow".to_string(),
short: true,
magic_offset: 0,
always_display: true,
magic: signatures::qcow::qcow_magic(),
parser: signatures::qcow::qcow_parser,
description: signatures::qcow::DESCRIPTION.to_string(),
extractor: None,
},
// ARJ archive
signatures::common::Signature {
name: "arj".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ pub mod pem;
pub mod pjl;
pub mod pkcs_der;
pub mod png;
pub mod qcow;
pub mod qnx;
pub mod rar;
pub mod riff;
Expand Down
25 changes: 25 additions & 0 deletions src/signatures/qcow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use crate::signatures::common::{SignatureError, SignatureResult, CONFIDENCE_MEDIUM};
use crate::structures::qcow::parse_qcow_header;

pub const DESCRIPTION: &str = "QEMU QCOW Image";

pub fn qcow_magic() -> Vec<Vec<u8>> {
vec![b"QFI\xFB".to_vec()]
}

pub fn qcow_parser(file_data: &[u8], offset: usize) -> Result<SignatureResult, SignatureError> {
// Successful return value
let mut result = SignatureResult {
offset,
name: "qcow".to_string(),
confidence: CONFIDENCE_MEDIUM,
..Default::default()
};

if let Ok(qcow_header) = parse_qcow_header(file_data) {
result.description = format!("QEMU QCOW Image, version: {}, storage media size: {:#x} bytes, cluster block size: {:#x} bytes, encryption method: {}", qcow_header.version, qcow_header.storage_media_size, 1 << qcow_header.cluster_block_bits, qcow_header.encryption_method);
return Ok(result);
};

Err(SignatureError)
}
1 change: 1 addition & 0 deletions src/structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ pub mod pcap;
pub mod pchrom;
pub mod pe;
pub mod png;
pub mod qcow;
pub mod qnx;
pub mod rar;
pub mod riff;
Expand Down
106 changes: 106 additions & 0 deletions src/structures/qcow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use crate::structures::common;
use crate::structures::common::StructureError;
use std::collections::HashMap;

#[derive(Debug, Default, Clone)]
pub struct QcowHeader {
pub version: u8,
pub storage_media_size: u64,
pub cluster_block_bits: u8,
pub encryption_method: String,
}

pub fn parse_qcow_header(qcow_data: &[u8]) -> Result<QcowHeader, StructureError> {
let qcow_basehdr_structure = vec![("magic", "u32"), ("version", "u32")];
let qcow_header_v1_structure = vec![
("backing_filename_offset", "u64"),
("backing_filename_size", "u32"),
("modification_timestamp", "u32"),
("storage_media_size", "u64"),
("cluster_block_bits", "u8"),
("level2_table_bits", "u8"),
("reserved1", "u16"),
("encryption_method", "u32"),
("level1_table_offset", "u64"),
];
let qcow_header_v2_structure = vec![
("backing_filename_offset", "u64"),
("backing_filename_size", "u32"),
("cluster_block_bits", "u32"),
("storage_media_size", "u64"),
("encryption_method", "u32"),
("level1_table_refs", "u32"),
("level1_table_offset", "u64"),
("refcount_table_offset", "u64"),
("refcount_table_clusters", "u32"),
("snapshot_count", "u32"),
("snapshot_offset", "u64"),
];
let qcow_header_v3_structure = vec![
("backing_filename_offset", "u64"),
("backing_filename_size", "u32"),
("cluster_block_bits", "u32"),
("storage_media_size", "u64"),
("encryption_method", "u32"),
("level1_table_refs", "u32"),
("level1_table_offset", "u64"),
("refcount_table_offset", "u64"),
("refcount_table_clusters", "u32"),
("snapshot_count", "u32"),
("snapshot_offset", "u64"),
("incompatible_feature_flags", "u64"),
("compatible_feature_flags", "u64"),
("autoclear_feature_flags", "u64"),
("refcount_order", "u32"),
("file_hdr_size", "u32"), // 104 or 112
];

let encryption_methods = HashMap::from([(0, "None"), (1, "AES128-CBC"), (2, "LUKS")]);

if let Ok(qcow_base_header) = common::parse(qcow_data, &qcow_basehdr_structure, "big") {
let qcow_version = qcow_base_header["version"];
let qcow_data = qcow_data.get(8..).ok_or(StructureError)?;
let qcow_header = match qcow_version {
1 => common::parse(qcow_data, &qcow_header_v1_structure, "big"),
2 => common::parse(qcow_data, &qcow_header_v2_structure, "big"),
3 => common::parse(qcow_data, &qcow_header_v3_structure, "big"),
_ => Err(StructureError),
}?;

let encryption_method = encryption_methods
.get(&qcow_header["encryption_method"])
.ok_or(StructureError)?
.to_string();

let cluster_block_bits = *qcow_header
.get("cluster_block_bits")
.filter(|&&bits| (9..=21).contains(&bits))
.ok_or(StructureError)?;

// sanity check: existing offsets need to be aligned to cluster boundary
if let Some(offset) = qcow_header.get("level1_table_offset") {
if offset % (1 << cluster_block_bits) != 0 {
return Err(StructureError);
}
}
if let Some(offset) = qcow_header.get("refcount_table_offset") {
if offset % (1 << cluster_block_bits) != 0 {
return Err(StructureError);
}
}
if let Some(offset) = qcow_header.get("snapshot_offset") {
if offset % (1 << cluster_block_bits) != 0 {
return Err(StructureError);
}
}

return Ok(QcowHeader {
version: qcow_version as u8,
storage_media_size: qcow_header["storage_media_size"] as u64,
cluster_block_bits: cluster_block_bits as u8,
encryption_method,
});
}

Err(StructureError)
}
Binary file added tests/inputs/qcow.bin
Binary file not shown.
17 changes: 17 additions & 0 deletions tests/qcow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
mod common;

#[test]
fn integration_test() {
const SIGNATURE_TYPE: &str = "qcow";
const INPUT_FILE_NAME: &str = "qcow.bin";

let expected_signature_offsets: Vec<usize> = vec![0];
let expected_extraction_offsets: Vec<usize> = vec![];

let results = common::run_binwalk(SIGNATURE_TYPE, INPUT_FILE_NAME);
common::assert_results_ok(
results,
expected_signature_offsets,
expected_extraction_offsets,
);
}
Loading