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:

Now that 5 years has passed, lets begin!

What has changed?

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 a 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 a 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' is 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 oppinion, cheaters will always find a way to cheat, and the only thing that the virtual machine detection does is to prevent people from using the software in a secure environment - or using a operating system that is not windows or mac.

Bypassing it

This part of my original write down was written before i read secret.club's post - In my younger days I used this to bypass ( PunkBuster & FairFight )

By hooking into bitblt, we can hide ‘cheating’ windows like chatgpt, 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 approch this another way - bypassing their detections without hooking their client, but by abusing "WDA_EXCLUDEFROMCAPTURE"

Check out examsnyd.dk for a working application.

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 spy on everyone, when SO few people cheat in exams? If we want to prevent people from cheating no matter the cost - we need to take a step back and ask ourselves, why? The people who cheat, have been cheating for the past 100 years, and that is not going to change. No reason to spy on everyone, when so few people cheat - and solutions like ExamCookie are so trivial to bypass.