Breaking My Security Assignments

Jun 15, 2025 - 06:45
 0  0
Breaking My Security Assignments

Breaking my Security Assignments

A secure system broken by the fact that I can screw with a VM's internals


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.

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. 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 root1 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 VM2, I can do that using these instructions: 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]

!!!!3

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 code4, 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:

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:

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 detected5, 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.

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 introuction 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.

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.

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.


  1. ...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.)

  2. The original VM is a VirtualBox image that I converted to qcow2 format to use in QEMU. Instructions here.

  3. Seeing this for the first time gave me the I-shouldn't-be-doing-this jittery adrenaline rush.

  4. 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.

  5. There are 4.6e16 possible 11 character long random hex strings so I'd wager a collision will never happen at this scale.

Thoughts? Corrections? Questions? Comment via email!

What's Your Reaction?

Like Like 0
Dislike Dislike 0
Love Love 0
Funny Funny 0
Angry Angry 0
Sad Sad 0
Wow Wow 0