Skip to content

Commit 2916ddf

Browse files
authored
Merge pull request #833 from theguy147/feat/qemu-qcow
feat: Add support for QEMU QCOW images
2 parents 98e0840 + 0b9e944 commit 2916ddf

File tree

7 files changed

+161
-0
lines changed

7 files changed

+161
-0
lines changed

src/magic.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,17 @@ pub fn patterns() -> Vec<signatures::common::Signature> {
11521152
description: signatures::dpapi::DESCRIPTION.to_string(),
11531153
extractor: None,
11541154
},
1155+
// QEMU QCOW image
1156+
signatures::common::Signature {
1157+
name: "qcow".to_string(),
1158+
short: true,
1159+
magic_offset: 0,
1160+
always_display: true,
1161+
magic: signatures::qcow::qcow_magic(),
1162+
parser: signatures::qcow::qcow_parser,
1163+
description: signatures::qcow::DESCRIPTION.to_string(),
1164+
extractor: None,
1165+
},
11551166
// ARJ archive
11561167
signatures::common::Signature {
11571168
name: "arj".to_string(),

src/signatures.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ pub mod pem;
172172
pub mod pjl;
173173
pub mod pkcs_der;
174174
pub mod png;
175+
pub mod qcow;
175176
pub mod qnx;
176177
pub mod rar;
177178
pub mod riff;

src/signatures/qcow.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use crate::signatures::common::{SignatureError, SignatureResult, CONFIDENCE_MEDIUM};
2+
use crate::structures::qcow::parse_qcow_header;
3+
4+
pub const DESCRIPTION: &str = "QEMU QCOW Image";
5+
6+
pub fn qcow_magic() -> Vec<Vec<u8>> {
7+
vec![b"QFI\xFB".to_vec()]
8+
}
9+
10+
pub fn qcow_parser(file_data: &[u8], offset: usize) -> Result<SignatureResult, SignatureError> {
11+
// Successful return value
12+
let mut result = SignatureResult {
13+
offset,
14+
name: "qcow".to_string(),
15+
confidence: CONFIDENCE_MEDIUM,
16+
..Default::default()
17+
};
18+
19+
if let Ok(qcow_header) = parse_qcow_header(file_data) {
20+
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);
21+
return Ok(result);
22+
};
23+
24+
Err(SignatureError)
25+
}

src/structures.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ pub mod pcap;
145145
pub mod pchrom;
146146
pub mod pe;
147147
pub mod png;
148+
pub mod qcow;
148149
pub mod qnx;
149150
pub mod rar;
150151
pub mod riff;

src/structures/qcow.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use crate::structures::common;
2+
use crate::structures::common::StructureError;
3+
use std::collections::HashMap;
4+
5+
#[derive(Debug, Default, Clone)]
6+
pub struct QcowHeader {
7+
pub version: u8,
8+
pub storage_media_size: u64,
9+
pub cluster_block_bits: u8,
10+
pub encryption_method: String,
11+
}
12+
13+
pub fn parse_qcow_header(qcow_data: &[u8]) -> Result<QcowHeader, StructureError> {
14+
let qcow_basehdr_structure = vec![("magic", "u32"), ("version", "u32")];
15+
let qcow_header_v1_structure = vec![
16+
("backing_filename_offset", "u64"),
17+
("backing_filename_size", "u32"),
18+
("modification_timestamp", "u32"),
19+
("storage_media_size", "u64"),
20+
("cluster_block_bits", "u8"),
21+
("level2_table_bits", "u8"),
22+
("reserved1", "u16"),
23+
("encryption_method", "u32"),
24+
("level1_table_offset", "u64"),
25+
];
26+
let qcow_header_v2_structure = vec![
27+
("backing_filename_offset", "u64"),
28+
("backing_filename_size", "u32"),
29+
("cluster_block_bits", "u32"),
30+
("storage_media_size", "u64"),
31+
("encryption_method", "u32"),
32+
("level1_table_refs", "u32"),
33+
("level1_table_offset", "u64"),
34+
("refcount_table_offset", "u64"),
35+
("refcount_table_clusters", "u32"),
36+
("snapshot_count", "u32"),
37+
("snapshot_offset", "u64"),
38+
];
39+
let qcow_header_v3_structure = vec![
40+
("backing_filename_offset", "u64"),
41+
("backing_filename_size", "u32"),
42+
("cluster_block_bits", "u32"),
43+
("storage_media_size", "u64"),
44+
("encryption_method", "u32"),
45+
("level1_table_refs", "u32"),
46+
("level1_table_offset", "u64"),
47+
("refcount_table_offset", "u64"),
48+
("refcount_table_clusters", "u32"),
49+
("snapshot_count", "u32"),
50+
("snapshot_offset", "u64"),
51+
("incompatible_feature_flags", "u64"),
52+
("compatible_feature_flags", "u64"),
53+
("autoclear_feature_flags", "u64"),
54+
("refcount_order", "u32"),
55+
("file_hdr_size", "u32"), // 104 or 112
56+
];
57+
58+
let encryption_methods = HashMap::from([(0, "None"), (1, "AES128-CBC"), (2, "LUKS")]);
59+
60+
if let Ok(qcow_base_header) = common::parse(qcow_data, &qcow_basehdr_structure, "big") {
61+
let qcow_version = qcow_base_header["version"];
62+
let qcow_data = qcow_data.get(8..).ok_or(StructureError)?;
63+
let qcow_header = match qcow_version {
64+
1 => common::parse(qcow_data, &qcow_header_v1_structure, "big"),
65+
2 => common::parse(qcow_data, &qcow_header_v2_structure, "big"),
66+
3 => common::parse(qcow_data, &qcow_header_v3_structure, "big"),
67+
_ => Err(StructureError),
68+
}?;
69+
70+
let encryption_method = encryption_methods
71+
.get(&qcow_header["encryption_method"])
72+
.ok_or(StructureError)?
73+
.to_string();
74+
75+
let cluster_block_bits = *qcow_header
76+
.get("cluster_block_bits")
77+
.filter(|&&bits| (9..=21).contains(&bits))
78+
.ok_or(StructureError)?;
79+
80+
// sanity check: existing offsets need to be aligned to cluster boundary
81+
if let Some(offset) = qcow_header.get("level1_table_offset") {
82+
if offset % (1 << cluster_block_bits) != 0 {
83+
return Err(StructureError);
84+
}
85+
}
86+
if let Some(offset) = qcow_header.get("refcount_table_offset") {
87+
if offset % (1 << cluster_block_bits) != 0 {
88+
return Err(StructureError);
89+
}
90+
}
91+
if let Some(offset) = qcow_header.get("snapshot_offset") {
92+
if offset % (1 << cluster_block_bits) != 0 {
93+
return Err(StructureError);
94+
}
95+
}
96+
97+
return Ok(QcowHeader {
98+
version: qcow_version as u8,
99+
storage_media_size: qcow_header["storage_media_size"] as u64,
100+
cluster_block_bits: cluster_block_bits as u8,
101+
encryption_method,
102+
});
103+
}
104+
105+
Err(StructureError)
106+
}

tests/inputs/qcow.bin

512 Bytes
Binary file not shown.

tests/qcow.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
mod common;
2+
3+
#[test]
4+
fn integration_test() {
5+
const SIGNATURE_TYPE: &str = "qcow";
6+
const INPUT_FILE_NAME: &str = "qcow.bin";
7+
8+
let expected_signature_offsets: Vec<usize> = vec![0];
9+
let expected_extraction_offsets: Vec<usize> = vec![];
10+
11+
let results = common::run_binwalk(SIGNATURE_TYPE, INPUT_FILE_NAME);
12+
common::assert_results_ok(
13+
results,
14+
expected_signature_offsets,
15+
expected_extraction_offsets,
16+
);
17+
}

0 commit comments

Comments
 (0)