Code Examples
The following examples demonstrate practical usage of the freemdu protocol library for low-level diagnostics, reverse-engineering, and security research.
All examples provided below are included in the repository under the protocol/examples/ directory. You can execute them directly via Cargo. Ensure your hardware adapter is flashed in bridge mode and connected via USB.
cargo run --all-features --example <EXAMPLE_NAME>
1. Brute-Forcing Unlock Keys (find_keys.rs)
Context: Miele does not publish the memory access keys for their appliances. Every unique Software ID has a specific pair of 16-bit security keys: one for read access, and one for full (write/execute) access.
This script demonstrates how to sequentially brute-force the keys space ($0x0000$ to $0xFFFF$). It attempts an unlock, verifies success by trying to read memory, and handles the necessary timeouts and retries.
use freemdu::{Interface, serial::Port};
use std::{error::Error, thread, time::Duration};
use tokio::time;
const UNLOCK_TIMEOUT: Duration = Duration::from_millis(500);
const ERROR_RETRY_DELAY: Duration = Duration::from_secs(4);
const CHECK_TIMEOUT: Duration = Duration::from_millis(100);
async fn find_read_access_key(intf: &mut Interface<Port>) -> Result<u16, Box<dyn Error>> {
for i in 0x0000..=0xffff {
println!("Attempting Read Access Key: {i:04x}");
// Attempt connection with timeout to handle unresponsive states
while let Err(err) = time::timeout(UNLOCK_TIMEOUT, async {
intf.query_software_id().await?;
intf.unlock_read_access(i).await
})
.await
{
eprintln!("Protocol Error: {err}");
thread::sleep(ERROR_RETRY_DELAY);
}
// Verification: If we can successfully read memory 0x0000, the key is correct
if let Ok(Ok(_)) = time::timeout(CHECK_TIMEOUT, intf.read_memory::<u8, _>(0x0000)).await {
return Ok(i);
}
}
Err("Failed to discover read access key".into())
}
async fn find_full_access_key(
intf: &mut Interface<Port>,
read_key: u16,
) -> Result<u16, Box<dyn Error>> {
for i in 0x0000..=0xffff {
println!("Attempting Full Access Key: {i:04x} (Using Read Key: {read_key:04x})");
while let Err(err) = time::timeout(UNLOCK_TIMEOUT, async {
intf.query_software_id().await?;
// Read access must be unlocked before attempting full access
intf.unlock_read_access(read_key).await?;
intf.unlock_full_access(i).await
})
.await
{
eprintln!("Protocol Error: {err}");
thread::sleep(ERROR_RETRY_DELAY);
}
// Verification: If we can issue the halt command, full access is granted
if let Ok(Ok(())) = time::timeout(CHECK_TIMEOUT, intf.halt()).await {
return Ok(i);
}
}
Err("Failed to discover full access key".into())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
let port = freemdu::serial::open("/dev/ttyACM0")?;
let mut intf = Interface::new(port);
println!("Starting brute-force attack...");
let read_key = find_read_access_key(&mut intf).await?;
println!("--> Found Read Access Key: {read_key:04x}");
let full_key = find_full_access_key(&mut intf, read_key).await?;
println!("--> Found Full Access Key: {full_key:04x}");
Ok(())
}
2. Dumping Volatile Memory (dump_memory.rs)
Context: Once an appliance is unlocked, you can read the internal RAM state to reverse-engineer where specific sensor values (like temperature or program phase) are mapped in memory. This script extracts data in 128-byte chunks and appends it safely to a binary file.
Best Practice: Dumping memory over a 2400-baud serial line is slow. This script automatically checks the existing file size and resumes the dump using the
offsetvariable if it gets interrupted.
use std::{
error::Error,
fs::OpenOptions,
io::{Seek, SeekFrom, Write},
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
// Adjust these addresses based on the target microcontroller datasheet
const START: u32 = 0x0000_0000;
const END: u32 = 0x0000_ffff;
let mut port = freemdu::serial::open("/dev/ttyACM0")?;
// Using the high-level Device abstraction automatically handles the unlocking phase
let mut dev = freemdu::device::connect(&mut port).await?;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open("memory_dump.bin")?;
// Resume dumping process if previously interrupted
let offset: u32 = file.seek(SeekFrom::End(0))?.try_into()?;
for addr in (START + offset..=END).step_by(0x80) {
println!("Reading RAM chunk at address {addr:08x}");
// Fall back to the low-level interface for raw reading
let data: [u8; 0x80] = dev.interface().read_memory(addr).await?;
file.write_all(&data)?;
}
Ok(())
}
3. Dumping Non-Volatile EEPROM (dump_eeprom.rs)
Context: The EEPROM stores long-term configuration data, persistent fault logs, and calibration values. Dumping it is crucial for fully backing up an appliance's configuration.
Caution: Do not mistakenly use the
write_eepromfunction. EEPROM chips have limited write cycles, and arbitrarily overwriting data here can permanently brick the appliance's control board.
use std::{
error::Error,
fs::OpenOptions,
io::{Seek, SeekFrom, Write},
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
// Adjust address range here based on the EEPROM capacity
const START: u16 = 0x0000;
const END: u16 = 0x07ff;
let mut port = freemdu::serial::open("/dev/ttyACM0")?;
let mut dev = freemdu::device::connect(&mut port).await?;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open("eeprom_dump.bin")?;
// Resume dumping process if previously interrupted
let offset: u16 = file.seek(SeekFrom::End(0))?.try_into()?;
// Step by 128 bytes (0x80) at a time
for addr in (START + offset..=END).step_by(0x80) {
println!("Reading EEPROM chunk at address {addr:04x}");
// Note: Some older models address EEPROM in 16-bit words rather than bytes.
// We divide the address by 2 to account for this word-alignment.
let data: [u8; 0x80] = dev.interface().read_eeprom(addr / 2).await?;
file.write_all(&data)?;
}
Ok(())
}