A technical report of what a mandatory exam-proctoring browser, its hook DLL, and its boot-persistent kernel driver actually do on a student's machine.
LockDown Browser is exam-proctoring software published by Respondus, Inc. Universities and colleges require students to install it before sitting an online exam; once launched, it takes over the screen, blocks other applications, and disables the usual ways of leaving the exam window. For most students, this step is mandatory.
What i will be putting under scrutiny today is whether or not that process is within legal bounds of European students' digital privacy rights. Their marketing is "merely a browser that locks down" but what actually ships in the install is considerably more than a browser: a hooking DLL that installs itself into every window-owning process on the desktop, and a kernel-mode driver that loads at boot, attaches to every disk volume, and watches every process, thread, and module load on the machine independent of whether an exam is even running.
A kernel driver doesn't fail the way an application does. A bug in a normal program crashes that program only but a bug in a kernel driver can crash the machine or even worse quietly hand an attacker the same privileges the driver itself runs with. Software that runs in/with ring-0, global input hooks and boot persistence is asking for a different, much higher standard of engineering scrutiny than a browser extension, and it should be held to that standard whether or not anything has gone wrong yet.
It's also not optional software. A student who objects to a kernel driver monitoring their machine during an unrelated evening doesn't have the option to simply uninstall it. The exam requires it, meaning, said student would have to manually disable the driver when not at school or not partaking in an exam every single time. Reading the privacy policy won't cut it here, the vendor needs to be transparent about what it collects if it forces a kernel driver down your throat.
For all intents and purposes, i am not claiming that whoever is affiliated with or in charge of Lockdown Browser is selling student data. Nothing in this analysis shows LockDown Browser selling student data to data brokers, i have stated that plainly further down but the fact that a proctoring vendor's client software needs to be reverse-engineered before anyone outside the company can even come to that conlusion at all is itself the point. Companies that build this kind of footprint into mandatory software should be transparent about the telemetry they collect.
The kernel minifilter driver. Loaded at boot (SERVICE_SYSTEM_START), attached to every mounted volume, and running independent of whether LockDown Browser or an exam session is active.
The driver opens a single communication port (\ApDriverPort) via FltCreateCommunicationPort, with MaxConnections hard-capped at 1, only one client process can hold the port at any moment. The security descriptor is built with FltBuildDefaultSecurityDescriptor, which per Microsoft's own documentation, independent of the specific access mask passed, restricts the object to processes already running as SYSTEM or Administrator. Every finding below on this driver is an admin-to-kernel attack surface.
Three callbacks sit behind the port, reached through a vtable dispatch at offsets +16 and +32:
| Method | Address |
|---|---|
onConnect | 0x140001480 |
onMessage | 0x140001560 |
onDisconnect | 0x140001510 |
onConnect checks the connecting process's PID against a single stored "authorized PID" slot. That slot is populated by ImageMonitor::imageCallback, registered via PsSetLoadImageNotifyRoutine which fires on every image load system-wide, not just new process creation, including a plain LoadLibrary call inside any already-running process.
When the slot is empty, the loading image is run through an Authenticode verification pass: CodeIntegrity::validateSignature (0x1400032CC) opens the file, walks its signature chain, and checks two literal strings: the signer's Common Name equals "Respondus, Inc.", and the issuing intermediate CA equals "DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1". Only if both match does the loading process's PID get recorded as authorized.
That blocks the naive attack of renaming malware to LockDownBrowser.exe. But the check verifies only that an image is validly signed by Respondus under that certificate chain, not which Respondus binary it is, and it isn't scoped to new-process creation. Respondus ships at least three independently-obtainable binaries under the same signing chain: the main executable, the updater, and the hook DLL itself. Because the authorized-PID slot resets to empty the moment its holder exits (confirmed in ProcessMonitor::processCallback's exit branch), it should be feasible in principle for any local process to win the authorization race by loading any legitimately-obtained Respondus-signed module before or after the real browser claims or releases the slot.
Confirmed narrowing gap in the trust model, but not necessarily a memory-safety bug, and still gated behind the admin/SYSTEM precondition above.
The message handler receives whatever a connected client sends via FltSendMessage: an input buffer, its claimed length, and an output buffer. The only upfront validation is a check that the input is at least 4 bytes, after which the first 4 bytes are read as an opcode. Several opcode branches then read further into the buffer without ever re-checking that the claimed length covers those offsets.
The clearest case is opcode 3. It reads a 4-byte value at byte offset 260 of the input buffer with no length check beyond the initial 4-byte minimum, adds 264 to it, allocates a new kernel pool buffer of that size, and memmoves that many bytes starting from the beginning of the caller's original (possibly 4-byte) buffer into the new allocation.
Two concrete failure modes follow directly. If the out-of-bounds "length" happens to be very large or point at garbage memory, the allocation or the copy can fault which causes a locally triggerable kernel crash. And because the newly allocated buffer is threaded straight back through the message-response path, whatever kernel memory happened to sit adjacent to the original tiny buffer can end up copied into data eventually returned to the calling user-mode process, a kernel-to-userspace information disclosure primitive (CWE-125 combined with CWE-200).
A follow-up decompilation pass confirms my finding: the opcode-3 branch is gated behind an internal flag that's only set by a prior, successful opcode-2 message. What actually makes it fire is a two-message sequence.
onDisconnect unconditionally destroys the encrypted channel's cryptographic key handles (BCryptDestroyKey on three keys, BCryptCloseAlgorithmProvider on two algorithm providers) with no lock or interlock of any kind visible in the disassembly. onMessage's entry sequence has no locking either. Because MaxConnections: 1 means exactly one live port instance exists, all three callbacks operate on the same shared object, and FLT Manager itself does not guarantee mutual exclusion between the message and disconnect notifications for a given port. So, serializing them is the minifilter author's own responsibility. This is a bug that is well-known and has been regurgitated again and again in Windows minifilter drivers.
What's confirmed is that the the disconnect path destroys key handles unconditionally. What i haven't confirmed yet is whether a message mid-flight necessarily touches the exact struct fields disconnect but just zeroed.
One clarification in the driver's favor first: its FLT_REGISTRATION struct sets OperationRegistration and ContextRegistration to null, and zeroes all nine remaining file-system callback slots. Despite being architecturally a minifilter attached to every volume, it registers zero file-system I/O interception: it never reads, inspects, or modifies file content. This is also why i think it attaches to every volume without any operation callbacks. It's (kind of) legit and a low-cost guarantee that the driver is present systemwide.
What it does monitor is broader than "the exam browser." At boot, it unconditionally registers PsSetCreateProcessNotifyRoutineEx, PsSetCreateThreadNotifyRoutine, and PsSetLoadImageNotifyRoutine which are kernel callbacks that fire for every process, thread, and module load on the entire machine, all the time and NOT scoped to an active exam session. It can also attach into and read the memory of any arbitrary process by PID, confirmed via the PsLookupProcessByProcessId → KeStackAttachProcess → (inspect) → KeUnstackDetachProcess pattern.
For every process it observes, it extracts and can report: the image path, CompanyName, FileVersion, and OriginalFilename + the on-disk file owner account of the executable, resolved through ZwQuerySecurityObject and RtlGetOwnerSecurityDescriptor. That last field is the file's NTFS owner, typically an installer or admin account.
A few capabilities i want to point out that i find interesting: a dedicated fileless-execution detector (ThreadMonitor::detectFilelessExecution), SHA-1/SHA-256 hashing of loaded images as part of the signature pipeline, and a second, separately-tracked PID reserved specifically for dwm.exe, plausibly related to the screen-capture-exclusion feature needing the Desktop Window Manager's cooperation. The driver also imports KeBugCheckEx with code 0x139 which is simply the standard MSVC /GS stack-cookie violation handler present in virtually every compiled kernel driver and isn't necessarily a deliberate anti-tampering technique.
LockDownBrowser.dll installs three global Windows hooks, all with dwThreadId=0: WH_KEYBOARD_LL (13), WH_SHELL (10), and WH_MOUSE (7). The low-level keyboard hook is exempt from Windows' classic hook-injection behavior, but WH_SHELL and WH_MOUSE are not. Installed with a thread ID of 0, Windows has to map this DLL into the address space of every process on the interactive desktop that owns a message queue, meaning every open application, chat client, and unrelated browser, for the hook to function. That's DLL injection into processes that could quite literally nothing to do with exam integrity. Imo, this a materially invasive technique few students would expect from an exam browser.
The keyboard hook's actual filtering logic was fully decompiled: it unconditionally blocks both Windows keys (VK_LWIN, VK_RWIN) and F12, and conditionally blocks Alt+Tab and Alt+Escape combinations; everything else passes through untouched. On uninstall, if a flag is set, the DLL calls SendInput to synthesize a key-up event for the Windows key, confirmed to be a legitimate fix, preventing the OS from perceiving the key as stuck down after the intercepting hook is removed, not a hidden input-injection feature.
One resource-leak bug turned up here: a second, unconditional WH_MOUSE hook installation overwrites the handle from an earlier, flag-conditional one without ever unhooking it.
The main executable imports the full Win32 toolkit for surveying everything running on the machine: CreateToolhelp32Snapshot, K32EnumProcesses, EnumWindows, SetWinEventHook, and EnumServicesStatusExA, matched against a configurable <program_blacklist>: confirmed named detections include AutoHotkeyGUI, Loom Screen Recorder, and SCRE.IO Screen Recorder. None of these Win32 APIs can be scoped to "processes relevant to the exam": every personal messaging app, unrelated browser tab, and unrelated piece of software a student runs is visible to (and potentially logged by) this enumeration.
Alongside that sits a process-injection-capable API set: OpenProcess, CreateRemoteThread, GetThreadContext/SetThreadContext, SuspendThread/ResumeThread, paired with OpenProcessToken/AdjustTokenPrivileges, the same combination of imports used by process injectors and RAT loaders (MITRE ATT&CK T1055). No client-side bug was found that actually exploits this combination in this component; it's documented here as a technique match: the capability to write into and redirect execution in unrelated processes exists in the import table, well beyond what monitoring a browser window requires.
Full clipboard access (OpenClipboard/SetClipboardData/EnumClipboardFormats), Windows Event Log reads (OpenEventLogW/ReadEventLogW, a whole-system log covering login history and other applications' errors), a broad registry surface (open/query/set/create/delete across ADVAPI32), and SetWindowDisplayAffinity, the WDA_EXCLUDEFROMCAPTURE mechanism, which actively prevents the student, or an auditor, from screen-recording their own exam session for their own records.
String evidence confirms LMS credential handling inside the exam session: "SDK AES256 encrpyted password", "Blackboard password mismatch", and a literal DOM injection that writes a password value directly into a form field.
The main binary carries a roughly 70 MB .mfrt segment (over three times the size of its own 11 MB .text section), consistent with PACE Anti-Piracy's protection runtime wrapping the product's iLok licensing code. A live decompilation pass enumerated the engine's full internal name table (roughly 50 entries) and found a coherent pipeline: debug-register setup (DR0–DR3), a debug-object-style exception port, hardware-breakpoint registration and takeover protocols, RDTSC-based timing checks for debugger-induced slowdown, several arithmetic/control-flow obfuscation primitives, and a deliberate state-corruption response, destroy_state:clobber, to detected tampering, with an "undo" path for the legitimate case.
Cross-reference queries against every one of those 50 something name-table entries return zero code references from anywhere the decompiler can see: only the data-to-data link from the table slot to its own string. That makes me think of PACE's control-flow virtualization because the code that actually walks this table isn't native x86 a disassembler can read and is almost certainly interpreted bytecode running inside the .mfrt segment's own virtual machine.
LockDownUpdater.exe is the one binary with no findings across any category.
Extracted directly from the main executable's format-string table, this is the query string appended to session-report requests:
The same string table shows a related field sitting just below it which is a separate &loginId=%s string. This is referenced from a different call site in the same function. Every field above is confirmed present, alongside numerous %s/MONServer/ldb/... and %s/MONServer/livechat/... endpoint templates that append token=, key=, studentKey=, time=, and mac=. There are 2 things that had me scratching my head and pondering:
1 server takeover by a threat actor, and millions of students have their data leaked.
| Bug | Component | Confidence | Type |
|---|---|---|---|
| Out-of-bounds kernel read / DoS, opcode 3, gated behind a prior opcode-2 message | LockDownService215.sys, onMessage |
Candidate | CWE-125 / CWE-200 |
| Unsynchronized crypto-key teardown racing concurrent message handling | LockDownService215.sys, onDisconnect |
Candidate | CWE-416 |
| Signature-based (not binary-specific) port authorization: any Respondus-signed image can claim the port | LockDownService215.sys, onConnect |
Confirmed narrowing gap | Trust-boundary weakness |
HHOOK handle leak: second unconditional hook call overwrites an unhooked handle |
LockDownBrowser.dll, CLDBDoSomeStuff |
Confirmed | Resource leak |
Both driver-side candidates require the connecting process to already run as SYSTEM or Administrator, reaching the communication port at all is gated on that precondition. No bugs were found in LockDownUpdater.exe.
The strongest evidence in the entire install sits in the kernel driver: process, thread, and image-load notification callbacks are registered unconditionally at boot, system-wide, for every process on the machine, not scoped to LockDown Browser or an active exam session in any way. The main executable adds a second layer, transmitting a fixed set of PII fields and a persistent MAC address in plain GET query strings on every session, as detailed above.
Kernel-mode hashing of every loaded image, a communication-port authorization mechanism with a confirmed narrowing gap, global input hooks that inject a DLL into unrelated processes as a side effect, a process-injection-capable API set, and two candidate memory-safety bugs in the driver's message dispatcher. Combining all of this i think it's safe to say that this software quite literally imitates a rootkit.
Obviously it's not real malware: full Authenticode/WHCP signing throughout, a properly access-controlled kernel communication port restricted to SYSTEM/Administrator and techniques that mirror legitimate EDR, DRM, and anti-cheat products. Technique overlap with malicious tooling is not on its own evidence of malicious intent.
WH_SHELL/WH_MOUSE hooks force the DLL into every windowed process on the desktop.This category has the most granular evidence of any of the four: a precise, decompiled accounting of exactly which fields get collected and where they go. For every process on the machine, the kernel driver extracts image path, CompanyName, FileVersion, OriginalFilename, and the on-disk file owner account. The main process reads the whole-system Windows Event Log, holds a broad registry surface, can enumerate clipboard contents, and actively defeats the student's own ability to screen-record their session via WDA_EXCLUDEFROMCAPTURE.
No string, endpoint, or code path referencing a data-broker relationship was found anywhere in the main executable's full string table, and no code path in the driver constructs or transmits data toward any destination other than its own user-mode client through the local communication port. This is also the one claim static analysis of the client genuinely cannot resolve either way: what a vendor's backend does with data after it leaves the client is a matter of server-side and contractual behavior, entirely outside what a disassembler can show. Stated plainly: this analysis found no evidence of it, and it makes no claim beyond what it found.
Third-party subprocessors (Google Analytics, TinyMCE, ReadSpeaker) do sit in the data path (see the telemetry section above) but none of them are data brokers. Each is a vendor for analytics, editing, or text-to-speech.
This is not a compliance determination. Establishing an actual violation requires the vendor's data-processing agreements, retention policies, and institutional contracts, none of which are visible from this/a binary. The following is a documentation of the technical capability and the default data flow.
| Observed capability | Relevant framework(s) | Note |
|---|---|---|
| PII + persistent MAC address in GET query strings | GDPR Art. 5 (data minimization), Art. 32 (security of processing) · FERPA (safeguarding education records) | Query-string transmission is a logging/exposure risk independent of TLS |
| Whole-machine process, window, event-log, and service visibility | GDPR Art. 5(1)(c) data minimization · FERPA scope of "school official" access | Technical capability exceeds a browser-lockdown purpose |
| Cross-process DLL injection via global hooks | GDPR Art. 25 (data protection by design) | Affects processes with no connection to the exam or the institution |
| Boot-persistent, whole-volume, whole-process-tree kernel monitoring | GDPR Art. 5(1)(b) purpose limitation · PIPEDA collection-limitation principle | Materially broader footprint than "browser lockdown" |
| Third-party subprocessors in the data path | GDPR Art. 28 (processor agreements) · FERPA redisclosure rules | Should already be documented in the vendor's own privacy notice |
None of this requires assuming bad faith. Signature-based authorization, hardware-backed anti-tamper engines, and system-wide process monitoring all have legitimate uses in anti-cheat and DRM engineering. But we may never conflate competence with proportionality. A piece of software a student is required to install that loads a kernel driver at boot and keeps watching long after the exam ends owes students more transparency about its scope than simply "a locked down browser".
The bugs documented here (the unchecked kernel read and the unsynchronized key teardown) are mere candidates. Converting them into confirmed findings would require me sending crafted messages through a live harness to the driver's port inside an isolated VM. I explicitly deferred this type of work and focused only on static analysis. Everything else (the telemetry format, the hook scope, the authorization gap, the four category verdicts) is as settled as decompiled code and Microsoft's own documentation can make it.