DEV Community

Yuvraj Raghuvanshi
Yuvraj Raghuvanshi

Posted on • Originally published at yuvrajraghuvanshis.Medium on

Reverse Engineering SmartLock by Parivahan: What I Found Inside a Python Proctoring App

I didn’t plan to reverse engineer a proctoring application. I just wanted to understand why a page kept refreshing in an infinite loop.

That one puzzling symptom ended up pulling me down a rabbit hole that took days to climb out of — involving PyInstaller internals, broken decompilers, browser automation quirks, and a race condition that convincingly pretended to be tamper detection. The journey was longer than I expected, and honestly more interesting. So I figured I might as well write it up.

The application in question is SmartLock by Parivahan , a proctoring system used for government driver’s learning license exams in India, and yes, I am getting a driving license at the age of 25. We all start somewhere.

It’s a Python desktop app that locks down your machine, watches your screen, monitors USB ports, controls your browser, and talks to a remote server — all at the same time. Understanding how it does all of this, and how the pieces fit together, is what this article is about.

Phase 1: Opening the Box

The first thing I did was look at the installation directory. This usually tells you a lot before you write a single line of code.

_internal/
browser/
config/
log/
Pictures/
Smartlock.exe
Enter fullscreen mode Exit fullscreen mode

That _internal/ folder was the giveaway. It's a classic PyInstaller signature. The folder contains Python libraries and compiled bytecode - essentially, a self-contained Python runtime bundled into a single executable.

Screenshot: Installation directory structure showing _internal/ folder and Smartlock.exe
Screenshot: Installation directory structure showing _internal/ folder and Smartlock.exe

So the application was built in Python and packaged using PyInstaller. That meant extraction was possible using pyinstxtractor:

python pyinstxtractor.py Smartlock.exe
Enter fullscreen mode Exit fullscreen mode

After extraction, the structure looked like this:

Smartlock.exe_extracted/
├── PYZ-00.pyz_extracted/
│ ├── asyncio/
│ ├── psutil/
│ ├── pydivert/
│ ├── selenium/
│ ├── websockets/
│ ├── win32com/
│ ├── yaml/
│ ├── controller.pyc
│ ├── registry_edit.pyc
│ └── ...
├── core.pyc
└── ...
Enter fullscreen mode Exit fullscreen mode

Most of what you see here is noise — third-party libraries. The signal is in the handful of .pyc files: core.pyc, controller.pyc, registry_edit.pyc. These contain the actual application logic. Everything else is plumbing.

Phase 2: Decompilation and Why It’s Always Messier Than It Sounds

This is where things got annoying.

I tried the standard tools first:

uncompyle6 core.pyc
decompyle3 core.pyc
Enter fullscreen mode Exit fullscreen mode

Both failed. Version mismatch — the bytecode was compiled with a Python version these tools didn’t fully support. I eventually got somewhere using pychaos, but I want to be honest about what “decompiled code” actually looks like in practice. It’s not clean. Comments are gone (they’re never stored in bytecode). Control flow gets reconstructed heuristically and is often wrong. You get artifacts like this:

while __CHAOS_PY_TEST_NOT_INIT_ERR__ :
    message = yield None
Enter fullscreen mode Exit fullscreen mode

When the actual code probably looked something like:

async for message in websocket:
    msg = json.loads(message)
Enter fullscreen mode Exit fullscreen mode

The decompiler is doing its best, but it’s guessing. Reverse engineering at this level is less about reading code and more about reconstructing intent from imperfect evidence. You develop a feel for what the code is trying to do, even when the syntax is broken.

The three key files broke down roughly like this:

core.pyc Main orchestrator - startup, thread management

controller.pyc Enforcement logic - monitoring, detection

registry_edit.pyc OS-level restrictions - registry modifications

Phase 3: Reconstructing the Architecture

Once I had a working (if imperfect) picture of the code, the overall architecture became clear:


Screenshot: detailed diagram showcasing the connections between SmartLock application, bundled Chrome application, and remote server

It’s a tightly coupled system. The desktop app and the browser aren’t independent — they’re in constant communication. And both of them are talking to the remote server. Remove any one of these connections and the whole thing breaks.

Phase 4: The Browser That Kept Redirecting

After reconstructing and running the application, I ran into something strange.

The exam webpage was redirecting continuously to a 403.jsp webpage and in a split second back to exam login webpage. Every few seconds — reload, reload, reload. My first instinct was that this was intentional tamper detection. After all, the whole point of a proctoring system is to detect when something isn’t right. Maybe it had detected something about my environment and was punishing me with an infinite loop.

That turned out to be wrong. But figuring out why it was wrong took a while.

The browser bundled with SmartLock isn’t a standard Chrome installation. It’s a portable Chromium build with a preconfigured user profile:

browser/
├── App/
│ └── Chrome-bin/
├── Data/
│ └── profile/
│ └── Default/
Enter fullscreen mode Exit fullscreen mode

I inspected the stored cookies, session tokens, cached scripts, and extensions looking for some kind of tamper detection artifact. Nothing useful. The refreshing wasn’t coming from stored state.


Screenshot: browser/ directory structure

Phase 5: SmartSocket.js — Where It All Connected

The actual cause was in the exam webpage itself. Buried in the page source was this:

<script src="SmartLock/SmartSocket.js"></script>
Enter fullscreen mode Exit fullscreen mode

This script establishes a WebSocket connection to the local application:

socket = new WebSocket('ws://localhost:8000/');
Enter fullscreen mode Exit fullscreen mode

This is the critical link. The browser doesn’t just display the exam — it actively depends on the local application being alive and reachable. As soon as the connection is established, the browser authenticates:

reqOb.type = "Authentication";
reqOb.token = '1234';
reqOb.userid = appl_no;
socket.send(JSON.stringify(reqOb));
Enter fullscreen mode Exit fullscreen mode

And if the connection fails — even momentarily — the page clears the session and reloads:

// On connection failure:
// → Clear session
// → Redirect or reload
// → UI resets to initial state
Enter fullscreen mode Exit fullscreen mode

This is not tamper detection. It’s a strict runtime dependency. The browser requires the local WebSocket server to be up before it finishes loading. If it isn’t, you get an infinite reload loop.

Screenshot: SmartSocket.js connection code in browser devtools
Screenshot: SmartSocket.js connection code in browser devtools

Phase 6: The Race Condition

With that understanding, the root cause became obvious. The application starts the WebSocket server and the browser in parallel:

thread1 = start_websocket()
thread2 = launch_browser()
Enter fullscreen mode Exit fullscreen mode

The problem with this is that “starting” the WebSocket server takes a moment. The browser, however, is fast — it loads the page, runs the script, and tries to connect to localhost:8000 before the server is actually ready. Connection fails. Page reloads. Tries again. Same thing. Infinite loop.

The sequence of events looked like this:

Browser loads page
 → SmartSocket.js executes immediately
 → Attempts WebSocket connection to localhost:8000
 → Server not ready yet
 → Connection refused
 → Page session cleared
 → Page reloads
 → Same thing happens again
 → ...forever
Enter fullscreen mode Exit fullscreen mode

It perfectly mimicked tamper detection behavior, which is why I assumed that’s what it was. But it was just a timing issue.

The fix is simple: wait for the server to be ready before launching the browser.

def browser_start(self):
    try:
        import socket as sock
        import time

        for _ in range(50):
            try:
                s = sock.create_connection(("localhost", 8000), timeout=0.1)
                s.close()
                break
            except OSError:
                time.sleep(0.1)

        # Now launch browser
Enter fullscreen mode Exit fullscreen mode

With this in place, the correct sequence is:

Start App
 → Start WebSocket server
 → Poll until server is accepting connections
 → Launch browser
 → WebSocket connects successfully
 → Authentication succeeds
 → Exam proceeds normally
Enter fullscreen mode Exit fullscreen mode


Screenshot: Before — infinite reload loop vs successful startup

Phase 7: What the App Is Actually Doing Under the Hood

Once the startup problem was solved, I could look more carefully at all the enforcement mechanisms running in the background. There’s quite a lot.

OS-Level Lockdown

The registry editor modifies Windows to disable the usual escape routes:

DisableTaskMgr = 1
DisableLockWorkstation = 1
NoLogoff = 1
Enter fullscreen mode Exit fullscreen mode

This disables Task Manager, the lock screen, and the ability to log off. The Ctrl+Alt+Del menu effectively becomes useless.

Process Monitoring

The monitoring engine maintains a list of software that shouldn’t be running during an exam. Screen recording and virtual camera tools are specifically targeted:

  • OBS Studio
  • ManyCam
  • XSplit

If any of these processes are detected, a violation is flagged.

USB Monitoring

The app takes a snapshot of connected USB devices at startup and watches for changes:

if current_usb != initial_usb:
    flag_violation()
Enter fullscreen mode Exit fullscreen mode

Plugging in a USB drive during the exam is treated as a potential integrity violation.

Multi-Monitor and VM Detection

Multiple displays are blocked. The app also checks whether it’s running inside a virtual machine — which would make it easier to manipulate the environment without being detected.

Network Filtering via pydivert

This was the most interesting piece. The app uses pydivert - a Python wrapper around WinDivert - to implement packet-level network filtering. During an exam, only certain destinations are allowed. Everything else is dropped at the kernel level.

Phase 8: Two WebSockets, Not One

I initially assumed there was a single WebSocket connection: browser to local app. There are actually two:

Local WebSocket (localhost:8000) Browser ↔ Desktop App

Remote WebSocket Desktop App ↔ Remote Server

The local one handles session management for the browser. The remote one is for continuous telemetry — the app regularly sends status updates to the server:

{ "type": "USB", "status": true }
{ "type": "ProcessCheck", "detected": false }
Enter fullscreen mode Exit fullscreen mode

The server isn’t passive. It’s continuously validating that the client is behaving correctly. If the telemetry stops or reports a violation, the server can terminate the session.

Configuration

All the connection details live in a YAML config file:

ExamIp: 164.100.69.5
ExamUrl: https://sarathi.parivahan.gov.in/sarathiservice/authenticationaction.do?authtype=Anugyna
SocketPort: 8000
SocketServerPort: 3000
SocketUrl: ws://sarathi.parivahan.gov.in
StatusApiUrl: https://sarathi.parivahan.gov.in/sarathiWS/rsServices/smartLockCheck/smartLockCheck
ViolationApiUrl: https://sarathicov.nic.in:8443/sarathiWS/rsServices/smartLockCheck/examViolation
primaryServerIPV4: 10.172.31.33
primaryServerIPV6: 2001:4408:7204:8:5d93:8239:8876:d238
secondaryServerIPV4: 10.172.31.30
secondaryServerIPV6: 2001:4408:7204:9::aac:2033
Enter fullscreen mode Exit fullscreen mode

This means the behavior is somewhat server-controlled. The exam URL, the socket address, the session logic — it’s all configured externally, which makes the server the real authority over how the session runs.

Phase 9: The Firewall

One more thing worth mentioning: the app appears to interact with the Windows Firewall directly.

fw.pbox_fw_backup("pbox_bkp.wfw")
Enter fullscreen mode Exit fullscreen mode

This exports the current firewall rules to a backup file before modifying them. The behavior is that the app replaces your firewall rules with its own restricted ruleset for the duration of the exam, then restores the backup afterward. It may also hash the backup to detect if someone has tampered with the rules mid-session.

Phase 10: Why You Can’t Just Rebuild It

One last I want to address directly: extracting the PyInstaller binary does not give you a working copy of the application. There’s a common misconception that extraction = reconstruction. It doesn’t.

The workflow for actually rebuilding would be:

  1. Decompile .pyc files to .py
  2. Manually correct the decompilation errors
  3. Rebuild with PyInstaller

Steps 1 and 2 are where it falls apart in practice. The decompiled code has inaccuracies that aren’t always obvious. Some of the control flow is wrong in subtle ways that only become apparent at runtime. There are also timing dependencies baked into the threading model, and the server-side validation means you’d need a cooperating server to test anything properly.

The security here doesn’t come from any single mechanism being unbreakable. It comes from the combination:

  • The local app monitors the OS
  • The browser depends on the local app
  • The server monitors the local app
  • The network is filtered at the kernel level
  • The firewall is replaced during the session

Each layer on its own is probably defeatable. Together, they create a system where defeating one layer doesn’t help much because the others remain intact.

What I Took Away From This

A few things stuck with me after this investigation.

The infinite reload loop was genuinely convincing as tamper detection. I spent more time than I’d like to admit looking for a security mechanism that wasn’t there. The lesson is that emergent behavior from a race condition can look exactly like intentional defensive behavior. Don’t assume intent before you’ve traced the actual execution path.

The browser is doing real security work here, not just displaying a UI. SmartSocket.js is the link between the exam session and the local enforcement system. If that connection breaks, the exam can't proceed. That's a deliberate architectural choice, not an accident.

And PyInstaller extraction, while possible, is just the beginning. The hard part isn’t getting the bytecode out. It’s making sense of what the decompiler gives you and reconstructing what the developer originally meant to write.

What Could Come Next

If I were going to continue this investigation, the natural directions would be:

  • Mapping the full WebSocket protocol between browser and app, and between app and server
  • Tracing the telemetry payloads to understand exactly what data gets sent and when
  • Building a sequence diagram for the full session lifecycle, from startup through exam completion
  • Looking more carefully at the firewall manipulation and how (or whether) it detects tampering with the backed-up rules

None of that fits in one article. But the architecture is now clear enough that any of those threads could be pulled independently.

Screenshot: Final — application running normally with exam loaded, WebSocket connection established
Screenshot: Final — application running normally with exam loaded, WebSocket connection established

This article is for educational purposes — understanding how production security systems are architected and why they’re difficult to tamper with. The focus throughout has been on the design and behavior of the system, not on defeating it.

This article is rewritten using AI chatbots.

April 07, 2026

Top comments (0)