Стеганография — это искусство и наука сокрытия информации внутри других, казалось бы, невинных данных. Давайте создадим его на Rust с нуля
Стеганография — это искусство и наука сокрытия информации в других, казалось бы, невинных данных или носителях, таких как изображения, аудиофайлы или текст. Он работает по принципу, согласно которому скрытое сообщение остается незамеченным кем-либо, кроме предполагаемого получателя. В последнее время я работаю в сфере кибербезопасности и обнаружил, что стеганография LSB — это забавное упражнение для практики моего Rust.
Что такое ЛСБ?
Стеганография LSB, также известная как стеганография с наименьшим значащим битом, хранит информацию в последнем бите двоичного числа. Давайте посмотрим пример:
Изображение — это просто набор пикселей, организованных в виде матрицы. Мы можем видеть каждое из значений пикселей как байт, что позволяет нам использовать LSB для хранения нашей информации и слегка изменять значение информации в изображении, чтобы оно не было заметно.
Мы можем использовать LSB с другими методами, такими как шифрование, чтобы еще больше запутать наше сообщение. В этой статье мы будем кодировать наше сообщение в виде простого текста.
Реализация LSB
Мы можем разделить реализацию на две части: кодирование и декодирование. Кодирование — это часть, в которой сообщение вставляется в конкретное изображение. Декодирование — это то, где мы извлекаем данные, хранящиеся внутри изображения.
Кодирование
Для начала нам понадобится функция со следующими параметрами: путь к изображению, которое мы будем использовать для скрытия информации, путь к файлу с информацией, которую нужно скрыть, и, наконец, пункт назначения для сохранения изображения с нашим скрытым сообщением. В Rust это будет выглядеть так:
fn encode(src: &str, msg_src: &str, dest: &str) { // Load file content as bits // Get message size (stored as a 4 byte value) // Add size to the message // Open the image // Check capacity of the image // Encode data using LSB // Save the image }
Как видите, я также показываю шаги, которые необходимо выполнить в алгоритме кодирования. Делаем первое:
fn encode(src: &str, msg_src: &str, dest: &str) { // Load file content as bits let message_bytes = read(msg_src).unwrap(); let message_bits = BitUtils::make_bits(message_bytes); // Get message size (stored as a 4 byte value) // Add size to the message // Open the image // Check capacity of the image // Encode data using LSB // Save the image }
Это было легко, но, как видите, я использовал функцию, содержащуюся в BitUtils
, под названием make_bits
. Эта функция берет байт, представленный в десятичной системе счисления, и преобразует его в двоичное представление. Это означает, например, что байт 4
будет [0,0,0,0,0,1,0,0]
. Реализация выглядит следующим образом:
// Takes a bytes vector and transforms them into a bit // For example a vector like [4, 8] would be [0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0] pub fn make_bits(bytes: Vec<u8>) -> Vec<u8> bytes .iter() // Iterates over every byte .flat_map(|byte| Self::byte_to_bit(*byte)) // Transforms the current byte into a bit .collect() // Collects the results } // Transforms a decimal represented byte into its bit representation // For example 4 would be [0,0,0,0,0,1,0,0] pub fn byte_to_bit(byte: u8) -> Vec<u8> { (0..8) .rev() // Itererates from 7 to 0 .map(|i| (byte >> i) & 1) // Gets the bit in the current position .collect() // Collects the results }
Теперь мы можем продолжить нашу функцию кодирования, получая количество битов, которые являются частью нашего сообщения, и добавляя его к началу нашего сообщения, чтобы обеспечить более быстрое восстановление в дальнейшем.
fn encode(src: &str, msg_src: &str, dest: &str) { // Load file content as bits let message_bytes = read(msg_src).unwrap(); let message_bits = BitUtils::make_bits(message_bytes); // Get message size (stored as a 4 byte value) let message_size = BitUtils::byte_u32_to_bit(message_bits.len() as u32); // Add size to the message let mut complete_message = Vec::new(); complete_message.extend_from_slice(&message_size); // adds the message size complete_message.extend_from_slice(&message_bits); // adds the message // Open the image // Check capacity of the image // Encode data using LSB // Save the image }
Нам нужно открыть изображение, на котором мы будем выполнять стеганографию LSB. Для этого я буду использовать только .png
изображений, используя png
ящик.
fn encode(src: &str, msg_src: &str, dest: &str) { // Load file content as bits let message_bytes = read(msg_src).unwrap(); let message_bits = BitUtils::make_bits(message_bytes); // Get message size (stored as a 4 byte value) let message_size = BitUtils::byte_u32_to_bit(message_bits.len() as u32); // Add size to the message let mut complete_message = Vec::new(); complete_message.extend_from_slice(&message_size); // adds the message size complete_message.extend_from_slice(&message_bits); // adds the message // Open the image // Decodes the image where we are going to hide our data let decoder = Decoder::new(File::open(src).unwrap()); // Get the reader as mutable to perform operations let mut binding = decoder.read_info(); // This is needed because of borrow let reader = binding.as_mut().unwrap(); // Check capacity of the image // Encode data using LSB // Save the image }
Теперь, когда у нас есть декодированное изображение, мы можем проверить данные и метаданные, например, емкость изображения. Мы будем использовать это, чтобы проверить, достаточно ли места для наших данных на изображении.
fn encode(src: &str, msg_src: &str, dest: &str) { // Load file content as bits let message_bytes = read(msg_src).unwrap(); let message_bits = BitUtils::make_bits(message_bytes); // Get message size (stored as a 4 byte value) let message_size = BitUtils::byte_u32_to_bit(message_bits.len() as u32); // Add size to the message let mut complete_message = Vec::new(); complete_message.extend_from_slice(&message_size); // adds the message size complete_message.extend_from_slice(&message_bits); // adds the message // Open the image // Decodes the image where we are going to hide our data let decoder = Decoder::new(File::open(src).unwrap()); // Get the reader as mutable to perform operations let mut binding = decoder.read_info(); // This is needed because of borrow let reader = binding.as_mut().unwrap(); // Check capacity of the image if complete_message.len() > reader.output_buffer_size() { error!("Image is too small: message size is {} and image allows for {}", complete_message.len(), reader.output_buffer_size()); return; } // Encode data using LSB // Save the image }
Теперь настало время, когда мы ждали реальной реализации LSB.
fn encode(src: &str, msg_src: &str, dest: &str) { // Load file content as bits let message_bytes = read(msg_src).unwrap(); let message_bits = BitUtils::make_bits(message_bytes); // Get message size (stored as a 4 byte value) let message_size = BitUtils::byte_u32_to_bit(message_bits.len() as u32); // Add size to the message let mut complete_message = Vec::new(); complete_message.extend_from_slice(&message_size); // adds the message size complete_message.extend_from_slice(&message_bits); // adds the message // Open the image // Decodes the image where we are going to hide our data let decoder = Decoder::new(File::open(src).unwrap()); // Get the reader as mutable to perform operations let mut binding = decoder.read_info(); // This is needed because of borrow let reader = binding.as_mut().unwrap(); // Check capacity of the image if complete_message.len() > reader.output_buffer_size() { error!("Image is too small: message size is {} and image allows for {}", complete_message.len(), reader.output_buffer_size()); return; } // Encode data using LSB let mut data = vec![0; reader.output_buffer_size()]; // Create a data vector reader.next_frame(&mut data).unwrap(); // Gets image data let info = reader.info(); // Gets metadata of the image let mut i = 0; // Tracks current bit // Iterate over bits of the message for bit in complete_message.iter() { if *bit == 1 && data[i] % 2 == 0 { // Check if the current bit of the message is equal // to 1 and check if the LSB of the image data is 0 // If both are true, then we need to flip the data to 1 (+1) data[i] += 1; } else if *bit == 0 && data[i] % 2 != 0 { // Check if the current bit of the message is equal // to 0and check if the LSB of the image data is 1 // If both are true, then we need to flip the data to 0(-1) data[i] -= 1; } i += 1; } // Save the image // Creates the destination file let encoded_img = File::create(dest).unwrap(); // Creates a png encoder, with the same dimension than the original image let mut image_encoder = Encoder::new(BufWriter::new(encoded_img), info.width, info.height); // Set the same properties than the original img image_encoder.set_color(info.color_type); image_encoder.set_depth(info.bit_depth); // Writes the image image_encoder .write_header() .unwrap() .write_image_data(&data) .unwrap(); }
Функция encode
кодирует данные с помощью стеганографии LSB. Он загружает содержимое файла сообщения, преобразует его в биты, определяет размер сообщения и объединяет его с битами сообщения. Затем он открывает файл изображения, считывает его данные и проверяет, имеет ли изображение достаточную емкость для размещения полного сообщения. Если это так, он изменяет данные изображения, переворачивая определенные биты на основе битов сообщения, используя манипуляции с LSB.
Наконец, он создает файл назначения, устанавливает свойства закодированного изображения и записывает измененные данные изображения в файл. Полученный файл изображения содержит скрытое сообщение с использованием стеганографии LSB.
Теперь у нас есть полностью работающая функция кодирования, но как мы узнаем, что она работает, если мы не можем вернуть сообщение, чтобы проверить его? Здесь появляется функция decode
.
Расшифровка
Функция декодирования намного проще, чем кодирование. Этот считывает изображение, считывает первые 32 бита, чтобы проверить длину сообщения, затем считывает количество битов, указанное длиной, и сохраняет восстановленные данные в файл.
fn decode(src: &str, dest: &str) { // Decodes the data from the image with the message let decoder = Decoder::new(File::open(src).unwrap()); let mut binding = decoder.read_info(); // Gets the reader let reader = binding.as_mut().unwrap(); // Reads the data in the reader to the data vector (we will recive bytes) let mut data = vec![0; reader.output_buffer_size()]; reader.next_frame(&mut data).unwrap(); // Splits the first 32 bits to check the message length, // then the rest of the data let (message_len, image_data) = data.split_at(32); // Transforms the message length bytes to it's decimal value, // signifing the number of bits let message_len = BitUtils::byte_u32_to_decimal(BitUtils::read_lsb(message_len.to_vec())); // Gets the bytes that have LSB that // are part of the message, discards the rest let (bytes_message, _): (&[u8], &[u8]) = image_data.split_at(message_len as usize); // Gets the bit of the message, by reading the LSB of ever byte let message_bits = BitUtils::read_lsb(bytes_message.to_vec()); // Gets the bytes of the message again let message_retrived = BitUtils::bits_to_bytes(message_bits); // Creates and saves the file with the message let mut output_file = File::create(Path::new(dest)).unwrap(); output_file.write_all(&message_retrived).unwrap(); }
Как и в случае с декодером, мы используем созданную нами функциональность BitUtils
. Код показан ниже:
// Transforms 4 bytes in its bit form into its decimal representation pub fn byte_u32_to_decimal(byte: Vec<u8>) -> u32 { byte.iter() // Iterate over each byte .enumerate() // Enumerate the bits of each byte, pairing each bit with its index .filter(|(_, &bit)| bit == 1) // Filter the enumerated bits, keeping only those where the value is equal to 1 .fold(0u32, |acc, (i, _)| acc + 2u32.pow(31 - i as u32)) // Perform a folding operation, accumulating the values obtained from the filtered bits // - The accumulator `acc` is initialized with 0u32 // - For each filtered bit, raise 2 to the power of (31 - i) and add it to the accumulator // The result is the accumulated value, representing the decimal interpretation of the filtered bits } // Reads the least significant bit (LSB) from a byte array pub fn read_lsb(bytes: Vec<u8>) -> Vec<u8> { bytes.iter() // Iterate over each byte in the `bytes` vector .map(|byte| byte % 2) // Apply the modulo operation (`byte % 2`) to each byte, resulting in the LSB .collect(); // Collect the mapped values into a new `Vec<u8>` } // Takes bits and transforms them into bytes pub fn bits_to_bytes(bits: Vec<u8>) -> Vec<u8> { let mut output: Vec<u8> = Vec::new(); // Iterate over chunks of 8 bits in the `bits` vector for byte in bits.chunks(8) { // Check if the chunk has exactly 8 bits if byte.len() == 8 { // Convert the chunk of 8 bits to a decimal value and push it to the `output` vector output.push(Self::byte_to_decimal(byte.to_vec())); } } output }
Теперь, когда у нас есть кодировщик и декодер, мы можем прекрасно использовать программу для создания стеганографии LSB, но менять источники файлов было бы неудобно. Таким образом, мы можем создать простой интерфейс командной строки с помощью clap.
CLI
Мы создадим CLI, который будет называться rust-steganography --option write|read --file path/to/file.txt --image path/to/image.png --output path/to/file-or-image
. С помощью clap это можно сделать с помощью структуры, представляющей нашу команду. Вот как выглядит код:
#[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Cli { /// If is to read orwrite #[arg(short, long)] option: String, /// The path to the file to read the message from #[arg(short, long)] file: Option<String>, /// The path to the image to use to hide the message using LSB #[arg(short, long)] image: String, /// The path to output the file #[arg(long)] output: String, } fn main() { // Read arguments let args = Cli::parse(); // Match the value of the `option` field in the `args` struct match args.option.as_str() { "read" => { info!("Starting to read file {}", &args.image); decode(&args.image, &args.output) // Call `decode` function with `image` and `output` fields as arguments }, "write" => { // Match the value of the `file` field in the `args` struct match args.file { Some(file) => { info!("Starting to write file {}", &args.output); encode(&args.image, &file, &args.output) // Call `encode` function with `image`, `file`, and `output` fields as arguments }, None => eprintln!("ERROR: File not passed!") // Print error message if `file` field is `None` } }, _ => panic!("No valid option given: please try to use --help to see the valid options"), // Panic with an error message for an invalid option } }
Таким образом, теперь у нас есть следующее приложение:
Мы видим, что наша программа сохранила данные в образ out.png
, потому что размер файла больше.
Затем с помощью функции или декодирования мы можем получить данные, сохраненные в out.png
, как показано ниже:
Заключение
Мы создали простой текстовый интерфейс командной строки для стеганографии LSB с использованием Rust, который позволяет нам кодировать сообщение (отображаемое как текст, но работает с изображениями и другими файлами) в изображение .png
для последующего восстановления. Хотя это работает, его можно улучшить следующим образом:
- Использование шифрования для защиты информации, даже если стеганография скомпрометирована.
- При использовании других алгоритмов это различается по типу, и его можно увидеть здесь.
- Удаление запахов кода Rust.
- Все, что вашей душе угодно
Вы можете найти этот код в этом репозитории GitHub.