dtrh.net~/blog
KBS
│KBS all posts

Reverse Engineering the Wii U GamePad — Part 2: It Was The Clock

Reverse Engineering the Wii U GamePad — Part 2: It Was The Clock

Reverse Engineering the Wii U GamePad — Part 2: It Was The Clock

Part 1 ended with a GamePad that drove my Linux desktop perfectly while its screen kept saying "could not connect". This is the rest of that hunt: I decrypted a real console's session, rebuilt the media plane to byte parity, retired two extremely convincing wrong answers with measurements, and found the actual gate — a timestamp domain — after which the desktop appeared on the pad. Then an unrelated wifi firmware bug started taking it away again, which turned out to be its own lesson about what "working" means.

The tools are public. Every script, patch, test harness and research document behind this article lives in one repository: DTRHnet/WiiU-GP-RE — MIT for my own work, each patch under the licence of the upstream it modifies. If all you want is the folder map, jump to Appendix A.


Published: 2026-11-04
Author: KBS admin@dtrh.net
Subject: Wii U GamePad (DRC) — my own hardware, my own console, my own box
Method: Passive monitor capture, WPA2-CCMP decryption with a known PSK, protocol archaeology, userspace reimplementation, A/B on live hardware
Evidence: 165 MB authenticated capture, 167,154 decrypted frames, 73,500 decrypted console video datagrams, three working runs I watched live, and one controlled A/B that removes only the fix


Contents

  1. Where Part 1 left off
  2. Decrypting a real console session
  3. Rebuilding the media plane to parity
  4. The metric that was lying to me
  5. Two wrong answers, retired with data
  6. The gate was the clock domain
  7. The A/B that proves it
  8. The picture arrives — and the input bugs underneath it
  9. Then the radio started dying
  10. Where it actually stands
  11. Next post: the toolkit
  12. Reference material studied
  13. Appendix A — what each folder contains

1. Where Part 1 left off

The score at the end of Part 1 was half of a working console:

  • The pad associates to my box's impersonated AP on channel 149, survives Nintendo's rotated PTK, takes 192.168.1.11 over DHCP.
  • Touch and buttons drive my Linux desktop. That half has worked from the beginning.
  • The screen shows "Could not connect to the Wii U console", and the pad emits 4-byte datagrams to UDP/50010 at tens per second.
text1 line
192.168.1.11:50110 > 192.168.1.10:50010   payload: 01 00 00 00

libdrc calls those resync requests: "I got frames, I cannot decode them, start over." I believed that reading completely — and it shaped every experiment I ran afterwards.

2. Decrypting a real console session

The only way to stop guessing was to look at a real console↔pad session, decrypted. The capture is ordinary WPA2-CCMP at layer 2, but five separate things have to be exactly right before a single frame comes out, and four of them are wrong in every stock tool:

# element the trap
1 PTK stock 802.11i PRF-512 — this part is normal
2 key rotation Nintendo rotates the whole PTK left by three bytes (CONFIG_TENDONIN); unrotated, nothing verifies
3 proof of correctness the rotated KCK must reproduce the pad's message-2 MIC and the rotated KEK must AES-unwrap message-3's key data — the second check is what yields the group key
4 AAD FC ‖ A1 ‖ A2 ‖ A3 ‖ SeqCtl with SeqCtl zeroed ‖ QoS control; send it as captured and every frame fails
5 nonce priority ‖ A2 ‖ PN — and the priority octet is the TID
text4 lines
derive  PTK-48 from PMK + ANonce/SNonce + AP/STA MACs   (802.11i)
rotate  ptk = ptk[3:] || ptk[:3]                        (CONFIG_TENDONIN)
CCMP    nonce = priority(=TID) || A2 || PN bytes 7,6,5,4,1,0
        AAD   = FC || A1 || A2 || A3 || SeqCtl(0) || QoS

That priority octet is the single most consequential detail in the derivation. With priority = 0, only TID-0 traffic decrypts — which is exactly the pad's 128-byte input packets. Everything else looked like group-encrypted media I could not touch. Fix the TID and the "group-encrypted" verdict evaporates: 167,154 of 167,324 protected frames decrypt, 99.9%, and the residue is only the frames straddling three re-key boundaries.

Three sessions, not one — each with its own PTK, because the pad re-associated twice:

session starts at note
0 0.59 s pad already streaming the console's home screen
1 53.98 s pad re-associates
2 73.00 s re-associates again; media runs to the end

Deriving one PTK and stopping there is why only 7,088 frames of that epoch were ever visible. Decrypted and attributed per epoch, the session looks like this:

flow datagrams what
50020 → 50120 73,500 video, 14,137 frames over 238.86 s
50021 → 50121 41,715 audio + the per-frame format message
50023 → 50123 1,475 command requests
50123 → 50023 1,475 command replies, exactly 1:1
50025 → 50125 220 96-byte panel table, ~1 Hz
50110 → 50010 3 the resync requests

A trap worth flagging: my extractor names its dump files by destination port, so port50123.bin is console→pad, not pad→console. That naming invites reasoning about "the wrong direction"; the 1475/1475 symmetry is a genuine 1:1 request/reply pairing, not a writer artefact.

3. Rebuilding the media plane to parity

Part 1 left nine defects, all found by looking at bytes rather than summaries. Condensed, because each was verified on the wire:

# defect fix
1 vstrm ext-option bytes emitted in the wrong order rebuild bytes 8–15 in console order (IDR: 80 82 00 83 85 06 00 00, P: 82 00 83 85 06 00 00 00)
2 a stray iptables OUTPUT DROP for UDP/50121 discarded 120 MB, including the per-frame format message removed; the runbook deletes it if it reappears
3 format message body wrong (length field, outer bytes, clocks) 32 bytes, zero length, clocks 16000/16000/21000/21000
4 stream was VNC-change-driven, not continuous push every iteration, median inter-frame gap 16,683 µs
5 media source ports were ephemeral bind 50020 (video) and 50021 (audio + format)
6 UVC/UAC fields big-endian little-endian (80 3E = 16000)
7 no display command ever sent assert svc 5/0x14 (backlight) — visible effect on the panel
8 init flag set once per streamer lifetime, so it left before the pad joined re-assert on every IDR / session
9 the console's time command was never sent publish it from the UVC/UAC synchroniser

The payload itself was never the problem, and I proved that offline before trusting any live run: reconstructing the live stream's chunks into a normal H.264 file and handing it to FFmpeg gave 242/242 frames decoded, zero errors, exact expected output size. Geometry, six-macroblock-row strips, option bytes, cadence — correct on the wire from the start.

There is a subtlety worth stating because it invalidates a whole class of "your chunking is broken" theories: chunk boundaries are transport-only. Decode any single chunk on its own and it fails — for the console's stream exactly as for mine — because the pad concatenates the five chunks into one CABAC slice. Both streams need the pad's hardcoded SPS/PPS plus a synthesised slice header to decode offline, and under that model both decode cleanly.

Which is precisely why the next part hurt.

4. The metric that was lying to me

The console's pad asked for a keyframe three times in 23 milliseconds, at t=74.9 s of a 300-second session, and then never again. Three datagrams, all identical 01 00 00 00 — and the console answered with a fresh IDR.

My pad, in every one of my own sessions, asked 20–60 times per second, forever.

So I built the search around that: low resync = accepted, high resync = rejected. I wrote it into a document as an invariant. Parameter sweeps were tuned against it. Runs were scored by it. It is an extremely seductive metric — it is objective, it is on the wire, it is the only signal the system emits, and it correlates with everything you are changing.

It is wrong.

When the picture finally arrived, I measured the pad's resync rate in those same runs: 41.8/s, 22.0/s, 60/s — while the pad displayed video correctly, with me watching, on three separate occasions. The console's 3-in-300-s is not a validity signal; it is one transient hiccup. The pad asks for keyframes as a matter of course, gets them, and shows the picture anyway.

Every sweep that "did not converge" was chasing noise: the best configuration scored badly and the worst scored well, for reasons unrelated to the display.

The pad's screen is the only judge. Nothing in this system reports acceptance on the wire.

5. Two wrong answers, retired with data

Both of these were confident, documented, and wrong. Writing down how they were killed is the point.

UVC/UAC: the documented trap

libdrc's notes say of the camera/microphone command (query type 1, once a second):

"Recent versions of the GamePad firmware will only show the video stream after receiving one such command."

That is exactly the shape of my symptom, so I tested it properly. My first attempt said the pad answered 0 of 101 queries — but that was my tool: the pad's reply is a 24-byte datagram while the query is 56, and I classified by length. Fixed to match the payload signature (02 00 01 00 = query-type-1 reply), and the truth is:

  • 228 queries → 228 replies in the console session;
  • 101 queries → 47 replies in mine, and all 47 are inside the pad's awake window. Replies stop at exactly t=58.0 s — and so do resyncs, because that is when the pad went to sleep.

Replies match query sequence numbers one-for-one. The documented precondition was met the entire time. Not the gate. It also produced a rule I now apply everywhere: classify packets by content, never by length.

Degenerate chunks: a good theory killed by one run

A static desktop makes x264 emit 1–2 byte P-frame chunks; the console never goes below 395 bytes (its content is a UI with graphics). A 1-byte strip in a fixed-function decoder is a plausible reason to reject a frame, so I played a real video fullscreen on the served display and measured again:

source fps datagrams/frame pad asks for keyframes
static desktop 8.98 5.30 ~60/s
fullscreen video 55.52 5.15 ~47/s

Content shape made no difference. Not the gate — and the suspicion was wrong in an instructive way: the console's large chunks are a property of its content, not of a protocol requirement.

And one belief I had to give up entirely

I had it written down that the pad "never starts its media services", because no pad traffic ever reaches 50020/50021. Those are the pad's camera and microphone uplinks, and they are silent in a healthy console session too. The pad was in the AV session the whole time, asking for keyframes.

6. The gate was the clock domain

The pad schedules playback against the TSF — the 802.11 timestamp it synchronises from the AP's beacons. Every console video datagram carries a 32-bit microsecond timestamp in that domain. In the decrypted session it behaves like a real-time clock: first frame 92.036 s, last 330.874 s, span 238.839 s over 238.8 s of wall clock, median delta 16,683 µs — 59.19 fps, exactly.

On my box, GetTsf() has nowhere to read from: iwlwifi exposes no /sys/class/net/<if>/tsf. So the stack ran on a synthetic monotonic clock plus an offset file:

text1 line
frame_timestamp = (monotonic_now - streamer_start) + offset

which means offset must equal the AP's own TSF at the moment the streamer started. Mine was 2000000 — two seconds, left over from an earlier session — while the live AP TSF was 1637 seconds. Every frame I sent was timestamped twenty-seven minutes in the past.

The tool that was supposed to fix this computed exactly the right value and then silently did nothing, because it needs the streamer's pid to subtract the streamer's age — and I was starting it before the streamer. It skipped with no output, so the offset sat at 2 s for every run before the fix.

Measured directly: my frames were stamped 2.667 s → 101.98 s while the AP's TSF was 1637.06 s.

One number makes the mechanism obvious. When the pad cannot display a frame it asks for a keyframe, so if it fails on every frame you get 55 fps in and ~55 requests/s out — precisely the signature I had been reading as "rejection". The metric was not lying about the rate; it was lying about the direction of causality.

The rule is a band, not an equality (and why that matters)

"The timestamp must equal the AP's TSF" is an approximation that happens to be true enough. Measured against the beacon TSF at five points across the console session, the console's own video clock is a constant −132.369 s away from its phy's TSF — a per-boot constant, because the video subsystem starts after the phy does. By the strict reading, the console is 132 seconds "wrong" and displays perfectly.

So the real tolerance is: the clock must be monotonic, running at the TSF's rate, and inside the pad's band. Consequences I now design around:

  • one validated seed per session is enough, because the rate comes from the source clock and the offset only has to be right once;
  • rewriting the offset mid-stream is actively harmful. One guard write of -23191915 put a live session into a negative domain and killed it;
  • with a single seed taken from two agreeing monitor samples I measured 11 ms of accuracy against the AP's TSF — far inside the band.

7. The A/B that proves it

Same content, same pad, ~56 fps both times, everything else identical. The only difference is the epoch of the frame clock.

run clock domain what I saw
moving video, correct epoch 1734.9 s → 1840.5 s (AP TSF) video on the pad
verification, correct epoch 1954.2 s → 2059.9 s video on the pad
desktop only, correct epoch 175.3 s → 268.8 s video on the pad
moving video, epoch left at 0 0.5 s → 90.0 s nothing

In the control run the timestamp delta was still exactly 16,683 µs and the stream was otherwise byte-identical to console parity. It made no difference: the pad discarded every frame because the epoch was wrong.

This is the run. Recorded 4 November at 12:43 local — the same minute as the last capture in the log with a confirmed picture — with the desktop live on the pad at ~57 fps while the pad's own sticks and touchscreen drive the box. Everything in the third and fourth rows of the table above is this stream with one field changed.

Open the video full size ↗

The fix is two lines of plumbing and one ordering rule — sample a live TSF before starting the streamer, and keep the offset honest afterwards. The ordering rule is not a detail; it is the entire bug. And because a single bad sample poisons the whole session in a way that looks exactly like "video stopped working", the seed takes two consecutive agreeing samples (monotonic, within 12 s of each other) or it refuses to start.

8. The picture arrives — and the input bugs underneath it

With the clock fixed the desktop appeared, and the input half turned out to be broken in ways that had nothing to do with the display.

I added a raw-bitmask log (DRC_INPUT_LOG=1) because "the buttons do nothing" is otherwise unfalsifiable. The pad sends every button correctly, one bit each:

text2 lines
A 0x8000   B 0x4000   X 0x2000   Y 0x1000   Up 0x0200
L 0x0020   R 0x0010   ZL 0x0080  ZR 0x0040  Plus 0x08  Minus 0x04

Three real bugs, all upstream of the pad:

  1. Buttons were dead by configuration. drcvncclient defaults to joystick mode and my scripts passed -joystick, which routes every button to a uinput gamepad feeder that nothing on the desktop reads. Mouse mode maps them to keys and clicks over the VNC session.
  2. A tap could never click. The touch handler only ever sent pointer moves; there was no code that pressed the left button. Touch-down now presses rfbButton1Mask, touch-up releases it.
  3. The sticks had no code path at all in mouse mode — they fed the same dead feeder. Left stick is now relative pointer motion and the right stick is the wheel, time-based with a squared response curve. My first attempt used a fixed step per report (~630 px/s) and my own note was "sensitivity is way too high"; the retune got "the speed is good now".

A second, quieter bug was in the same neighbourhood: the client only pushed a frame when the VNC server reported a change. On a static desktop that is one packet per second on the air while the process looks perfectly healthy — gdb showed the main thread parked in select() inside WaitForMessage, waiting for a screen update that never came. The fix is an independent re-push thread firing every 16.7 ms, used only after ~50 ms of VNC silence so it cannot alternate a cached frame with live content. Static desktop went from 1 pkt/s to 527 pkt/s.

9. Then the radio started dying

Having a working picture changed the problem. What remained was that it kept stopping, and the cause was not in my protocol code at all:

text4 lines
iwlwifi 0000:03:00.0: Unhandled alg: 0x71b          (repeating, 30-50 s apart)
iwlwifi 0000:03:00.0: Unhandled alg: 0x3000707
iwlwifi 0000:03:00.0: Device error - SW reset
iwlwifi 0000:03:00.0: restart completed

After the firmware resets, the AP stops transmitting entirely. hostapd is still running and every pid and socket check passes, but no beacons leave the card: no beacons means the pad cannot associate, so it sits on "could not connect" and eventually sleeps. Meanwhile my streamer keeps blasting ~500 packets per second at a station that is not there — the ARP entry is static, so a packet capture looks perfectly healthy and is completely fake.

The crash interval tracks load: 70–233 s while streaming, versus 758–1033 s in the idle gaps. My running suspects are 600+ packets/second with A-MPDU disabled and RTS/CTS on every frame (deliberately un-console-like settings I adopted for link parity), plus key reinstallation on every pad rejoin.

That state only clears with a driver reload, and after the reload iwlwifi's LAR marks every 5 GHz channel no IR until an idle supplicant's country hint plus a scan unlocks it — otherwise hostapd dies with Could not select hw_mode and channel (-3).

Each reset also resets the phy TSF, which corrupts the media clock again. Across successive resets I measured offsets of 1003 s → 173 s → 87.5 s → 71 s → 61 s → 6 s. Every one needs a reseed, and the pad needs a fresh init frame, because its own clock stepped too.

So the setup now heals itself:

tool job
ap-health.sh exit non-zero unless tx_packets grows ≥5 in 5 s — catches the exact state every process check misses
ap-recover.sh driver reload → 5 GHz unlock → drc-up.sh → supervised streamer
drc-watchdog.sh polls health; two consecutive failures → recovery. It fired for real during this work
tsf-guard.sh one TSF sample per minute (a permanent monitor capture made the crashes worse); on a step, rewrite the offset and signal the streamer to re-arm and re-init
uvc-live.sh / uvc-start.sh the whole session, detached, so it outlives the shell that launched it
uvc-kill-live.sh the safe stop. pkill -f run/uvc-live.sh matches the caller's own command line and kills your shell — which happened repeatedly

The measured cost of the loop, on the last run of the day: the pad associated at 240 input packets/s, video flowed at 290.9 datagrams/s — console parity, 307 in the real session — and 4 seconds after the pad dropped, Unhandled alg: 0x3000707. Forty seconds of usable session, then a ~50 s rebuild that is slower than the crash rate, which is why unattended operation thrashes.

Two things about that health check that I got wrong, in case you build one
  1. tx_packets does not count beacons on this driver. With no station present, "tx grew by 0 in 5 s" is expected, not proof of a wedge — I read it as a dead AP twice. The signature that actually means something is tx_packets flat while a station is associated and video is flowing.
  2. A monitor interface on the same radio cannot be re-tuned while the AP is running. iw dev mon0 set channel 149 HT20 exits 0 and leaves the vif with no channel, so it captures nothing. Every "live clock correction" scheme I wrote with it silently stopped correcting.

10. Where it actually stands

Working: live Kali desktop on the pad at ~57 fps, stick driving the cursor, tap clicking, buttons mapped, right-stick scroll. Plus one controlled A/B in which removing only the clock fix removes the picture.

Working but fragile: the AX210 firmware crashes every one to two minutes under streaming load, and each crash costs a driver reload. The watchdog turns that into a ~60–90 second outage instead of a dead session, but it is not a fix. Moving the AP to a different radio is the obvious next move — and specifically to a 5 GHz adapter that implements get_tsf(), which would delete the synthetic clock, the offset file and the whole reseed-on-reset dance in one stroke. This box's only 5 GHz radio is the one that crashes, and its only other radio is a 2.4 GHz dongle, which the pad cannot even see.

Not verified: audio on the pad's speakers. The pad also sleeps after ~30–90 s untouched — it wakes on touch and resumes, but a real console session never idles that way, and sending the console's own continuous svc 0/0x00 query did not prevent it.

One thing I already know is still wrong. Now that the session is decrypted I can diff my command plane against the console's command-by-command, and mine is a four-command approximation of a six-command opener: the console also sends svc 4/0x05 (state read), svc 5/0x0c with a 5-byte payload, and svc 0/0x10 with 32 bytes — none of which my stack has ever sent. Worse, I disabled one command on a misreading: I had recorded the console's svc 5/0x02 as zero-length, and it is 102 zero bytes, which the pad answers with error 0. That is where Part 3 starts.


Next Post:

I'd really enjoy having some kind of persistence. It may be a while. However, here are my tools — I've made them public.

The repository

Everything this article describes is in one place: DTRHnet/WiiU-GP-RE — the decryptors that produced the ground truth, the stack runners, the patches, the offline test harnesses, and the research record every tool cites. The folder-by-folder map is in Appendix A.

bash3 lines
git clone https://github.com/DTRHnet/WiiU-GP-RE
cd WiiU-GP-RE/WiiU-tk
sha256sum -c MANIFEST.sha256      # 149 tracked files, byte-identical to the working tree

Two things to know before you run anything:

  • The scripts hardcode ROOT=/home/kali/Wiiu. WiiU-tk/README.md §"Paths" has the one-line sed to repoint them. The copies are kept verbatim on purpose, so the hashes above stay meaningful.
  • Nothing third-party is vendored. pc2drc, hostapd, x264 and the kernel tree are referenced by pinned revision and shipped as patches only. My original work is MIT; each patch carries the licence of the upstream it modifies. THIRD-PARTY.md has the full table — including what was deliberately stripped before publication: the packet captures, the device dumps, the console's PSK, and three core dumps that contained process memory.

Reference material studied

The written protocol reference for this device is small, and this is all of it. Almost everything Part 2 asserts came from captures — these are what told me where to point the analyser.

reference what it gave me
libdrc protocol reference: sc-cmd · sc-vstrm · services and ports the service/method map, the command retry state machine, the VSTRM header layout
pc2drc (GPLv2) the stack I actually ran; the mac80211 TSF patch originates in its kernel-patch-files/
libdrc — Mema Hacking (BSD-2) the library underneath both of the above
wozt/libdrc · GaryOderNichts/libdrc (BSD-2) forks diffed for wire-format differences
Barista (GPLv2) an independent DRC implementation, used as a second reference for framing and port roles
WiiUBrew — GamePad the link, pairing and the protocol as publicly described
WiiUBrew — GamePad/LVC the pad firmware's internal structure: drivers, services, managers
kuribo64 WUP hardware wiki LCD controller registers, the H.264 block, the UIC, EEPROM layout and CRCs
IEEE 802.11i / CCMP the crypto I reimplemented against the capture, including the two deviations documented in §2

Component versions, since they matter more than they should: hostapd / wpa_supplicant 2.12 (Jouni Malinen et al., GPLv2) built with CONFIG_TENDONIN, x264 with the DRC DRH patch (GPLv2), the Kali 7.1.5 kernel's mac80211 with tsf-kali-7.1.5.patch, and drcvncclient (GPLv3) for the VNC bridge.

Appendix A — what each folder contains

At the repository root: README.md (what this is, the default-deny policy, the audit you should run before pushing), LICENSE (MIT, original work only), THIRD-PARTY.md (provenance and per-patch licensing), and WiiU-tk/ — the toolkit itself.

folder what is in it start with
WiiU-tk/00-docs/ the written record: the plan, the ~1,550-line runbook and lab log, the research log, the codec spec, the decrypted-session ground truth, the clock finding, the failure audit, pad boot/modes, the wiki digests and the RCE-hunt notes research/DISPLAY-WORKS.md
WiiU-tk/01-radio-kernel/ the kernel side, portable: the TSF sysfs patch for Kali 7.1.5, the Nintendo-OUI cfg80211 patcher, the out-of-tree module builder, plus the upstream pc2drc kernel patches kept for reference tsf-kali-7.1.5.patch
WiiU-tk/02-bringup/ the documented path in five gated steps, each with a PASS/FAIL: load the module → pair → raise the AP → stream → diagnose 01-load-tsf.sh
WiiU-tk/03-pi/ the Raspberry Pi 5 as the radio host: be-the-console, WPS pairing from the Pi, capture, radio self-tests, fake-console builds, SD/USB recovery — all detached and self-restoring, because taking the radio down kills your SSH session pi-be-the-console.sh
WiiU-tk/04-stack/ the day-to-day stack: the live runner, its detached start/stop, radio health and recovery, the clock seeding and guard, the pad-state and command-plane probes, the live watcher, the reset watcher, and the stack runbook uvc-live.sh
WiiU-tk/05-capture/ monitor capture of a real console session, channel hunting, and the two independent decryptors (EAPOL/PTK derivation, H.264 extraction) capture-console-session.sh
WiiU-tk/06-experiments/ the phase 2–12 experiments, and the two bisects that establish causality (which command, which message) phase5-full-session.sh
WiiU-tk/07-protocol/ the protocol-plane instruments: pad state oracle, command-plane driver, arm-trigger probe, byte-exact console replays, the fuzzer command-plane.py
WiiU-tk/08-tests/ hardware-free validation and offline census: streamer smoke, codec probe, stream validation against FFmpeg, packet round-trip, IDR policy, chunk audit run-tests.sh
WiiU-tk/09-rce-hunt/ the pad-side attack surface: read-only enumeration, safe sweeps, fragment/IR/firmware-update probes, the DHCP responder, and the parsed reference tables probe-readonly.py
WiiU-tk/10-build/ reproducible builds: the drcbuild:20.04 container with its inner-build rounds, the native hostap build, and the arm64 Pi recipes build-client-in-container.sh
WiiU-tk/11-patches/ my diffs against upstream — the pc2drc work-tree changes and the earlier paced-sender/IDR-policy patch — with the per-target licence mapping written into the header pc2drc-local-work.patch
WiiU-tk/_meta/ provenance and hygiene: the inventory generator, the source map (toolkit path ⇥ origin ⇥ sha256), and the prepublish audit prepublish-audit.sh

Two files at the toolkit root are the ones people skip: INVENTORY.md (every file → purpose → origin, matched by digest) and MANIFEST.sha256 (sha256sum -c over the whole set).


All work in this article was performed on my own hardware: my GamePad, my console, my box. The pairing step deliberately unpairs the GamePad from its console and is reversible by pressing SYNC twice on the console. No warranty is implied; if you try this, you will brick more sessions than you solve.

normal~/dtrh.net│Reverse Engineering the Wii U GamePad — Part 2: It Was The Clock