Static reverse-engineering report

LockDown Browser's
prying eyes

A full technical breakdown of what a mandatory exam-proctoring browser, its hook DLL, and its boot-persistent kernel driver actually do on a student's machine and why software that watches this much deserves to be watched back.

github.com/ntdlll discord: intdll
Respondus LockDown Browser 2.1.5.01· Main executable, hook DLL, updater, LockDownService215.sys
Context

What LockDown Browser is

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.

That framing is what i will be putting under scrutiny today. 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. Everything below is a static account, function by function, of what that footprint actually does.

Why this matters

Scrutinize anything that watches this closely

A kernel driver doesn't fail the way an application does. A bug in a normal program crashes that program; a bug in a kernel driver can crash the machine or worse; quietly hand an attacker the same privileges the driver itself runs with. Software that asks for ring-0 access, 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.

1 server takeover by a threat actor, and millions of students have their data leaked.

It's also not optional software. A student who objects to a kernel driver monitoring their machine during an unrelated evening of homework 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.

Nothing in this analysis shows LockDown Browser selling student data to data brokers, i have stated plainly further down but the fact that a proctoring vendor's client software needs to be reverse-engineered before anyone outside the company can answer that question at all is itself the point. Companies that build this kind of footprint into mandatory software should welcome that scrutiny.

Methodology

Scope and confidence

Everything documented here comes from static reverse engineering: disassembly and decompilation with IDA Pro 9.4 and the Hex-Rays decompiler, cross-checked against Microsoft's own API documentation. No binary in this analysis was executed, no VM was booted, the kernel driver was never loaded, and no live network traffic was captured. Every finding is therefore labeled with one of two confidence levels:

Confirmed  directly established by decompiled code, cross-referenced call sites or a signature verification pass

Candidate  reasoned entirely from static code and structurally sound, but not yet triggered or observed live

All four first-party binaries carry valid signatures: LockDownBrowser.exe, LockDownBrowser.dll, and LockDownUpdater.exe under Respondus, Inc.'s DigiCert certificate, and LockDownService215.sys under Microsoft's Windows Hardware Compatibility Publisher program. This is genuine, shipping vendor code, not a modified or reconstructed binary.

The technical reversing: part one

LockDownService215.sys

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.

An eye for an eye, a tooth for a tooth

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:

MethodAddressConfidence
onConnect0x140001480Confirmed
onMessage0x140001560High-confidence inference
onDisconnect0x140001510Confirmed

Client authorization: signature-based, not binary-specific

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.

ImageMonitor setup, registering PsSetLoadImageNotifyRoutine
ImageMonitor setup, registering PsSetLoadImageNotifyRoutinesub_140005814

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.

ImageMonitor::imageCallback, authorization check against the loading image
ImageMonitor::imageCallback, authorization check against the loading imagesub_140005798 dispatch

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.

Candidate: unchecked read past a 4-byte buffer

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.

onMessage, opcode 3 dispatch, pool allocation and re-entrant dispatch
onMessage, opcode 3 dispatch, pool allocation and re-entrant dispatchsub_140001560

Two concrete failure modes follow directly. If the out-of-bounds "length" happens to be very large or point at unmapped 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 refined the trigger further: the opcode-3 branch is gated behind an internal flag that's only set by a prior, successful opcode-2 message. The real trigger is a two-message sequence, not a single request. The underlying unchecked read and oversized allocation are otherwise exactly as described. Candidate, reasoned entirely from decompiled code; the driver was never loaded and no message was ever sent to it.

Candidate: key material torn down with no lock

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 well-documented, recurring bug class in Windows minifilter drivers.

onDisconnect, tearing down the port and its crypto key material
onDisconnect, tearing down the port and its crypto key materialsub_140001510

What's confirmed: the disconnect path destroys key handles unconditionally, with no visible synchronization anywhere nearby. What isn't yet confirmed: whether a message mid-flight necessarily touches the exact struct fields disconnect just zeroed. Candidate race condition, CWE-416, gated behind the same SYSTEM/Administrator precondition as everything else on this port.

What it monitors, and what it doesn't

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. Attaching to every volume without any operation callbacks is a legitimate, low-cost way to guarantee the driver is present system-wide; it isn't evidence of file-content monitoring.

What it does monitor is broader than "the exam browser." At boot, it unconditionally registers PsSetCreateProcessNotifyRoutineEx, PsSetCreateThreadNotifyRoutine, and PsSetLoadImageNotifyRoutine: kernel callbacks that fire for every process, thread, and module load on the entire machine, all the time, 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 (pulled by a hand-rolled kernel-mode parser of the target's VS_VERSION_INFO resource), plus 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, not the running process's own user token.

OwnerSIDWrapper, resolving the on-disk file owner SID
OwnerSIDWrapper, resolving the on-disk file owner SIDRtlGetOwnerSecurityDescriptor
CompanyName field extraction
CompanyName extractionsub_140006204
FileVersion field extraction
FileVersion extractionsub_14000623C

A few capabilities round out the picture without adding to the concerns above: 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, not a deliberate anti-tamper "crash the machine" design, despite how it might read out of context.

ThreadMonitor::detectFilelessExecution, ZwQueryVirtualMemory against MemorySectionName
ThreadMonitor::detectFilelessExecution, ZwQueryVirtualMemory against MemorySectionNamesub_1400077B8 region
The technical reversing: part two

The browser and its hook DLL

Global hooks map the DLL into every window on the desktop

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 zero, 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 have nothing to do with the exam, as an intrinsic side effect of the hook type chosen rather than an attack, but 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.

Keyboard hook filter, blocking VK_LWIN, VK_RWIN and F12
Keyboard hook filter, blocking VK_LWIN, VK_RWIN and F12fn @ 0x1800018E0

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. Confirmed, reproduced identically across two independent decompilations, a robustness bug, not a security vulnerability.

Whole-machine enumeration in the main process

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.

Rounding out the surface: 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.

Credentials pass through this process

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. Worth independent verification is whether the described encryption is applied consistently, and whether plaintext copies are cleared from process memory promptly rather than retained.

Anti-tamper: a hardware-breakpoint engine wrapped around iLok licensing

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.

PE section table, the .mfrt segment against .text
PE section table, the .mfrt segment against .textIDA segment view

Cross-reference queries against every one of those ~50 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's the concrete signature of PACE's control-flow virtualization: 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. Fully tracing it would need a PACE-specific bytecode disassembler or live tracing of the exception port, outside the scope of a static-only pass, and reported here as an honest limit rather than papered over.

Cross-reference query on a hwbp:* name-table entry, one data reference, zero code references
Cross-reference query on a hwbp:* name-table entry, one data reference, zero code referencesxrefs to aSetupHwbpGatew

The updater: a clean bill of health

LockDownUpdater.exe is the one first-party binary with no findings across any category: a conventional WININET-based downloader with bundled zlib/minizip for archive extraction, standard file staging, and no kernel or credential interaction beyond checking for and launching an update.

What actually goes out over the wire

The telemetry format, byte for byte

Extracted directly from the main executable's format-string table, this is the query string appended to session-report requests:

Session report format string in .rdata
.rdata, session report format stringaTokenSCoursere_1

The same string table shows a related field sitting just below it: a separate &loginId=%s string, 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=. Three things stand out:

Consolidated findings

Bugs found

BugComponentConfidenceType
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, and none were found in the fileless-execution detector despite dense branching: its one resource lock is acquired once and released through every exit path, including all three error branches.

Aggregating across every component

Judgement

01 / Telemetry

Extensive, largely unscoped telemetry

Substantiated

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.

LockDownService215.sysRegisters system-wide, boot-persistent process/thread/image monitoring independent of exam state.
LockDownBrowser.exeSends PII and a MAC address as URL query parameters on every session-report call.
02 / Technique

Malware- and rootkit-like technique

Substantiated as technique

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. Together, this is a real, technique-level match to malware and rootkit patterns, now backed by concrete candidate bugs rather than pattern-matching alone.

What distinguishes it from actual 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 rather than being unique to this software. Technique overlap with malicious tooling is a real engineering-scrutiny concern; it is not, on its own, evidence of malicious intent.

LockDownService215.sysSHA-1/256 hashing of every loaded image, a signature-based auth gap, and two candidate memory-safety bugs.
LockDownBrowser.exeFull T1055-pattern process-injection API surface present in the import table; no exploit of it confirmed in this component.
LockDownBrowser.dllGlobal WH_SHELL/WH_MOUSE hooks force the DLL into every windowed process on the desktop.
03 / Privacy

Privacy concerns

Substantiated

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.

LockDownService215.sysPer-process metadata and file-owner identity extracted for every process on the machine, not just the exam browser.
LockDownBrowser.dllThe low-level keyboard hook observes every keystroke system-wide while installed, not only keystrokes sent to the exam window.
LockDownBrowser.exeEvent log, registry, and clipboard access with no technical boundary at "exam-relevant."
04 / Data brokers

Selling student data to data brokers

Not established

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 by function; each is a conventional vendor for analytics, editing, or text-to-speech.

Closing note

Watch the things that watch you

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, and much of what's documented here reads as exactly that: competent, if aggressive, protection tooling. But competence isn't the same question as proportionality, and 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 its users more transparency about scope than a plain "lock down the browser" pitch provides.

The candidate bugs documented here (the unchecked kernel read and the unsynchronized key teardown) are exactly that: candidates. Converting them into confirmed findings needs a live harness sending crafted messages to the driver's port inside an isolated VM, work explicitly deferred in this static-only pass. 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.