SECRET CLUB

The nadir of surveillance (Den Digitale Prøvevagt)


If you only came here for the bypass, scroll down to the Circumvention section at the bottom.

Danish Exam surveillance

The Danish Ministry of Education recently published an update to the current school exam system, Net Exams, which announced the implementation of The Digital Exam Monitor (Den Digitale Prøvevagt in Danish). This surveillance program is mandatory for any exam attendee if said student uses a compatible operating system (Windows and OS X); if the student chooses not to use any of the aforementioned operating systems, they will have to take the exam under “strict supervision”. This strict supervision is not defined in their announcement and according to their guidelines is completely arbitrary. Students are currently publicly announcing their distrust to the state agency, as the surveillance tool is neither open-source nor seemingly properly documented on their website, besides the following description:

The Digital Exam Monitor begins its surveillance when the respective exam begins. This means that The Digital Exam Monitor can be installed on student’s machines and ready to use, without the student being monitored. The Digital Exam Monitor automatically stops the surveillance when the student turns in their assignment. In case the student does not turn in any assignment, the surveillance automatically stops ten minutes after the exam ends.

When students launch The Digital Exam Monitor, the program will periodically send information to Net exams regarding its launch. This enables the respective exam guards to monitor which students are ready to participate in the exam. As the exam begins, The Digital Exam Monitor continually logs web-addresses and active processes on the computer. The Digital Exam Monitor will also periodically send screenshots as well as on screen resolution changes.

This vague description has sparked fear in many students, questioning the motive and means of the respective surveillance tool. In this article we will try to clarify the functionality, as well as publish an example of circumvention to prove that the surveillance tool needs to be reworked. As we will show, this is a complete disaster and its flaws can not go unnoticed.

Executable information

The program is a x86 .NET executable that is deployed through ClickOnce. The binary of observation was compiled 03/03/2019 and the md5 hash is 3C1F9F3CF1E4BC2BD1C53C929F696B6E. Due to the fact that the executable is .NET, reverse engineering the respective binaries is a piece of cake, especially considering the binary has not been obfuscated at all and has been released with complete type information, essentially granting us 1:1 source code.

When you launch the surveillance software, a screen overlay opens telling you that the exam has not begun. For the first five seconds of launch, the entire primary screen is scanned for a QR code that contains the 6 letter exam identification number. This QR code is presumably shown on the Net exam website on exam day. If the QR code was not found, a simple winforms popup is shown asking for manual input.

The publishers have not removed any of the debug code in this binary, which allows anyone to patch the Optional Header -> Subsystem to Windows Console and see the console debug output. Two exam identification numbers have also been hardcoded into the application, 000000 and _TEST_. These two ids are respectively for offline testing and emulation, the latter simulating sending data, enabling reverse engineers to test their hooks locally while seeing how the packet data changes.

After a valid exam identification number has been acquired, the server gets notice of a hardware identification number, the Windows version and the binary version. The hardware identification number is calculated as shown:

public static string GetFingerPrint()
{
	if (string.IsNullOrEmpty(MachineFingerprintGenerator.fingerPrint))
	{
		MachineFingerprintGenerator.fingerPrint = MachineFingerprintGenerator.GetHash(
			"CPU >> " + MachineFingerprintGenerator.cpuId() + '\n' +
			"BIOS >> " + MachineFingerprintGenerator.biosId() + '\n' +
			"BASE >> " + MachineFingerprintGenerator.baseId() + '\n' +
			"DISK >> " + MachineFingerprintGenerator.diskId() + '\n' +
			"VIDEO >> " + MachineFingerprintGenerator.videoId() + '\n' +
			"MAC >> " + MachineFingerprintGenerator.macId()
		);
	}
	return MachineFingerprintGenerator.fingerPrint;
}

private static string cpuId()
{
	string id = MachineFingerprintGenerator.identifier("Win32_Processor", "UniqueId");

	if (id != string.Empty)
		return id;

	id = MachineFingerprintGenerator.identifier("Win32_Processor", "ProcessorId");

	if (id != string.Empty)
		return id;

	id = MachineFingerprintGenerator.identifier("Win32_Processor", "Name");

	if (id != string.Empty)
		return id;

	id = MachineFingerprintGenerator.identifier("Win32_Processor", "Manufacturer");
	
	if (id != string.Empty)
		return id;

	return MachineFingerprintGenerator.identifier("Win32_Processor", "MaxClockSpeed");
}

private static string biosId() => 
	MachineFingerprintGenerator.identifier("Win32_BIOS", "Manufacturer") +
	MachineFingerprintGenerator.identifier("Win32_BIOS", "SMBIOSBIOSVersion") +
	MachineFingerprintGenerator.identifier("Win32_BIOS", "IdentificationCode") +
	MachineFingerprintGenerator.identifier("Win32_BIOS", "SerialNumber") + 
	MachineFingerprintGenerator.identifier("Win32_BIOS", "ReleaseDate") +
	MachineFingerprintGenerator.identifier("Win32_BIOS", "Version")

private static string baseId() =>
	MachineFingerprintGenerator.identifier("Win32_BaseBoard", "Model") + 
	MachineFingerprintGenerator.identifier("Win32_BaseBoard", "Manufacturer") + 
	MachineFingerprintGenerator.identifier("Win32_BaseBoard", "Name") + 
	MachineFingerprintGenerator.identifier("Win32_BaseBoard", "SerialNumber");

private static string diskId() =>
	MachineFingerprintGenerator.identifier("Win32_DiskDrive", "Model") + 
	MachineFingerprintGenerator.identifier("Win32_DiskDrive", "Manufacturer") + 
	MachineFingerprintGenerator.identifier("Win32_DiskDrive", "Signature") + 
	MachineFingerprintGenerator.identifier("Win32_DiskDrive", "TotalHeads");

private static string videoId() => 
	MachineFingerprintGenerator.identifier("Win32_VideoController", "DriverVersion") + 
	MachineFingerprintGenerator.identifier("Win32_VideoController", "Name");

private static string macId() => 
	MachineFingerprintGenerator.identifier("Win32_NetworkAdapterConfiguration", "MACAddress", "IPEnabled");

This is a very vague hardware identifier and will be different if the exam attendee updates their graphics driver, making it essentially useless for long-term identification. To be fair, this is most likely just used during the exam to tell different machines apart, but the implementation is noticibly sloppy, including static entires that do not add to identification.

Encryption

The binary executable has stored some information ‘encrypted’. The quotation marks are there because the actual encryption is obsolete and there’s absolutely no reason for it to be there. The cryptography routines are the following:

private static byte[] key = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };
private static byte[] iv = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };

public static string Encrypt(string text)
{
	ICryptoTransform cryptoTransform = DES.Create().CreateEncryptor(key, iv);
	byte[] bytes = Encoding.Unicode.GetBytes(text);
	return Convert.ToBase64String(cryptoTransform.TransformFinalBlock(bytes, 0, bytes.Length));
}

public static string Decrypt(string text)
{
	ICryptoTransform cryptoTransform = DES.Create().CreateDecryptor(key, iv);
	byte[] array = Convert.FromBase64String(text);
	return Encoding.Unicode.GetString(cryptoTransform.TransformFinalBlock(array, 0, array.Length));
}

This mechanism is being used in production right now and has already been deployed on thousands of machines. We’re not quite sure why they decided to encrypt anything with this as base64 would’ve done the job just as well.

Functionality

After initialisation, The Digital Exam Monitor will periodically send five types of data to the surveillance servers:

These information ‘packets’ are serialized and sent to the api-server https://1qk4wqinaf.execute-api.eu-west-1.amazonaws.com/test with the api-key bFywbPRqgF5uSnpfH4EhR45u36wIZjP46yQ3eDWX.

The Digital Exam Monitor has also added various packet identifiers that are not currently in use, hinting the features might come in a later revision or due to a decision to scrap these features before initial release. Here are the defined packet identifiers:

public enum ColTypeEnum
{
	Heartbeat = 1,
	NetworkControl,
	Screenshot,
	VmDetection,
	ProcessList,
	Clipboard,
	Keylog,
	Sites,
	ActiveApplication,
	Usb,
	Bttrf
}

We will neither be going over the full extent of the api system nor the packet format, as it is not relevant to our conclusion at the end of the article.

Screenshots

The periodic screenshot method is implemented using .NET’s Graphics library:

using (Image image = ScreenCaptureTool.CaptureScreenNew())
{
	var data = ScreenCaptureTool.ImageToByteArray(image, 30);
	packet = new List<DataPackage> {
		new DataPackage(
                    DataPackage.ColTypeEnum.Sshot, false, data, 
                    DataPackageEnvelopeAwsReceiver.ServerTime, base.GetAndIncrementWorkSequence())
	};
}
public static Image CaptureScreenNew()
{
	Rectangle bounds = Screen.PrimaryScreen.Bounds;
	Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height);
	Size blockRegionSize = new Size(bitmap.Width, bitmap.Height);
	using (Graphics graphics = Graphics.FromImage(bitmap))
	{
		graphics.CopyFromScreen(0, 0, 0, 0, blockRegionSize);
		return bitmap;
	}
}

These functions are in themselves fine, as most people will not have multiple screens in an exam situation, but since no rule explicitly states you’re not allowed to bring another monitor with you, the respective screenshot implementation is not sufficient.

The Digital Exam Monitor also sets up a hook for the EVENT_SYSTEM_FOREGROUND event, which is raised every time the active window changes on the machine, forcing the same aforementioned routine used for timed screenshots to be invoked.

Open website list

The Digital Exam Monitor will parse any selected tab’s URL of any of the four known browsers using an Automation element. We’ve decided to only show the implementation for Chrome as they are all very similar. The algorithm boils down to sending the CTRL+L hotkey (marks content of URL tab) to Chrome and copying the selected text.

private static CurrentBrowserUrlsTool.BrowserType Parse(string processName)
{
	processName = processName.ToLower();

	if (processName.Contains("chrome"))
	{
		return CurrentBrowserUrlsTool.BrowserType.GOOGLE_CHROME;
	}

	if (processName.Contains("applicationframehost"))
	{
		return CurrentBrowserUrlsTool.BrowserType.MICROSOFT_EDGE;
	}

	if (processName.Contains("iexplore"))
	{
		return CurrentBrowserUrlsTool.BrowserType.INTERNET_EXPLORER;
	}

	if (processName.Contains("firefox"))
	{
		return CurrentBrowserUrlsTool.BrowserType.FIREFOX;
	}

	return CurrentBrowserUrlsTool.BrowserType.Empty;
}
Process[] processes = Process.GetProcesses();

using (Dictionary<string, CurrentBrowserUrlsTool.BrowserType>.Enumerator enumerator = 
		CurrentBrowserUrlsTool._processSearchStringsForBrowsertypes.GetEnumerator())
{
	while (enumerator.MoveNext())
	{
		KeyValuePair<string, CurrentBrowserUrlsTool.BrowserType> browserPair = enumerator.Current;
		foreach (Process process in from p in processes
			where p.ProcessName.ToLower().Contains(browserPair.Key)
				select p)
		{
			if (process.MainWindowHandle == IntPtr.Zero)
				continue;
			
			string urlfromProcess = CurrentBrowserUrlsTool.GetURLFromProcess(
							process, browserPair.Value, process.MainWindowHandle);

			if (string.IsNullOrEmpty(urlfromProcess))
				continue;
			
			StaticFileLogger.Current.LogEvent(
				"CurrentBrowserUrlsTool._timer_Elapsed()", "Url harvested", 
				urlfromProcess, EventLogEntryType.Information);

			
			CurrentBrowserUrlsTool._urlSet.Add(urlfromProcess);
		}
	}
}
if (browser.Equals(CurrentBrowserUrlsTool.BrowserType.GOOGLE_CHROME))
{
	AutomationElement automationElement2 = null;
	try
	{
		AutomationElement automationElement3 = automationElement.FindFirst(
					TreeScope.Children, 
					new PropertyCondition(AutomationElement.NameProperty, "Google Chrome"));

		if (automationElement3 == null)
			return null;

		automationElement2 = 
			TreeWalker.RawViewWalker.GetLastChild(automationElement3).
				FindFirst(TreeScope.Children, 
					new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Pane)).
				FindFirst(TreeScope.Children, 
					new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Pane)).
				FindFirst(TreeScope.Descendants, 
					new PropertyCondition(AutomationElement.AccessKeyProperty, "Ctrl+L"));

		if (automationElement2 == null)
			automationElement2 = 
				automationElement3.FindFirst(
					TreeScope.Descendants, 
					new PropertyCondition(AutomationElement.AccessKeyProperty, "Ctrl+L"));
	}
	catch
	{
		return null;
	}

	if (automationElement2 == null)
		return null;

	if ((bool)automationElement2.GetCurrentPropertyValue(AutomationElement.HasKeyboardFocusProperty))
		return null;

	AutomationPattern[] supportedPatterns = automationElement2.GetSupportedPatterns();

	if (supportedPatterns.Length != 1)
		return null;

	string text = "";
	try
	{
		ValuePattern.ValuePatternInformation valuePatternInformation = 
			((ValuePattern)automationElement2.GetCurrentPattern(supportedPatterns[0])).Current;

		text = valuePatternInformation.Value;
	}
	catch
	{
	}

	if (text == "" || !Regex.IsMatch(text, "^(https:\\/\\/)?[a-zA-Z0-9\\-\\.]+(\\.[a-zA-Z]{2,4}).*$"))
		return null;

	
	if (!text.StartsWith("http"))
	{
		text = "http://" + text;
	}

	return text;
}

We noticed that instead of parsing the shell/open registry keys for installed browsers, they’ve decided to hardcode process names to differentiate between browsers. This is incredibly lazy as it allows anyone to rename their browser on disk to prevent the surveillance software from ever grabbing the active browser tab.

This point of data also suffers from the fact that anyone can modify the content of the address bar in their browser, essentially spoofing the result without any technical effort, as seen here:

As previously mentioned, modifying the PE headers to force open a console allows us to inspect the results of each event: eventSource='CurrentBrowserUrlsTool._timer_Elapsed()', title='Url harvested', description='https://hello.com', entryType='Information'

So without any real effort, this check can be bypassed. For the curious readers thinking: “How else would you do this?”, you wouldn’t. You never want to be dependent on changeable information like this. A proper method to log what a exam attendee was browsing would be to create browser-specific modules that hook the respective functions that are responsible for opening websites (easy to find). With that method, you can know with absolute certainty if a student is using any forbidden website.

Network interfaces

Unlike what was publicly announced, The Digital Exam Monitor sends information about all network adapters / interfaces to their server. At the time of writing, this is not publicly known, and will most likely feed into the idea that the responsible developers are monitoring more than they say. This is not really critical data you need to worry about, but the motive is unknown. We suspect this is used to log people using virtual private networks during exams. Exam situations in Denmark are taken on school wifi, which is usually monitored by the responsible IT department, and using a virtual private network would therefore make this surveillance unable to work. Oddly enough, using virtual private networks while taking an exam is not forbidden, which makes this check a little odd.

The network configuration routine is implemented as following:

internal string GetNetworkConfigurationData()
{
	this._builder.Clear();

	NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
	this._numberOfInterfacesAtLastCount = allNetworkInterfaces.Count<NetworkInterface>();

	(from nwi in allNetworkInterfaces.ToList<NetworkInterface>()
		orderby nwi.OperationalStatus
			select nwi).ToList<NetworkInterface>().
				ForEach(delegate(NetworkInterface nwi)
	{
		this._builder.Append(nwi.GetStateAsString());
	});

	return this._builder.ToString();
}

Running processes

As mentioned on the website, The Digital Exam Monitor will also log running processes, including the executable’s file description. The data is very straight forward, but is not really a definite point of interest when catching cheaters, as anyone can rename the executable or modify the executable’s manifest information. Skepticism aside, this would arguably flag cheaters unbeknownst to the process check, but you still won’t be able to draw conclusions from just a file name and some manifest information.

Process[] processes = Process.GetProcesses();
this._lastNumberOfRunningProcesses = processes.Count<Process>();
this._builder.Clear();
foreach (Process process in processes)
{
	string text = "";
	try
	{
		text = process.MainModule.FileVersionInfo.FileDescription;
	}
	catch
	{
	}
	this._builder.Append(string.Concat(new object[]
	{
		process.Id,
		",",
		process.ProcessName,
		",",
		text,
		";"
	}));
}

Inactive functionality

Interestingly enough, The Digital Exam Monitor contains several features somehow not truncated from the production build. These functions are never called in the binary and should’ve been optimized away, which implies that the production binary has been compiled without optimization. This inactive functionality has for the past few weeks sparked controversy in various newspapers as keylogging is always a hot topic and low-hanging fruit for criticism and conspiracy theories.

Virtual machine check

This virtual machine check is supposedly meant to detect anyone attending the exam while running the surveillance program itself on a separate virtual machine. What has ended up happening here is that they’re now detecting if you’re hosting one of the common commercial virtual machine, again by comparing process names. To be fair, they’re also checking specifically for the presence of the Hyper-V hypervisor, using a very vague WMI check that anyone with hypervisor, let alone kernel, access would be able to circumvent.

private bool AmIRunningInVirtualMachineOrHaveAVirtualMachineProcessRunning()
{
	return this.AmIRunningInsideAVirtualMachine() || this.IsVirtualMachineProcessRunning();
}
public bool AmIRunningInsideAVirtualMachine()
{
	try
	{
		foreach (ManagementBaseObject managementBaseObject in 
			new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_BaseBoard").Get())
		{
			ManagementObject baseObject = managementBaseObject["Manufacturer"];
			
			return (baseObject.ToString().ToLower() == "microsoft corporation".ToLower());
		}

	}
	catch
	{

	}

	return false;
}
private bool IsVirtualMachineProcessRunning()
{
	return Process.GetProcesses().Any((Process p) => p.ProcessName.ContainsOneOrMoreInList({
			"vmware",
			"virtualpc",
			"virtualbox"
		}));
}

Keylogger

As previously mentioned, the capability to monitor key input is present but not active. They Digital Exam Monitor is using a very standard windows hook on the WH_KEYBOARD_LL event, which might trigger heuristic based antiviruses on student machines under the exam, causing even more complications in the already stressed situtation.

private void KeyloggerHelper_KeyPressed(object sender, KeyEventArgs e)
{
	if (!char.IsLetterOrDigit((char)e.KeyCode))
	{
		this._builder.Append("[" + e.KeyCode + "]");
		return;
	}
	this._builder.Append(e.KeyCode);
}

Clipboard

Interestingly enough, there’s a full implementation of clipboard surveillance that for some reason is not being used. A clipboard check would be absolutely obvious to catch students plagiarizing under the exam by copying elements from other assignments.

if (Clipboard.ContainsText())
{
	return Encoding.UTF8.GetBytes(Clipboard.GetText());
}
else
{
	return Encoding.UTF8.GetBytes("no text");
}

Disclosure

We have contacted the Danish Ministry of Education some time ago, telling them about these flaws and how to combat them, but have not received any response on twitter nor email. This leads us to one conclusion: the Ministry of Education does not care that the tool is insufficient in catching cheaters, and as of now, is completely useless due to the following bypass.

Circumvention

How would we get around this surveillance system? According to their website, any student unable to use respective software for whatever reason, whether it is incompability or software issues, is allowed to take the exam under “strict supervision”, which makes the lazy bypass super easy: Delete its dependencies from your machine and it will not be able to run :) Deployment is dependent on ClickOnce, which is very rarely used by anything but installers (Chrome, for example, uses it), so you can without any issue delete it, or rename it for later recovery.

But telling you that would be a very boring conclusion, so we decided to write a complete native bypass, essentially a x86 usermode rootkit, to hide whatever cheeky website you’re using under the exam. Why would we do this, you may ask? Someone needs to take responsibility for the absolute disaster this software is, and the Ministry of Education needs to be transparent about what exactly they’re doing and why this software is necessary to install on personal machines to attend. This lack of trust doesn’t help the situation, as less than 0.1% of students in Denmark are thought to be cheating, and only 56% of those caught cheating used the internet to do so. This most likely won’t change with a surveillance system as this minority will take the chance either way.

In the end, protecting your surveillance system from bypasses is the same cat-and-mouse dilemma seen in the anti-cheating sector, and as long as the Ministry of Education are sufficiently annoying in their public announcements, we will continue playing that game. We are quite confident that they will never be able to match our technical competences, as we are used to reguarly dissecting kernel anticheats light-years ahead of this surveillance software.

Screenshots

Screenshots are managed by the Graphics.CopyFromScreen .NET function, that is essentially a bit-block transfer wrapper, internally calling gdi32!BitBlt. As we do in video games to prevent anti-cheats from screenshotting, we can hook BitBlt and hide any unwanted information before screenshots are performed.

BOOL __stdcall ayyxam::hooks::bit_blt(HDC hdc, int x, int y, int cx, int cy, HDC hdc_src, int x1, int y1, DWORD rop)
{
	// HIDE WINDOW
	const auto window_handle = FindWindowA("Notepad", nullptr);
	ShowWindow(window_handle, SW_HIDE);
	
	// SCREENSHOT
	auto result = ayyxam::hooks::original_bit_blt(hdc, x, y, cx, cy, hdc_src, x1, y1, rop);
	
	// SHOW WINDOW
	ShowWindow(window_handle, SW_SHOW);
	
	return result;
}

Open website list

The use of AutomationElement was foreign to us. Digging a little bit, we found that the native function called is UIAutomationCore!UiaGetPropertyValue. This function is called with the id ValueValue, which supposedly returns the final value from the browser. Looking at the limited documentation on msdn, the third argument is the resulting value. To figure out the type of the resulting object, we used ReClass and quickly figured out the structure. When that is all set and done, the only thing left is to override the value or implement a filter system, hiding specific websites.

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<value_structure*>(value);

	// ZERO OUT OLD URL
	std::memset(value_object->value, 0x00, std::wcslen(value_object->value) * 2);

	// CHANGE TO GOOGLE.COM
	constexpr wchar_t spoofed_url[] = L"https://google.com";
	std::memcpy(value_object->value, spoofed_url, sizeof(spoofed_url));

	return result;
}

Network interfaces

The implementation to get all network interfaces uses the .NET function NetworkInterface.GetAllNetworkInterfaces, that internally calls iphlpapi!GetAdaptersAddresses. Hiding adapters from this function is quite trivial:

ULONG WINAPI ayyxam::hooks::get_adapters_addresses(
	ULONG family, ULONG flags, PVOID reserved, 
	PIP_ADAPTER_ADDRESSES adapter_addresses, PULONG size_pointer)
{
	// CALL ORIGINAL TO HIDE ENTRIES
	const auto result = ayyxam::hooks::original_get_adapters_addresses(family, flags, reserved, adapter_addresses, size_pointer);

	// DO NOT HANDLE ERRORS
	if (!result)
		return result;

	for (auto current_entry = adapter_addresses, previous_entry = adapter_addresses;
		current_entry != nullptr; 
		current_entry = current_entry->Next)
	{
		// FILTER BY FRIENDLY NAME
		const auto friendly_name = std::wstring(current_entry->FriendlyName);

		// ITERATE GUARDED ADAPTERS
		for (auto protected_adapter : guard::hidden_adapter)
		{
			if (protected_adapter != friendly_name)
				continue;

			// PROTECTED ADAPTER FOUND:
			// IF NOT FIRST ENTRY, SKIP!
			if (previous_entry != current_entry)
			{
				previous_entry->Next = current_entry->Next;
			}
			else
			{
				// RELOCATE ENTIRE STRUCTURE TO OVERRIDE FIRST ENTRY :)

				// CALCULATE SIZE OF FIRST ENTRY
				const auto delta = current_entry->Length;
				const auto remaining_size = *size_pointer - delta;

				// CACHE ADDRESS TO COPY FROM LATER ON
				const auto copy_next = current_entry->Next;

				// RELOCATE ALL ENTRIES IN LINKED LIST, SKIP FIRST ELEMENT
				for (auto inner_entry = current_entry->Next; inner_entry != nullptr; )
				{
					// CACHE NEXT ADDRESS FOR LATER
					const auto real_next = inner_entry->Next;

					// RELOCATE
					*reinterpret_cast<std::uint8_t**>(&inner_entry->Next) -= delta;

					// CONTINUE ITERATING
					inner_entry = real_next;
				}

				// MOVE OVER ALL OTHER ENTIRES, OVERWRITING OLD
				std::memcpy(current_entry, copy_next, remaining_size);

			}

			break;

		}
	}

	return result;
}

Running processes

The .NET process interface will internally cache process data using the ntdll!NtQuerySystemInformation syscall. Hiding processes from this syscall requires a little bit of work, as many of the information types contain process information. Luckily, .NET only fetches one specific type of information, so we do not need to go full latebros

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<SYSTEM_PROCESS_INFO*>(
			reinterpret_cast<std::uintptr_t>(entry) + entry->NextEntryOffset);
	};

	// ITERATE AND HIDE PROCESS
	auto entry = reinterpret_cast<SYSTEM_PROCESS_INFO*>(system_information);
	SYSTEM_PROCESS_INFO* previous_entry = nullptr;
	for (; entry->NextEntryOffset > 0x00; entry = get_next_entry(entry))
	{
		constexpr auto protected_id = 7488;
		if (entry->ProcessId == reinterpret_cast<HANDLE>(protected_id) && previous_entry != nullptr)
		{
			// SKIP ENTRY
			previous_entry->NextEntryOffset += entry->NextEntryOffset;
		}

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

	return value;
}

Download

The entire project is available on my Github, and can be used by injecting the x86 binary into The Digital Exam Monitor’s process.