Стеганография — это искусство и наука сокрытия информации внутри других, казалось бы, невинных данных. Давайте создадим его на 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.