May 12, 2024 · 5 min read

ExamCookie: the illusion of exam integrity 5 years later


I did not contact ExamCookie before publishing this post.

Information!

After reversing ExamCookie, I was made aware that secret.club has already done this back in 2019.

Why am I reversing this?

I have to use ExamCookie for my exams this summer, so I wanted to know what it actually does and whether I should be worried about it. I first considered running the exam through a Windows VM, but after looking at ExamCookie I decided the hassle of getting my session flagged wasn’t worth the risk.

How does ExamCookie work?

ExamCookie has no new features since secret.club’s analysis. It still monitors:

  • Whether you’re running in a virtual machine
  • Screenshot (every x seconds or if too many changes occur)
  • Process list (when changes occur)
  • Clipboard (when changes occur)
  • Network adapter list (when changes occur)
  • Active application (when changes occur)

Now that 5 years have passed, let’s begin!

What has changed?

  • Security
  • Virtual machine detection

They have tried to improve security a bit. There is now a layer of actual authentication on top of the hardcoded username and password for the ‘WCF service’.

C#
private void SignInWithUniLogin(string username) {
  [...]
  int icon = this.ClientSignIn("", username, "", ref result);
  [...]
}

private void SignInWithManuelLogin(Uri uri) {
  [...]
  int icon = this.ClientSignIn("", uri.GetQueryString("user"), uri.GetQueryString("pass"), ref result);
  [...]
}

The this.ClientSignIn handles authentication and sets an access token for the rest of the application to use. The access token is used to authenticate the user when accessing the ‘WCF service’.

It might look like UniLogin authenticates with only a username and no password. It doesn’t. ClientSignIn is called with an empty password field because the USERNAME is a token that gets exchanged for an access token, and the actual authentication happens in GetUniLoginToken.

C#
private string GetUniLoginToken(string url) {
  string uniLoginToken;
    using (ExamApiV3Client examApiV3Client = Module1.ExamClient())
    {
      ExamApiV3Client client = examApiV3Client;
      Module1.SetCredentials(ref client);
      Dictionary<string, string> urlParameters = url.GetUrlParameters();
      Dictionary<string, object> dictionary1 = new Dictionary<string, object>();
      dictionary1.Add("Code", (object) urlParameters["code"]);
      dictionary1.Add("CodeVerifier", (object) Module1.UNI_CODE_VERIFIER);
      JavaScriptSerializer scriptSerializer = new JavaScriptSerializer();
      string webLoginUrl = examApiV3Client.GetWebLoginUrl(eWebLoginType.OIDC_TOKEN, scriptSerializer.Serialize((object) dictionary1));
      Dictionary<string, object> dictionary2 = (Dictionary<string, object>) scriptSerializer.DeserializeObject(webLoginUrl);
      if (dictionary2.ContainsKey("UniToken") & dictionary2.ContainsKey("PostLogoutUri"))
      {
        Module1.UNI_REDIRECT_LOGOUT_URL = dictionary2["PostLogoutUri"].ToString();
        uniLoginToken = dictionary2["UniToken"].ToString();
      }
      else
        uniLoginToken = "";
    }

  return uniLoginToken;
}

But the credentials for the ‘WCF service’ are still sitting in plaintext in Resources.resx, and they’re the same ones from 5 years ago. Without user authentication I can’t imagine they get you anything beyond the authentication API, but I haven’t checked, since that would be illegal.

XML
<data name="WCF_USERNAME" xml:space="preserve">
  <value>[redacted, unchanged since the secret.club writeup]</value>
</data>
<data name="WCF_PASSWORD" xml:space="preserve">
  <value>[redacted, unchanged since the secret.club writeup]</value>
</data>
<data name="WCF_ENDPOINT" xml:space="preserve">
  <value>https://examcookiewinapidk.azurewebsites.net</value>
</data>

What about the virtual machine detection?

ExamCookie STILL checks if you are running the application in a virtualized environment - and still relies on writing an external binary to disk. However, it now checks the signature of the ‘ecvmd.exe’ before running it.

C#
public int CheckFileSignature(string filename) {
  [...]
  num = x509Certificate2.Subject.Contains("CN=EXAMCOOKIE APS, O=EXAMCOOKIE APS, L=Aalborg, C=DK") ? (Operators.CompareString(x509Certificate2.SerialNumber, "0DDBCC532605343EB5A7E38F83B504FD", false) == 0 ? (Operators.CompareString(x509Certificate2.Thumbprint, "9A7635A149B3841485992ACFC92066E73A9FFE3D", false) == 0 ? 0 : 5) : 4) : 3;
  [...]
  return num;
}
C#
public int VmDetect() {
  if (this.CheckFileSignature(str4) == 0) {
    [...]
    // execute the binary
  }
  else {
    Module1.Log(Module1.LogType.ERROR, (object) MethodBase.GetCurrentMethod(), "VM Detect er ikke signeret med ExamCookie certifikatet: {0}", (object) str4);
    [...]
  }
}

Why bypass the virtual machine detection at all? Running software in a VM isolates it from your main system, which protects your personal data and contains malware. ExamCookie blocks VMs to stop cheating, so students have to choose between protecting their own machine and running the exam software the way it wants to be run.

In my opinion, people who want to cheat will always find a way, and the virtual machine detection mostly stops users from running the software in a secure environment or on an operating system that isn’t Windows or macOS.

Bypassing it

This part of my original writeup was written before I read secret.club’s post. In my younger days, I used this technique to bypass PunkBuster and FairFight.

By hooking into BitBlt, we can hide windows before the screenshot is taken. The possibilities are endless.

Since all the fun of hooking BitBlt and other native functionality has already been done by secret.club, I went another way: getting around their detection without touching their client at all, using WDA_EXCLUDEFROMCAPTURE.

On Windows, applications can set the WDA_EXCLUDEFROMCAPTURE flag to prevent their windows from appearing in screenshots or screen recordings. It’s a normal Windows API meant for privacy-sensitive apps. Here’s a tiny Electron app that uses it:

JavaScript
const { app, BrowserWindow } = require('electron')
const WEB_URL = 'https://chat.openai.com'

const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    alwaysOnTop: true,
    skipTaskbar: true,
    hasShadow: false,
  });

  // exclude app from screenshots and screen recordings
  win.setContentProtection(true)
  win.loadURL(WEB_URL)
}

app.whenReady().then(() => {
  createWindow()
})

A few lines of code hide any window from ExamCookie’s screenshot monitoring. The same approach works for Exammonitor.

What about Exammonitor by EDU?

I assumed Exammonitor would only be downloadable around exam time, to make reverse engineering harder. Nope. You can download the jar from https://login.exammonitor.dk/exam.jar and extract it using a tool like vineflower.

Exammonitor is written in Java, not C#, and Java decompiles just as easily. I didn’t find any hardcoded credentials, but it doesn’t seem to have any protection against malicious users either. The same bypass method used for ExamCookie works for Exammonitor.

Below is a snippet of the code that takes screenshots and sends them to the server.

Java
for (GraphicsDevice var12 : var11) {
  Robot var16 = new Robot(var12);
  Rectangle var8 = var12.getDefaultConfiguration().getBounds();
  DisplayMode var9 = var12.getDisplayMode();
  String var4 = "" + var12.hashCode() + var9.getWidth() + var9.getHeight();
  BufferedImage var13 = var16.createScreenCapture(new Rectangle((int)var8.getMinX(), (int)var8.getMinY(), var9.getWidth(), var9.getHeight()));
  ByteArrayOutputStream var17 = new ByteArrayOutputStream();
  ImageIO.write(var13, "jpg", var17);
  var17.flush();
  byte[] var14 = var17.toByteArray();
  var17.close();
  ByteArrayBody var15 = new ByteArrayBody(var14, var4 + ".jpg");
  var3.a("uploaded[]", var15);
  var3.a("screenid[]", var4);
}

Besides screenshots, Exammonitor also collects the process list, network adapters, and active applications, and sends them to the server at semi-random intervals. Here’s the code for that:

Java
if (this.aq <= 0 || var1) {
  this.aq = (int)(Math.random() * (double)Integer.parseInt(E.t().j("r_delay"))) + Integer.parseInt(E.t().j("s_delay"));
  StringBuilder var11 = new StringBuilder();
  StringBuilder var16 = new StringBuilder();
  StringBuilder var9 = new StringBuilder();

  for (OSProcess var21 : this.as.getOperatingSystem().getProcesses()) {
      var16.append(var21.getCommandLine().replace("\n", "").replace("\r", "") + "\n");
  }

  s var19;
  (var19 = s.l()).a("type", "systeminfo");
  var19.a("interfacelist", var11.toString());
  var19.a("processlist", var16.toString());
  var19.a("connectionlist", var9.toString());
  var19.m();
}

Conclusion

Does easily bypassed software like ExamCookie create a false sense of integrity? If so, why monitor everyone when so few people cheat in exams?

If we want to prevent people from cheating no matter the cost, we need to ask ourselves why. People who cheat have been cheating for the past 100 years, and that’s not going to change with surveillance software that can be bypassed in an afternoon.

Software that’s this trivial to bypass, aimed at a group where so few people cheat, is security theater.

  • Marius Kieler, over and out.