---
title: Breaking my Security Assignments
description: A secure system broken by the fact that I can screw with a VM's internals
publishedDate: 2025-02-05
tags:
  - uni
  - linux
---
<div class="note">
<div class="note-content">This post was first written in March 2024, but at the time, I was asked not to publish it for reasons that will shortly become apparent. Circumstances have changed since, so I've gone ahead and published it now with some minor alterations.</div>
</div>

For the assignments in my security module this semester, we've been given a barebones virtual machine that we install updates into and complete assignments on. When each new assignment is released, we get given a file to download and run with this preinstalled piece of software on the VM. This sets the assignment up for us and from there we go on to solve it to get a token that can be submitted, [capture-the-flag style](https://ctftime.org/ctf-wtf). These files aren't any obviously readable type of file - they mostly look like garbage data.

But without that update file, there's no trace of the assignment on the system anywhere - which also means that those update files must contain the tokens we ned to submit and the VM must somehow be able to decrypt them... so surely we can break that for our own advantage?

# Cracking Open the Updates

Having decided that I wanted to attack this the VM to try and pull out tokens without doing exercises, the first thing I turned my attention to was `installUpdate` - the program that was left on the VM for us to use to install updates. Every update file that we were given took the form `*.gpg`, so (working on the assumption that these are encrypted blobs) the logical thing to do would be to take a poke around this executable to see if there's any way to decipher how these updates are decrypted.

After getting `installUpdate` on my machine and checking that it wasn't a Bash script, I ran `strings` on it was greeted with an immediate goldmine:

```
$ strings installUpdate
...
/usr/bin/gpg --homedir /root/.gnupg --batch --pinentry-mode loopback --passphrase-file /root/.vmPassphrase --output %s/update.tgz --decrypt %s/updateArchive.gpg > /dev/null 2>&1
...
/usr/bin/tar zxf update.tgz > /dev/null 2>&1
...
```

Great! It looks like the update files are GPG encrypted tarballs. The decryption command references a `/root/.vmPassphrase` file, as well as `/root/.gnupg` that presumedly stores some keys, so it's probably a safe bet that we need both of those to do decryption.

In the VM, we don't have root[^2] so I can't just boot the VM and grab the password - but I can mount its virtual disk on my local machine. Since I'm using QEMU to run the VM[^3], I can do that using these instructions: [https://unix.stackexchange.com/a/598265](https://unix.stackexchange.com/a/598265)

The magical powers of having root on my own computer means that I can make a local copy of the VM's `/root` directory change the permissions of everything inside to be something a little more open.

With the directory in hand, some fiddling with some of the arguments in the above GPG command gives me something I can use to decrypt myself a tarball:

```
$ gpg --homedir root-copy/.gnupg --batch --pinentry-mode loopback --passphrase-file root-copy/.vmPassphrase --output update.tgz --decrypt ex3/update_ex3.gpg
gpg: WARNING: unsafe permissions on homedir '/media/akp/Data/Uni/S+N/root-copy/.gnupg'
gpg: encrypted with 3072-bit RSA key, ID 57ABAFF0A7DE8CC8, created 2024-01-11
      "************** (Encryption key for SecNet VMs) <*****@bham.ac.uk>"
gpg: Signature made Thu 15 Feb 2024 13:12:56 GMT
gpg:                using RSA key E18EFB226702A6240F9E9A150985CE5D508F4DAE
gpg:                issuer "*****@bham.ac.uk"
gpg: Good signature from ""************** (Signing key for SecNet VMs) <*****@bham.ac.uk>" [ultimate]
gpg:                 aka ""************** <*****@bham.ac.uk>" [ultimate]
```

!!!![^4]

# Digging Around Inside

Once we extract the tarball, we're met with a few directories - the actual names of the directories vary a little bit from exercise to exercise but at the very least, they all contain a `bin` directory and a `java` directory.

As far as I can tell, each update has an "entrypoint" in the form of a shell script called `bin/updateVM` that copies and uses files in the other directories of the archive (though I can't tell if any of these directories have special meaning since I've not put any time into reversing the `installUpdate` binary).

Much of the time, the entrypoint script compiles and runs some Java code from the `java` directory. Typically there are two files present - one file always named in the form `GenTokenEx\d_2023.java` and another just called `Keys.java`. Since this is just source code[^5], a cursory poke around reveals that these files handle on-the-fly generation and installation of tokens as required by the exercises.

Some update files also reference a mysterious `systemd` service called `tokens` and, quite frankly, I can't work out why it exists. Closer inspection of the shell script that it runs shows that's it's a mechanism to *run* the Java code included in update archives, but it's also only used by (so far) 1 of 3 exercises, with the rest opting to directly run the token generation code in the entrypoint script.

# Understanding the Token Format

Most of the Java code that's included in the update files has roughly the same format. Along the exercise-specific setup code, they all include a function called `genToken` that looks a little something like this, here shown in a tidied up form that has a little context as well:

```java
public class GenTokenEx2_2023 {
  static String moduleKey = Keys2023.getModuleKey();
  static SecureRandom random = new SecureRandom();
  static String randomString;

  public static void main(String[] paramArrayOfString) throws Exception {
    int i = 11;
    String str = new BigInteger(88, random).toString(36);
    randomString = str.substring(0, i);
    // [trimmed]
    byte[] tokenPart1 = genToken("Ex21");
    byte[] tokenPart2 = genToken("Ex22");
  }

  private static byte[] genToken(String paramString) throws Exception {
    String str = paramString + randomString;
    SecretKeySpec localSecretKeySpec = new SecretKeySpec(hexStringToByteArray(moduleKey), "AES");
    Cipher localCipher = Cipher.getInstance("AES");
    localCipher.init(1, localSecretKeySpec);
    return localCipher.doFinal(str.getBytes());
  }

  private static String byteArrayToHexString(byte[] paramArrayOfByte) {
    // [trimmed]
  }

  private static byte[] hexStringToByteArray(String paramString) {
    // [trimmed]
  }
}
```

This is all fairly self-explainatory - when the program starts, a random number is generated, converted to hexadecimal and the first 11 characters are retained. This sequence is then combined with a unique identifier for each exercise to form a 15 character long string that is AES encrypted using a "module key" (which is a key that's used for all token submissions across every exercise and is included in the `Keys.java` file). The exercise identifiers always start with `Ex` then contain a single digit each to represent the assignment number and exercise number (eg. exercise 2 part 1 is `Ex21`, exercise 3 part 2 is `Ex32`, exercise 1 part 1 is `Ex11`, etc.).

All-in, the raw body of a token looks like this:

![Diagram of a token's layout, byte by byte](./tokenlayout.png)

The encrypted payload is hex encoded and then used by the rest of the Java code to set up the assignment. This scheme means that everyone has different tokens so token sharing can be detected[^6], and these tokens can be submitted to a custom-built website that gives instant feedback on if they're correct or not.

So, armed with this knowledge and the source code, it's trivially simple to comment out some sections, add one or two new lines calling `genToken` with the right exercise identifier and you have a working token in-hand. This happens to be how I (somewhat inadvisably) ended up being the first person to submit their tokens for an exercise - taking literally about 45 minutes to "complete" quite a complex assignment that later took me about 4 hours to complete.

![Screenshot of a text conversation where one the module TAs tells me that I was the first to submit the assignment](firstsubmitwhoopsies.png)

# How This Could Be Prevented

This entire attack was possible because I have the VM's disk image right here on my computer and I can do absolutely whatever I want to it, such as overriding its access control settings.

Within the aims of the module this is fine - this is an introduction to security module so if you can exploit it like this, you're not really the target audience and you've already achieved the aims of the module.

That said, if we're trying to make this attack impossible, something like hosting a remote VM for each student enrolled on the module that could be accessed only via SSH. This way, with appropriate access control measures, there'd be no way to dump secrets from `/root` short of a kernel bug. Realistically though, hosting 330 VMs like that would never fly - it's simply too expensive and time-consuming versus the benefit gained.

<div class="note">
<div class="note-content">Comment from the future: bad predictions aside, that's exactly what they're now doing - giving each student their own VM to do the exercises on, properly.</div>
</div>

# To Sum Up

This was fun a fun little thing to meddle with for a while, but ultimately a pointless exercise. This isn't going to save me any time - I still need to do the assignments because they're assignments for a University module, which is supposed to teach me things. If I don't do the assignments and effectively cheat by submitting tokens I recover this way, **I personally will suffer** and not know what I'm doing in enough detail when it comes to the final exam and just generally will lack this knowledge that might be useful in future.

---

[^2]: ...yet. As I write this, it's halfway through the module, so I'm not actually sure what else is in store, though this post won't be published until the module is over to prevent an academic integrity investigation against myself. (In fact, it might not be published at all depending on what the module lead says since I wouldn't be surprised if they wanted to use this system again next year. If you're reading this, firstly hi, and secondly that means ~~it got approval from said module lead~~ the module changed how it works and I decided to publish now that this article is no longer useful for anyone trying to cheat.)
[^3]: The original VM is a VirtualBox image that I converted to `qcow2` format to use in QEMU. [Instructions here.](https://wiki.akpain.net/books/38059-operating-systems-and-systems-programming/page/converting-and-using-an-ova-image-in-qemu)
[^4]: Seeing this for the first time gave me the I-shouldn't-be-doing-this jittery adrenaline rush.
[^5]: I'm a little surprised that the source code was included as opposed to precompiled `.class` files to further obfuscate what's going on, but then again by this point, with the GPG encryption and all, I don't imagine the module team was focused on preventing me from meddling around as much as they were focused on getting a module out of the door.
[^6]: There are 4.6e16 possible 11 character long random hex strings so I'd wager a collision will never happen at this scale.
