Back to all posts

ExamCookie - The illusion of exam integrity 5 years later

Information!

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

Why am I reversing this?

As a student required to use ExamCookie this summer, I was curious about its functionality and whether it posed any concerns for users. Initially, I considered running the exam through a Windows VM. However, after examining ExamCookie, I concluded that the potential hassle of facing additional issues if my exam session were flagged wasn’t worth the risk.

How does ExamCookie work?

Functionally, there have been no new features added to ExamCookie since the analysis performed by secret.club. The software continues to monitor for the following activities:

  • Checks if you are running in a virtual environment
  • 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

There have been some attempts at improving security. Notably, there is now a layer of actual authentication, in addition to the previously hardcoded username and password required for accessing the ‘WCF service’.

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

However, it might seem like when authenticating with UniLogin, the authentication is being done with only a username and no password. This is not the case, as the ClientSignIn method is called with an empty password field and the USERNAME is the token used to exchange for an access token, and the actual authentication is done in the GetUniLoginToken method.

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;
}

However, the credentials needed to access the ‘WCF service’ are still in plaintext located in Resources.resx, and are still the same, 5 years later. Without user authentication, I cannot imagine those credentials gaining access to anything other than the authentication API. I have not confirmed this, since it would be illegal to do so.

<data name="WCF_USERNAME" xml:space="preserve">
  <value>VfUtTaNUEQ</value>
</data>
<data name="WCF_PASSWORD" xml:space="preserve">
  <value>AwWE9PHjVc</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.

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;
}
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);
    [...]
  }
}

You might be wondering why someone would want to bypass the virtual machine detection in the first place. The answer lies in privacy and security. Running software in a virtual machine allows users to isolate the software from their main system, protecting their personal data and preventing potential malware from causing harm. However, some software, like ExamCookie, includes virtual machine detection as a measure against cheating. This creates a conflict between the desire for security and privacy and the need to use the software as intended.

In my opinion, those who want to cheat will always find a way to cheat, and the virtual machine detection primarily serves to prevent users from running the software in a secure environment or using an operating system that is not 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, let me approach this another way - bypassing their detections without hooking their client, but by utilizing WDA_EXCLUDEFROMCAPTURE.

On Windows, applications can set the WDA_EXCLUDEFROMCAPTURE flag to prevent their windows from appearing in screenshots or screen recordings. This is a legitimate Windows API feature intended for privacy-sensitive applications. Here’s a simple Electron application that demonstrates this:

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()
})

With just a few lines of code, any window can be completely hidden from ExamCookie’s screenshot monitoring. The same approach works for Exammonitor.

What about Exammonitor by EDU?

Contrary to my initial assumption that Exammonitor would only be available close to exam times to prevent reverse engineering, it turns out that this is not the case. You can download the jar from https://login.exammonitor.dk/exam.jar and extract it using a tool like vineflower.

Interestingly, Exammonitor is written in Java, not C#. Java code can also be easily decompiled. While I didn’t find any hardcoded credentials in the code, it doesn’t seem to have protections against malicious users. 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.

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);
}

In addition to taking screenshots, Exammonitor also collects information about the system, such as the process list, network adapter list, and active applications. This information is sent to the server at regular intervals. Below is a snippet of the code that sends system information to the server.

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.

There’s no reason to monitor everyone when so few people actually cheat, and solutions like ExamCookie are so trivial to bypass. This creates security theater rather than actual security.

  • Marius Kieler, over and out.