[Translation] Surveillance: ExamCookie

[Translation] Surveillance: ExamCookie


I learned that the Danish government is not just suspended the Digital Exam Monitor program, which we analyzed and completely bypassed in previous article , and maybe completely shut down this system a week after we informed them about the hacking method. I don’t want to think that purely because of us the Danish government abandoned the idea of ​​monitoring exams, but our work was clearly noticed.

In this article, we will outline the technical details of how another schoolchildren surveillance tool works: ExamCookie. If you are only interested in a system walk, scroll down to the appropriate section.

ExamCookie


Recently, this tool got into the news due to an investigation into a violation of the GDPR. We decided to take a look at the second largest competitor of the above-mentioned system of spying on schoolchildren during the exams: ExamCookie . This is a commercial tracking system that is used by more than 20 Danish schools. There is no documentation on the site, except the following description:

ExamCookie is a simple software that monitors a student’s computer activity during an exam to make sure that the rules are followed. The program prohibits students from using any illegal form of assistance.

ExamCookie saves all activity on the computer: active URLs, network connections, processes, clipboard, and screenshots when the window is resized.

The program works simply: by going to the exam, you run it on your computer, and it controls your activity. When the exam is completed, the program closes and you can remove it from your computer.

To start surveillance, you need to use your UNI login, which works on various educational sites, or manually enter your credentials. We did not use the tool, so we cannot say in which cases the input is used manually. Perhaps this is done for students who do not have a UNI-login, which we do not consider possible.



Binary Information


The program can be downloaded from the main page of the ExamCookie website. It is an x86 .NET application. For reference, the analyzed binary MD5 hash is 63AFD8A8EC26C1DC368D8FF8710E337D , the signature is EXAMCOOKIE APS dated April 24, 2019. As shown by the last article , the analysis of the .NET binary can hardly be called reverse engineering, because the combination of easy-to-read IL code and metadata gives perfect source code.

Unlike the previous monitoring program, the developers of this tool not only removed it from the debug log, but also obfuscated it. At least tried :-)

Obfuscation (laughter to tears)


Opening the application in dnSpy, we quickly noticed the missing entry point:

 //Token: 0x0600003D RID: 61 RVA: 0x00047BB0 File Offset: 0x00045FB0
 [STAThread]
 [DebuggerHidden]
 [EditorBrowsable (EditorBrowsableState.Advanced)]
 [MethodImpl (MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
 internal static void Main (string [] Args)
 {
 
 }  

Strangely, some kind of packer is usually assumed, it modifies the bodies of the methods from the module constructor, which runs to the actual entry point, let's see:

 //Token: 0x06000001 RID: 1 RVA: 0x00058048 File Offset: 0x00055048
 static & lt; Module & gt; ()
 {
 & lt; Module & gt;  u202D \ u206C \ u202C \ u206C \ u200F \ u202C \ u206C \ u202C \ u200C \ u206A \ u200C \ u206C \ u20B \ u206B \ u202B \ u206E \ u202C \ u202B \ u202E ();
 & lt; Module & gt;  u202D \ u206D \ u200C \ u200F \ u206E \ u200F \ u206E \ u206A \ u202B \ u206B \ u200E \ u206B \ u202E \ u206F \ u206A \ u202E \ u202C \ u202A \ u202E ();
 & lt; Module & gt;  u206E \ u200D \ u206D \ u206A \ u202A \ u202C \ u200C \ u206F \ u206B \ u206E \ u200D \ u202E \ u206F \ u200C \ u206B \ u200E \ u206D \ u206A \ u202E ();
 }  

Cool. It's 2019 now, and people still use Confuser (Ex).

We instantly recognized this unpacking code and checked the assembler headers:

 [module: ConfusedBy ("Confuser.Core 1.1.0 + a36320377a")] 

At the moment, we thought that the code would actually be obfuscated, because the above-mentioned constructor decrypts the bodies and resources of the method. But, to our surprise, the developer of obfuscation decided ... not to rename the metadata:



This kills the whole buzz of reverse engineering. As we said in the last article , I would like to face the real problem of a properly protected A high-quality surveillance tool that takes more than five minutes to analyze.

In any case, unpacking any binary protected by confuser (ex) is very simple: use the .NET binary dumper or the ret instruction breakpoint in & lt; MODULE & gt; .ctor and dump it yourself. The process takes 30 seconds, and this packer will always remain my favorite, because protection against debug never works .

We decided to use MegaDumper: it's a bit faster than manually dumping:



After dumping the ExamCookie binary, the following message should appear:



Now you have a directory with all the assembler fragments that are loaded into the corresponding process, this time with decrypted method bodies.

Whoever implemented this obfuscation, thank God, at least he encrypted the strings:

  else if (System.Windows.Forms.Clipboard.ContainsData (DataFormats.SymbolicLink))
 {
 Module1.DebugPrint (& lt; Module & gt; .smethod_5 & lt; string & gt; (1582642794u), new object [0]);
 }
 else if (System.Windows.Forms.Clipboard.ContainsData (DataFormats.Tiff))
 {
 Module1.DebugPrint (& lt; Module & gt; .smethod_2 & lt; string & gt; (4207351461u), new object [0]);
 }
 else if (System.Windows.Forms.Clipboard.ContainsData (DataFormats.UnicodeText))
 {
 Module1.DebugPrint (& lt; Module & gt; .smethod_5 & lt; string & gt; (3536903244u), new object [0]);
 }
 else if (System.Windows.Forms.Clipboard.ContainsData (DataFormats.WaveAudio))
 {
 Module1.DebugPrint (& lt; Module & gt ;.smethod_2 & lt; string & gt; (2091555364u), new object [0]);
 }  

Yes, the good old Confuser (Ex) string encryption is the best pseudo-security in the .NET world. It’s good that Confuser (Ex) was so often hacked that deobfuscation tools are available for each mechanism on the Internet, so we’ll not touch anything about .NET. Launch the ConfuserExStringDecryptor binary from CodeCracker on the binary:



He converts the previous snippet into this:

  else if (System.Windows.Forms.Clipboard.ContainsData (DataFormats.SymbolicLink))
 {
 Module1.DebugPrint ("ContainsData.SymbolicLink", new object [0]);
 }
 else if (System.Windows.Forms.Clipboard.ContainsData (DataFormats.Tiff))
 {
 Module1.DebugPrint ("ContainsData.Tiff", new object [0]);
 }
 else if (System.Windows.Forms.Clipboard.ContainsData (DataFormats.UnicodeText))
 {
 Module1.DebugPrint ("ContainsData.UnicodeText", new object [0]);
 }
 else if (System.Windows.Forms.Clipboard.ContainsData (DataFormats.WaveAudio))
 {
 Module1.DebugPrint ("ContainsData.WaveAudio", new object [0]);
 }  

This is the whole application protection, broken in less than a minute ... We will not post our tools here, because we did not develop them and we do not have the source code. But anyone who wants to repeat the work can find them on Tuts4You . We no longer have a tuts4you account, so we cannot put a link to the mirrors.

Functionality


Surprisingly, no real "hidden functionality" was found. As indicated on the site, the following information is periodically sent to the server:

  • Process list (every 5000 ms)
  • Active application (every 1000 ms)
  • Clipboard (every 500 ms)
  • screenshot (every 5000 ms)
  • Network Adapter List (every 20,000 ms)

The rest of the application is very boring, so we decided to skip the entire initialization procedure and go directly to the functions responsible for capturing information.

Adapter


Network adapters are built with the .NET NetworkInterface.GetAllNetworkInterfaces () function, exactly as in previous article :

  NetworkInterface [] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces ();
 foreach (NetworkInterface networkInterface in allNetworkInterfaces)
 {
 try
 {
//...
//TEXT FORMATTING OMITTED
//...

  dictionary.Add (networkInterface.Id, stringBuilder.ToString ());
  stringBuilder.Clear ();
 }
 catch (Exception ex)
 {
  AdapterThread.OnExceptionEventHandler onExceptionEvent = this.OnExceptionEvent;
  if (onExceptionEvent! = null)
  {
  onExceptionEvent (ex);
  }
 }
 }
 result = dictionary;  

Active Application


This is getting interesting. Instead of registering all open windows, the utility monitors only the active application. The implementation is inflated, therefore we give the following pseudocode:

  var whiteList = {
  "devenv",
  "ExamCookie.WinClient",
  "ExamCookie.WinClient.vshost",
  "wermgr",
  "ShellExperienceHost"};
//GET WINDOW INFORMATION
 var foregroundWindow = ApplicationThread.GetForegroundWindow ();
 ApplicationThread.GetWindowRect (foregroundWindow, ref rect);
 ApplicationThread.GetWindowThreadProcessId (foregroundWindow, ref processId);
 var process = Process.GetProcessById (processId);

 if (process == null)
  return;
//LOG BROWSER URL
 if (IsBrowser (process))
 {
  var browserUrl = UiAutomation32.GetBrowserUrl (process.Id, process.ProcessName);

//SEND BROWSER URL TO SERVER
  if (ValidBrowserUrl (browserUrl))
  {
  ReportToServer (browserUrl);
  }

 }
 else if (! whiteList.contains (process.ProcessName, StringComparer.OrdinalIgnoreCase))
 {
  ReportToServer (process.MainWindowTitle);
 }  

Great ... people still use process names to differentiate them. They never stop and do not think: “Wait, you can change the names of the processes as you like,” so we can safely bypass this protection.

If you have read a previous article about another exam surveillance program, you will probably recognize this implementation of subpar for searching browsers:

  private bool IsBrowser (System.Diagnostics.Process proc)
 {
 bool result;
 try
 {
  string left = proc.ProcessName.ToLower ();
  if (Operators.CompareString (left, "iexplore", false)! = 0 & amp; & amp;
  Operators.CompareString (left, "chrome", false)! = 0 & amp; & amp;
  Operators.CompareString (left, "firefox", false)! = 0 & amp; & amp;
  Operators.CompareString (left, "opera", false)! = 0 & amp; & amp;
  Operators.CompareString (left, "cliqz", false)! = 0)
  {
  if (Operators.CompareString (left, "applicationframehost", false)! = 0)
  {
  result = false;
  }
  else
  {
  result = proc.MainWindowTitle.Containing ("Microsoft Edge");
  }
  }
  else
  {
  result = true;
  }
 }
 catch (Exception ex)
 {
  result = false;
 }
 return result;
 }  

  private string GetBrowserName (string name)
 {
 if (Operators.CompareString (name.ToLower (), "iexplore", false) == 0)
 {
  return "IE-Explorer";
  }
  else if (Operators.CompareString (name.ToLower (), "chrome", false) == 0)
  {
  return "Chrome";
  }
  else if (Operators.CompareString (name.ToLower (), "firefox", false) == 0)
  {
  return "Firefox";
  }
  else if (Operators.CompareString (name.ToLower (), "opera", false) == 0)
  {
  return "Opera";
  }
  else if (Operators.CompareString (name.ToLower (), "cliqz", false) == 0)
  {
  return "Cliqz";
  }
  else if (Operators.CompareString (name.ToLower (), "applicationframehost", false) == 0)
  {
  return "Microsoft Edge";
  }

  return "";
 }  

And the cherry on the cake:

  private static string GetBrowserUrlById (object processId, string name)
 {//...
  automationElement.GetCurrentPropertyValue (/*...*/);
 
  return url;
 }  

This is literally the same implementation as in the previous article. It is difficult to understand how the developers still do not understand how bad it is. Anyone can edit the URL in the browser, it is not even worth demonstrating.

Virtual Machine Discovery


Contrary to what is said on the website, launching on a virtual machine sets the flag. The implementation is ... interesting.

  File.WriteAllBytes ("ecvmd.exe", Resources.VmDetect);

 using (Process process = new Process ())
 {
 process.StartInfo = new ProcessStartInfo ("ecvmd.exe", "-d")
 {
  CreateNoWindow = true
  UseShellExecute = false,
  RedirectStandardOutput = true
 };
 process.Start ();
 try
 {
  using (StreamReader standardOutput = process.StandardOutput)
  {
  result = standardOutput.ReadToEnd (). Replace ("\ r \ n", "");
  }
 }
 catch (Exception ex3)
 {
  result = "-5";
 }
 }  

Well, for some reason they write an external binary to disk and execute it, and then rely completely on I/O results. This really happens quite often, but the transfer of such important work to another unprotected process is so-so. Let's see what file we are dealing with:



So now we use C ++? Well, interoperability is not really bad. And this may mean that we now really have to work on the reverse development (!!).Look at IDA:

  int __cdecl main (int argc, const char ** argv, const char ** envp)
 {
  int v3;//ecx
  BOOL v4;//ebx
  int v5;//ebx
  int * v6;//eax
  int detect;//eax
  bool vbox_key_exists;//bl
  char vpcext;//bh
  char vmware_port;//al
  char * vmware_port_exists;//ecx
  char * vbox_detected;//edi
  char * vpcext_exists;//esi
  int v14;//eax
  int v15;//eax
  int v16;//eax
  int v17;//eax
  int v18;//eax
  int v20;//[esp + 0h] [ebp-18h]
  HKEY result;//[esp + Ch] [ebp-Ch]
  HKEY phkResult;//[esp + 10h] [ebp-8h]

  if (argc! = 2)
  goto LABEL_20;
  v3 = strcmp (argv [1], "-d");
  if (v3)
  v3 = - (v3 & lt; 0) |  one;
  if (! v3)
  {
  v4 = (unsigned __int8) vm_detect :: vmware_port ()! = 0;
  result = 0;
  v5 = (vm_detect :: vpcext ()! = 0? 2: 0) + v4;
  RegOpenKeyExA (HKEY_LOCAL_MACHINE, "HARDWARE \\ ACPI \\ DSDT \\ VBOX__", 0, 0x20019u, & amp; result);
  v6 = sub_402340 ();
 LABEL_16:
  sub_404BC0 ((int) v6, v20);
  return 0;
  }
  detect = strcmp (argv [1], "-s");
  if (detect)
  detect = - (detect & lt; 0) |  one;
  if (! detect)
  {
 LABEL_20:
  phkResult = 0;
  vbox_key_exists = RegOpenKeyExA (HKEY_LOCAL_MACHINE, "HARDWARE \\ ACPI \\ DSDT \\ VBOX__", 0, 0x20019u, & amp; phkResult) == 0;
  vpcext = vm_detect :: vpcext ();
  vmware_port = vm_detect :: vmware_port ();
  vmware_port_exists = "1";
  vbox_detected = "1";
  if (! vbox_key_exists)
  vbox_detected = "0";
  vpcext_exists = "1";
  if (! vpcext)
  vpcext_exists = "0";
  if (! vmware_port)
  vmware_port_exists = "0";
  result = (HKEY) vmware_port_exists;
  v14 = std :: print ((int) & amp; dword_433310, "VMW =");
  v15 = std :: print (v14, (const char *) result);
  v16 = std :: print (v15, ", VPC =");
  v17 = std :: print (v16, vpcext_exists);
  v18 = std :: print (v17, ", vib =");
  v6 = (int *) std :: print (v18, vbox_detected);
  goto LABEL_16;
  }
  return 0;
 }  

This checks for the presence of the I/O port 'VX' from VMWare:

  int __fastcall vm_detect :: vmware_port ()
 {
  int result;//eax

  result = __indword ('VX');
  LOBYTE (result) = 0;
  return result;
 }  

Next, it checks the execution of the virtual pc extension instruction, which should work only when running in a virtualized environment, if it does not cause the machine to crash if it is not properly processed;):

  char vm_detect :: vpcext ()
 {
  char result;//al

  result = 1;
  __asm ​​{vpcext 7, 0Bh}
  return result;
 }  

... no real reverse engineering, just 30 seconds to rename two functions :(

This program simply reads the registry key and runs two hypervisor checks that look weird compared to their other program. I wonder where they copied it? Oh, look, an article titled “Methods for detecting virtual (sic) machines” which explains these methods :). In any case, these detection vectors can be circumvented by editing the .vmx file or using an enhanced version of any hypervisor to your taste.

Data Protection


As mentioned earlier, there is now an investigation into non-compliance with the GDPR, and their website states:

Data is encrypted and sent to a secure Microsoft Azure server, which can only be accessed with the correct credentials. After the exam data is stored for up to three months.

We are not quite sure how they define the “security” of the server, since the credentials are tightly coded in the application and stored in clear text in the metadata resources:

 Endpoint: https://examcookiewinapidk.azurewebsites.net
 Username: VfUtTaNUEQ
 Password: AwWE9PHjVc 

We have not studied the contents of the server (this is illegal), but we can assume that full access is granted there. Since the account is hard-coded in the application, there is no isolation between the containers of student data.

Legal disclaimer: we have the right to publish API credentials, since they are stored in a public binary file and, therefore, are not obtained illegally. However, their use with malicious intent clearly violates the law, so we strongly recommend readers not to use the above-mentioned credentials, and are not responsible for any potential actions.

Bypass


Since this application is incredibly reminiscent of Digital Exam Monitor, we simply updated ayyxam code to support ExamCookie.

Process List


The .NET process interface internally caches process data using the ntdll! NtQuerySystemInformation system call. Hiding processes from him requires some work, because the process information is listed in many places. Fortunately, .NET retrieves only one specific type of information, so you don’t have to use all the latebros methods.

Code to bypass the check of active processes.

  NTSTATUS WINAPI ayyxam :: hooks :: nt_query_system_information (
 SYSTEM_INFORMATION_CLASS system_information_class, PVOID system_information,
 ULONG system_information_length, PULONG return_length)
 {//DONT HANDLE OTHER CLASSES
 if (system_information_class! = SystemProcessInformation)
  return ayyxam :: hooks :: original_nt_query_system_information (
  system_information_class, system_information,
  system_information_length, return_length);
//HIDE PROCESSES
 const auto value = ayyxam :: hooks :: original_nt_query_system_information (
  system_information_class, system_information,
  system_information_length, return_length);
//DONT HANDLE UNSUCCESSFUL CALLS
 if (! NT_SUCCESS (value))
  return value;
//DEFINE STRUCTURE FOR LIST
 struct SYSTEM_PROCESS_INFO
 {
  ULONG NextEntryOffset;
  ULONG NumberOfThreads;
  LARGE_INTEGER Reserved [3];
  LARGE_INTEGER CreateTime;
  LARGE_INTEGER UserTime;
  LARGE_INTEGER KernelTime;
  UNICODE_STRING ImageName;
  ULONG BasePriority;
  HANDLE ProcessId;
  HANDLE InheritedFromProcessId;
 };
//HELPER FUNCTION: GET NEXT ENTRY IN LINKED LIST
 auto get_next_entry = [] (SYSTEM_PROCESS_INFO * entry)
 {
  return reinterpret_cast & lt; SYSTEM_PROCESS_INFO * & gt; (
  reinterpret_cast & lt; std :: uintptr_t & gt; (entry) + entry- & gt; NextEntryOffset);
 };
//ITERATE AND HIDE PROCESS
 auto entry = reinterpret_cast & lt; SYSTEM_PROCESS_INFO * & gt; (system_information);
 SYSTEM_PROCESS_INFO * previous_entry = nullptr;
 for (; entry- & gt; NextEntryOffset & gt; 0x00; entry = get_next_entry (entry))
 {
  constexpr auto protected_id = 7488;
  if (entry- & gt; ProcessId == reinterpret_cast & lt; HANDLE & gt; (protected_id) & amp; previous_entry! = nullptr)
  {
//SKIP ENTRY
  previous_entry- & gt; NextEntryOffset + = entry- & gt; NextEntryOffset;
  }

//SAVE PREVIOUS ENTRY FOR SKIPPING
  previous_entry = entry;
 }

 return value;
 }  

Buffer


For the internal implementation of buffers in .NET, ole32.dll! OleGetClipboard is responsible, which is very susceptible to hooks. Instead of spending a lot of time analyzing internal structures, you can simply return S_OK , and .NET error handling does the rest:

  std :: int32_t __stdcall ayyxam :: hooks :: get_clipboard (void * data_object [[maybe_unused]])
 {//lol
 return S_OK;
 }  

This will hide the entire buffer from the ExamCookie monitoring tool without disturbing the functionality of the program.

Screenshots


As always, people take a ready .NET implementation of the desired function. To bypass this function, we did not even have to change anything in the past code. Screenshots are controlled by the Graphics.CopyFromScreen .NET feature. It is essentially a wrapper for transmitting bit blocks that calls gdi32! BitBlt .As in video games, we can use BitBlt hooks and hide any unwanted information before taking screenshots to combat anti-cheat systems that take screenshots.


Opening sites


The Grabber URL is completely copied from the previous program, so that we can again use our code to bypass the protection. In last article we documented the structure of AutomationElement, which resulted in the following hook:

  std :: int32_t __stdcall ayyxam :: hooks :: get_property_value (void * handle, std :: int32_t property_id, void * value)
 {
 constexpr auto value_value_id = 0x755D;
 if (property_id! = value_value_id)
  return ayyxam :: hooks :: original_get_property_value (handle, property_id, value);

 auto result = ayyxam :: hooks :: original_get_property_value (handle, property_id, value);

 if (result! = S_OK)//SUCCESS?
  return result;
//VALUE URL IS STORED AT 0x08 FROM VALUE STRUCTURE
 class value_structure
 {
 public:
  char pad_0000 [8];//0x0000
  wchar_t * value;//0x0008
 };
 auto value_object = reinterpret_cast & lt; value_structure * & gt; (value);
//ZERO OUT OLD URL
 std :: memset (value_object- & gt; value, 0x00, std :: wcslen (value_object- & gt; value) * 2);
//CHANGE TO GOOGLE.COM
 constexpr wchar_t spoofed_url [] = L "https://google.com";
 std :: memcpy (value_object- & gt; value, spoofed_url, sizeof (spoofed_url));

 return result;
 }  

Virtual Machine Discovery


Lazy detection of a virtual machine can be circumvented in two ways: 1) a program patch that is flushed to disk; or 2) redirect the process of creating a process to a dummy application. The latter seems obviously easier :). So, internally, Process.Start () calls CreateProcess , so it’s enough to make a hook and redirect it to any dummy application that prints the character '0'.

  BOOL WINAPI ayyxam :: hooks :: create_process (
 LPCWSTR application_name,
 LPWSTR command_line,
 LPSECURITY_ATTRIBUTES process_attributes,
 LPSECURITY_ATTRIBUTES thread_attributes,
 BOOL inherit_handles,
 DWORD creation_flags,
 LPVOID environment,
 LPCWSTR current_directory,
 LPSTARTUPINFOW startup_information,
 LPPROCESS_INFORMATION process_information
 )
 {
//REDIRECT PATH OF VMDETECT TO DUMMY APPLICATION
 constexpr auto vm_detect = L "ecvmd.exe";
 if (std :: wcsstr (application_name, vm_detect))
 {
  application_name = L "dummy.exe";
 }

 return ayyxam :: hooks :: original_create_process (
  application_name, command_line, process_attributes,
  thread_attributes, inherit_handles, creation_flags,
  environment, current_directory, startup_information,
  process_information);
 }  

Download


The whole project is available in the Github repositories . The program works by injecting a binary x86 file into the appropriate process.

Source text: [Translation] Surveillance: ExamCookie