DiceCTF HOPE - drive
drive
is a virtual machine challenge I wrote for DiceCTF @ HOPE (challenges are here). This challenge involves writing bytecode for a small VM. It also involved figuring out how rust traits are implemented for objects. The source and solution can be found here.
The Virtual Machine
The VM in the challenge consisted of a state which looked like this:
pub struct State<'a> {
pub ip: usize,
pub mem: [u8; 4],
pub program: Vec<u8>,
pub devices: Vec<Box<dyn Device + 'a>>,
}
I decided on this design as I wanted to incorporate trait implementations within the reversing challenge. I was also trying to keep the virtual machine as simple as possible otherwise, so I only provided registers and devices.
Devices
The main functionality of the virtual machine were the devices. Each device could either send a byte or read in a byte, as seen by the trait.
pub trait Device {
fn get_byte(&mut self) -> u8;
fn send_byte(&mut self, byte: u8);
}
The devices could be used with the Send
and Get
opcodes. The Send
opcode sent a byte from a register to a device based on its index. The Get
opcode would retrieve a byte from a device and place it in a register.
There were four main devices attached to the virtual machine:
- A reader
- A writer
- An arithmetic unit
- A stack
The reader implemented get_byte
to read from a Cursor, and the writer implemented send_byte
to write to another Cursor. These were checked at the end to see if the program had the right output.
The stack was used for large memory. The get_byte
acted as a pop
and the send_byte
acted as a push
.
The arithmetic unit needed 3 bytes sent — the first value, the second value, and the operation.
impl Device for Arithmetic {
fn get_byte(&mut self) -> u8 {
match self.buffer[2] {
0 => self.buffer[0].wrapping_mul(self.buffer[1]),
1 => self.buffer[0].wrapping_add(self.buffer[1]),
2 => self.buffer[0].wrapping_sub(self.buffer[1]),
3 => self.buffer[0] ^ self.buffer[1],
4 => self.buffer[0].wrapping_div(self.buffer[1]),
5 => self.buffer[0] % self.buffer[1],
_ => 0,
}
}
fn send_byte(&mut self, byte: u8) {
self.buffer[self.index] = byte;
self.index += 1;
self.index %= 3;
}
}
Program
The drive
program first loaded a program from input and ran it on a random input. It then checked if the output was the same as the encrypt
function.
#[inline(never)]
fn encrypt(block: [u8; 4]) -> [u8; 4] {
let mut block = block;
let mut b = block.clone();
let i = block[0].wrapping_add(block[3]);
b[1] ^= i;
let i = block[1].wrapping_add(block[0]);
b[2] ^= i;
let i = block[2].wrapping_add(block[1]);
b[3] ^= i;
let i = block[3].wrapping_add(block[2]);
b[0] ^= i;
block = b;
for _ in 0..block[3] {
let mul = block[2].wrapping_mul(block[0]);
let sum = mul.wrapping_add(block[1]);
block[0] = sum % block[3];
}
block
}
Originally I only had add
, sub
and xor
in the arithmetic unit. Therefore, to implement the encryption, users would have had to implement multiplication and the modulus function, however implementing the encryption with only those three operations is more annoying than interesting and not worth the effort (thank you asphyxia for playtesting :D).
My solve still has the implementation of mul and mod within the bytecode since I was too lazy to change it. My solution and comments are here.
Finally, I also originally thought I would make this a golfing challenge, however the rust rev was already quite difficult and I did not want to add another source of annoyance.