Getting inside a MIB2
The last post ended with two screens I controlled: an ESP32-P4 panel in the car and an iPhone beside it. The third screen was already in the dashboard. It was the useful one, built into the car, bright enough for daylight and attached to the speakers and USB socket I eventually wanted to use.
Scope: every test described here ran locally against a head unit I personally own, with no Volkswagen, dealer or other third-party backend accessed. Authentication secrets, proprietary firmware and vendor SDK components are not included here or in the accompanying examples.
Volkswagen calls this generation MIB2. Mine is a TechniSat MIB2 Standard whose
dense software-train name, MST2_EU_VW_PQ_P0241T, identifies the product family
and European region before reaching the useful parts: PQ is the older
electrical platform used by the Scirocco, P0241T is the installed firmware
train, and an i.MX6 running QNX Neutrino 6.5 drives the factory 800 by 480
display.
I knew none of the useful runtime detail when I started. A diagnostic developer flag read disabled, and a long press on MENU opened the ordinary service screen instead of Testmode. The available evidence indicated that enabling the flag would expose the Green Engineering Menu. Establishing that result took fourteen attempts over three days.
The head unit refused the obvious write
The first attempt was deliberately narrow. I read the exact unit identity, entered the required diagnostic session, confirmed that developer mode was disabled, measured the supply, committed a single-shot journal, sent one write and read the value back. The head unit answered every setup request and then rejected the write:
[developer-mode read] -> disabled
[session entry] -> positive response
[developer-mode write] -> request-sequence refusal
[developer-mode read] -> still disabled
The refusal proved that the write reached the ECU and arrived in the wrong sequence, while saying nothing about which prerequisite was missing. The remaining investigation had to distinguish authentication from preparation and session choice without pretending the first refusal had diagnosed any of them.
I recovered enough of the unit's SecurityAccess flow to derive a key that the unit accepted. The key derivation is intentionally omitted here. The important result was that authentication succeeded while developer mode remained disabled: a second bounded write received the same sequence refusal and the readback still showed the original state. Authentication alone was insufficient.
The next lead was workshop preparation. Analysis of the relevant diagnostic
workflow showed that coding needed additional local metadata before the target
record could be written. I used the unit's own historical values without
publishing the tuple. The OBDLink CX answered that longer preparation request
with ?, its own invalid-command response, before any ECU acknowledgement was
visible. The adapter had become the problem.
A negative response became the transport proof
Long diagnostic requests use ISO-TP segmentation. The CX accepts an STPX
command and is supposed to handle the first frame, the head unit's flow-control
reply and every consecutive frame on the CAN bus, yet the longer payloads now
alternated between FC RX TIMEOUT, NO DATA and the adapter's ? after days
of reliable short requests, with no independent capture showing what the CX
had actually put on the wire.
I built a harmless overlength TesterPresent request to separate delivery from
meaning. If all eight payload bytes reached the head unit, the correct answer
was an ECU negative response for incorrect length, without changing any
configuration or relying on an authentication path: 7F 3E 13. A timeout could
not be counted as delivery, and an adapter error could not be reinterpreted as
an ECU refusal.
An explicit flow-control address pair finally produced exactly that response:
[explicit flow-control configuration] -> accepted
[harmless overlength TesterPresent] -> ECU incorrect-length response
That proved complete application-layer delivery under the tested setup. It did
not reveal the physical frame sequence or explain why the same adapter settings
behaved differently on later connections. Activation versions 2, 3 and 4 still
stopped at their transport preflight, once on FC RX TIMEOUT and twice on NO DATA, before any persistent ECU write was admitted.
The fifth version searched a bounded matrix of 49 adapter configurations. It never reached Bluetooth on the first run because the P4 could not create another durable record in its crowded NVS partition, while a revision that moved the journal into a reserved flash region stopped before flashing when that region proved to contain 3,459 non-erased bytes. Both safety gates held. Zero transport requests left the P4 in those attempts.

Version 5 revision 2 used a known blank storage area. The first seven transport
candidates returned CAN ERROR. Candidate eight returned the expected ECU
incorrect-length response, so the search stopped immediately and the activation
continued on that same established connection. The head unit accepted the
prepared metadata, then accepted the developer-mode write:
[workshop preparation] -> positive response
[developer-mode write] -> positive response
[developer-mode read] -> enabled
[default-session read] -> enabled
The collector still exited with failure. Its frozen audit demanded exact
equality between two timing fields sampled on opposite sides of a storage
guard; one recorded 3,080 ms, the other recorded 3,081 ms after the clock
crossed a millisecond boundary, and the strict result therefore remained
INCOMPLETE even though every command, acknowledgement, notification,
admission record and readback was present.
I left it that way. A separate read-only run, with the ignition state confirmed, matched the unit identity and found developer mode enabled in the default session. Its audit passed. Holding MENU now opened Testmode, and the factory unit showed Green Engineering Menu 3.5.


Turning the head unit into the network
Developer mode made the unit inspectable, though it did not provide the link I needed. The MIB already had Wi-Fi hardware and drivers. The configuration analysis identified one WLAN enablement field and one hotspot enablement field, while a separate client-mode visibility field had to remain untouched.
The first coding attempt looked familiar. Exact baselines passed, the coding session was active, and the first WLAN write received a sequence refusal. The hotspot write was never sent. Full readback showed both records unchanged.
Analysis of the relevant workflow explained the missing preparation. On a configured link, coding required workshop metadata and date preparation before the target record, followed by the hotspot adaptation on the same open link. I reproduced that bounded order with complete before-images and allowed one write to each target record.
Both changes were acknowledged and read back three times. Each target changed only its intended enablement bit; the client-HMI field and every unrelated bit remained as they were. The final reset produced a response-pending indication followed by a positive reset response in the same adapter result. My single-payload gate rejected that combined response and stopped before its final adapter handoff commands, while the retained bytes preserved the positive reset response and a physical check settled the remaining question by finding WLAN and Mobile Hotspot in the factory interface.
The head unit now created its own access point. An iPhone joined it and received an address automatically. The P4 then joined as a station and opened TCP port 23 on the MIB. Its first 15 received bytes were Telnet option negotiation, which proved a listener was there without sending Telnet application data.
Eight shell commands, six clean exits
The next package added Telnet negotiation and one root login, with authentication
inputs supplied at runtime and excluded from the firmware and evidence. It attempted
eight fixed read-only commands. The first version ran ifconfig, captured its
output, and stopped because this QNX image has no external printf command for
the completion marker. Replacing that marker with the shell's supported echo
let the same bounded inventory finish.
Six commands exited zero. netstat -rn returned 126 because the utility was
absent. Listing the root returned the directory contents and exit 1 because
/cdrom was missing. Preserving those nonzero results mattered; calling the
whole inventory successful would have erased two useful facts.
The 11,464 captured bytes established QNX Neutrino 6.5 on an i.MX6 MIBSTD2 with two Cortex-A9 cores at 996 MHz, 2,048 MiB of RAM with 1,394 MiB free at capture, an 800 by 480 display mode at 60 Hz, installed Screen, EGL and GLES libraries, and enough detail about mounted media, running processes and processor links to begin native work from the P4 without an Ethernet adapter or a filesystem write.
PPS was the next useful layer. QNX Persistent Publish/Subscribe is how many
services expose state as typed objects. Five directory listings found renderer
component and context directories plus iAP2 device, location and
nowplaying objects. I did not open the object bodies in that pass. A follow-up
snapshot package reached the root shell and failed its first utility-selection
gate in 289 ms. Because the compound check could not distinguish a missing
reader from a missing hex dumper, it stopped with zero payload bytes. The
directory map was real; live media metadata was still pending.
Building for a system with no compiler
The inventory found no compiler, headers or startup objects on the head unit, and I had no licensed QNX 6.5 development kit elsewhere. An archived BlackBerry PlayBook NDK matched its original vendor checksum, though its licence limited it to the BlackBerry platform, so inspection stopped short of using its proprietary headers, libraries or CRT for the MIB build. The route that remained was narrower. A reconstructed GNU toolchain could emit QNX ARM objects. I captured the ELF layout and public imports of three tiny programs already on my own unit, then wrote a small startup in assembly from those observed calling conventions. Link-only views rebuilt section metadata for the target's stripped libraries while preserving their original code and addresses. The resulting executables used only self-written startup code, public Khronos declarations and four runtime libraries already present on the MIB.
Deployment was deliberately awkward. Telnet commands wrote each executable to
an exclusive name in /dev/shmem, 64 encoded bytes at a time, then read every
byte back before granting execute permission; the first upload exposed an
oddity in QNX echo, whose octal parser consumed the character after each
escape, so adjacent escapes lost backslashes and corrupted the ELF header until
a disposable underscore after every octal value fixed both the encoding model
and the physical readback.
The corrected exit and stdout probes both ran once and returned zero. The
graphics upload then lost Wi-Fi before execution. A fresh graphics-only pass
verified all 11,024 bytes, created an 800 by 480 Screen window and two buffers,
then failed because eglGetDisplay returned no display. The process had
inherited an empty GRAPHICS_ROOT.
One final launch set GRAPHICS_ROOT=/usr/lib/graphics/iMX6X for that process
only. EGL initialised against the Vivante GC2000, created an ES2 context and
window surface, cleared the buffer magenta, read the pixel back, swapped it to
the factory display, waited four seconds and exited cleanly while I watched the
colour appear on the dashboard. No emulator ran. Audio and input remained
untested.
The same narrow native path could also read the existing display. A helper captured one 400 by 240 RGB565 factory frame through QNX Screen. The P4 received all 192,000 bytes, checked their CRC and displayed the still image with the orientation and colours intact.

At the end of 13th September, the head unit had a verified developer-mode readback, a working hotspot, a bounded root shell path and one self-written native frame. The full RetroArch frontend still stopped at missing development headers. The next job was to turn the tiny graphics proof into a menu, then find out whether the MIB could run an emulator at a useful speed.