View on GitHub

Running DOOM and a Minecraft server on a Hikvision Intercom

Chip-off root access on a Hikvision DS-KH6320-WTE1 indoor station, then porting DOOM and a Minecraft server to its touchscreen.

It runs DOOM. It also runs Minecraft.

DOOM running on the DS-KH6320-WTE1 indoor station's touchscreen, full device in frame

The Hikvision DS-KH6320-WTE1 is a wall-mounted video intercom “indoor station” — the panel inside the front door that shows you who’s outside, lets you buzz them in, and otherwise sits there running a locked-down vendor GUI on a Linux system nobody’s supposed to touch. It has a 1024×600 resistive-adjacent touchscreen, an ARM SoC, and — it turns out — just enough headroom to run DOOM and a Minecraft server, once you get root.

This is the write-up of doing exactly that: chip-off flash extraction, turning the vendor’s restricted debug console into a shell, and then finding where the framebuffer lives, to make a resistive touch panel behave like a D-pad, and squeezing a Java-protocol Minecraft server onto this hardware.

Rockchip RV1108SoC, single Cortex-A7 @ 1 GHz
128 MBRAM, no swap
1024×600Touch panel, single-touch
32 MBSPI NOR flash, chip-off read
8 GBmicroSD for persistent storage

Getting a shell: chip-off, and turning psh into sh

Like most Hikvision embedded products, this device doesn’t hand out a console for free. The serial port on the board does drop you to a shell prompt — but that shell is psh, a restricted vendor binary that gates anything useful behind an RSA challenge-response only Hikvision itself can answer. /etc/profile execs straight into it on every login, UART or otherwise, so short of the private key, a stock unit’s console is a dead end.

Getting past that started at the flash chip, not the console.

Macronix MX25L25645GZ2I-08G SPI NOR flash IC on the DS-KH6320-WTE1 mainboard, before desoldering

The device’s entire persistent storage: one Macronix MX25L25645GZ2I-08G, 32 MB SPI NOR flash

1. Chip-off. The device’s entire persistent storage is a single Macronix 32 MB SPI NOR flash IC, sitting exposed on the back of the mainboard. Using hot-air, it was desoldered and read directly with a flash programmer, and it gives a complete, bit-for-bit image of everything the device boots from — bootloader, kernel, root filesystem, application binaries. That image is what makes everything downstream possible: it can be studied, patched, and reflashed offline, with zero risk to a running unit until a rewritten image is actually written back.

2. Finding the actual boot path. The root filesystem here is a RAM-loaded initrd, rebuilt fresh from the flash image on every boot, and it’s mounted read-write at runtime. A boot script runs early in the init sequence, before the console shell ever spawns and sources /etc/profile. That script is itself obfuscated on flash, using an encryption scheme that turned out to lean on a single key baked directly into a kernel module, recoverable by disassemblying it. Using that key, the boot script can be decrypted, edited, and re-encrypted well enough to pass the device’s own check on the way in.

3. The actual patch. No binary got replaced — the fix is smaller than that. One line, inserted near the top of the (re-encrypted) boot script, strips the /bin/psh invocation out of the in-memory copy of /etc/profile before the console shell ever reads it:

if [ -f /etc/profile ]; then
    awk '!/^\/bin\/psh/' /etc/profile > /tmp/.profile.nopsh \
      && cat /tmp/.profile.nopsh > /etc/profile
fi

Nothing on flash changes structurally — the ramdisk itself is untouched, only this one boot script differs, and the edit above is redone in RAM on every single boot. Since the root filesystem gets rebuilt from the (unmodified) flash image every time anyway, this is about as close to a reversible modification as an embedded device gets. Flash the original image back and the device is exactly as it shipped.

The DS-KH6320-WTE1 mainboard, reassembled with its touch panel, ready for UART console access

The same mainboard, back in the unit — panel connector, test-point grid, and the console header this whole console-access step runs over.

Result: power the board, attach a UART adapter to the console header, and instead of a numeric RSA challenge you get a bare [root@dvrdvs] # prompt. id reports uid=0(root). No login, no password, no vendor key required — because the device is, at this point, unmistakably yours.

That console is also the delivery mechanism for everything that follows. The device ships with its own tftp client, so cross-compiled binaries get pushed over from a bench host and launched straight from the UART shell — no network stack changes, no SSH daemon needed, just TFTP in and a shell command to run it.


Porting DOOM to a door-intercom touchscreen

The obvious first target, because it’s the obvious first target for any embedded Linux device with a framebuffer. The port used is doomgeneric’s linuxvt backend — pure framebuffer plus evdev, no X11, no SDL — cross-compiled statically for ARM EABI5, since this device runs uClibc and almost nothing built for a normal glibc host will run on it unmodified.

Getting a static binary onto the device turned out to be the easy part. Getting a picture out of it was where the actual work was.

Finding — and actually writing to — the framebuffer

The device exposes /dev/fb0 for the Linux framebuffer console, and the first working build wrote to it exactly the way every framebuffer tutorial says to: mmap() the device, memcpy() pixel data into it, done. Every diagnostic said this was working — the process stayed alive at a believable frame rate, and reading the mapped memory back showed real, changing, DOOM-palette pixel data on every frame.

The panel stayed black.

The actual bug: on this SoC's framebuffer driver, mmap() only updates the backing memory — it does not by itself push anything to the display controller. A plain write() to /dev/fb0 does trigger a real update (confirmed by writing raw noise to the device directly), but doomgeneric, like most framebuffer software, only ever mmap()s and memcpy()s. The fix is one ioctl call — FBIOPAN_DISPLAY — issued once after the initial buffer setup and again at the end of every drawn frame. That call is what actually forces the panel to latch the new buffer; nothing about what gets written needed to change, only the missing commit afterward.

With that one ioctl added, arbitrary pixel data started reaching the physical panel for the first time — proven with the simplest possible test before ever pointing DOOM at it: pushing an ordinary photo straight into /dev/fb0 and watching it actually appear on the glass.

A test photo written directly to the DS-KH6320-WTE1's framebuffer, confirming the FBIOPAN_DISPLAY fix

First real proof the panel was listening: a plain photo, pushed straight into /dev/fb0 after the FBIOPAN_DISPLAY fix, showing up correctly on the actual glass.

Making the panel a controller

doomgeneric’s linuxvt backend only ever implemented keyboard input — it has a stub concept of touch, but its actual device scanner looks for a keyboard’s key set and silently ignores anything that only reports EV_ABS coordinates, which is all a touch panel ever sends. On a device with no keyboard at all, that meant the build failed outright at startup with “no compatible input device found.”

Real touch support meant:

Six-zone touch control layout overlaid on the rendered DOOM frame

Moving a finger off one zone releases it, and entering a new one presses that action — the same behaviour as a real d-pad, just drawn in software rather than existing as a physical control. Confirmed working end-to-end, not just at the event-detection level: dropped directly into a level with -warp 1 1, tapping FORWARD visibly walks the player, and FIRE discharges a weapon.

DOOM gameplay running on the panel after being moved by touch controls, tearing artifact visible near the top of the frame

Touch-controlled gameplay in E1M1 — the green band near the top is the tearing artifact covered next, not a touch-control issue.

The known bug: colours are off, and the picture is shifted

This one's real, understood, and not yet fixed. The screen is single-buffered — there's only enough video RAM for one frame, not two — and this SoC's FBIO_WAITFORVSYNC ioctl appears to be a stub that reports success without actually blocking on real display timing. The result is a classic tearing artifact: a frame can get committed mid-scan, so a moving image shows a visible horizontal split, and static content can appear shifted by a partial-frame offset. The colour palette reads slightly muted for the same underlying reason — confirmed by writing known-good static test patterns straight to the framebuffer (vivid, perfectly accurate colours, zero shift) versus the same content going through doomgeneric's continuous per-frame commit loop (same palette, visibly duller). The panel, the pixel format, and the colour path are all confirmed correct in isolation; it's specifically continuous, unsynchronised writes that produce the artifact.

The real fix is proper double buffering — render into an off-screen buffer and pan to it only once a frame is complete — but that needs more video RAM than this framebuffer currently has allocated, which is a bigger change than a userspace program can make on its own. Filed as future work rather than papered over.

Eight-bar colour test pattern written directly to the framebuffer, showing accurate colour reproduction with no tearing

The control test referenced above: a plain colour-bar pattern, written directly and once — no continuous per-frame commits. Vivid, accurate, zero shift. This is what confirms the artifact belongs to doomgeneric’s write pattern, not to the panel or the pixel format.

Getting here wasn’t a straight line. An earlier attempt at fixing the video path — re-latching the display mode on every frame — looked clean in isolated testing and then produced this on the very next real run:

A severe checkerboard/static corruption artifact from an earlier, since-abandoned framebuffer fix attempt

An earlier, more aggressive fix attempt, mid-failure. Traced to a mode-set ioctl that turned out to be non-deterministic on this hardware — it passed clean tests twice before corrupting output the third time for no input-side reason. Reverted outright rather than chased further; the tearing bug above is the one that remains, not this one.

What does work cleanly: touch input, end to end. Movement, turning, firing, menu navigation — all of it responds correctly and with no detectable input lag, on real evdev events from the physical panel, not a synthetic test harness. The rough edges here are entirely on the display side.

Then: a Minecraft server, on the same hardware

Once DOOM proved the toolchain and the deployment path both worked, the natural next target was something with a very different resource profile: a real, current-protocol Minecraft Java Edition server. Hand-rolling a server from the wire protocol was tried first and abandoned early — not because it’s impossible, but because reimplementing a modern Minecraft protocol from scratch on a device this constrained is a much bigger undertaking than porting an existing one. The project switched to UCraft, an MIT-licensed Minecraft server written in plain C specifically for resource-constrained machines, cross-compiled with the same static ARM toolchain used for DOOM.

Minecraft Java client join message confirming the server is running on the Hikvision intercom, not a PC or cloud VM

The join banner says it plainly, because a screenshot alone wouldn’t be believable: “This server is running on real hardware: Hikvision DS-KH6320-WTE1 IP video intercom indoor station… this exact server written in C from scratch and cross-compiled for the device — no JVM involved.”

It works — a real client, connecting normally. Point the official Minecraft Launcher at the device’s address on the matching protocol version, and it shows up in the multiplayer list with a custom MOTD, accepts the connection in offline mode, and spawns the player into a real, non-flat world — hills, caves, ore veins, the works, generated by actual 3D Perlin noise rather than a flat superflat placeholder.

Exploring the generated world at night, connected to the server running on the intercom

A few things mattered more than expected:

Net result: the same door intercom that shows you who's at the front gate, once, briefly, also ran a joinable Minecraft server off a memory card taped to the inside of a wall-mounted panel — while the device's own GUI kept working normally alongside it, since the server touches nothing display-related.

What’s left

Both ports are functional, not finished. The known open items:

None of it changes the headline: a locked-down commercial door intercom, running a 30-year-old first-person shooter and a modern Java game server, on hardware whose entire job description was supposed to be “show a video feed and unlock a door.”