Diagrams — Canon G6020 5B00 reset lifecycle + exploit flow¶
Source diagrams for the RE-to-native-reset story. Every claim in these diagrams is
traceable to a validated finding (see the per-file header comment for the exact
doc + line). The native reset they describe was validated on real hardware
(2026-06-01, commit d2f3c81).
Files¶
| File | Engine | What it shows |
|---|---|---|
lifecycle.mmd |
Mermaid | RE-to-native-reset lifecycle: service-mode → transport → session → keyword → cipher → set_command → clean-power-off commit. |
maintenance-state-machine.mmd |
Mermaid | The service-mode maintenance protocol as a state machine (set_session / get_keyword / set_command / get_command + the register reads + the empty-0x86 + the commit). |
methodology-trifecta.mmd |
Mermaid | The trace ⟷ decompile ⟷ correlate loop (usbmon ⟷ Frida ⟷ Ghidra). |
exploit-dataflow.dot |
Graphviz | Full data-flow: APP.BIN decrypt → devices.xml template → functor-3 envelope + bound keyword → functor-2 → 23-byte set_command → usbprint VENDOR_SET → EEPROM. |
drm-bypass-controlflow.dot |
Graphviz | WICReset's reset orchestrator with the 3 cloud gates patched (JZ→JMP) → clearCounters → genuine emit. |
Sources are the single source of truth; rendered SVG/PNG are build artifacts
(gitignored — render locally with just diagrams). On the docs site the Mermaid
diagrams below render client-side from these same sources; the Graphviz diagrams
are prerendered to SVG at build time.
The diagrams¶
Lifecycle — RE to native reset (Mermaid)¶
flowchart TD
%% Canon G6020 5B00 native reset — RE-to-reset LIFECYCLE
%% Source of truth: docs/runbook/g6020-native-reset.md,
%% docs/research/canon-service-mode-field-guide.md,
%% memory/canon-5b00-wicreset-pivot.md (VALIDATED on hardware 2026-06-01, commit d2f3c81).
%% Render: just diagrams (mmdc -> lifecycle.svg)
%%
%% The end-to-end path a native, key-free, cloud-free reset travels: from the
%% physical service-mode entry, through the recovered usbprint VENDOR transport,
%% the plain session + live-keyword handshake, the recovered write cipher, the two
%% set_command writes, and the CLEAN-POWER-BUTTON commit that flushes EEPROM.
start([5B00 / markerWasteInkReceptacleFull<br/>printer-state = stopped]):::problem
subgraph SM["1 - Service-mode entry (physical)"]
combo["Panel combo: power off →<br/>hold ON → resume/cancel ×5 → release"]
enuma["USB re-enumerates<br/><b>04a9:1865 → 04a9:12fe</b><br/>single printer-class iface 0, alt 0<br/>EP 0x01 OUT / 0x82 IN"]
combo --> enuma
end
subgraph TR["2 - Transport (usbprint VENDOR control, EP0 — NOT bulk)"]
vset["<b>VENDOR_SET</b> (IOCTL 0x220038)<br/>bmRequestType <b>0x41</b> OUT<br/>bRequest = frame[0]<br/>wValue = (frame[1]<<8)|frame[2]<br/>data = WHOLE frame verbatim"]
vget["<b>VENDOR_GET</b> (IOCTL 0x22003c)<br/>bmRequestType <b>0xC1</b> IN<br/>bRequest = frame[0]<br/>3-byte read header primes the read"]
end
subgraph SESS["3 - Service session (the ordered handshake)"]
ss["<b>set_session</b> 81 00 00 03<br/>PLAIN 4 bytes — device length-validates<br/>(enciphered 8-byte form STALLs)"]
gk["<b>get_keyword</b> prime 82 00 00<br/>→ live 3-byte device keyword<br/>(e.g. e4 7c 5a) — the ONLY runtime input"]
ss --> gk
end
subgraph CIPH["4 - Write cipher (recovered offline from devices.xml, CANON-SR5 / method=3)"]
pad["pad keyword → e4 7c 5a 00<br/>bind_keyword → bound 00 35 a9 09"]
env["envelope3(method=3, 85 00 00 || operand)<br/>→ 20-byte functor-3 ENVELOPE (SUBJECT)"]
f2["functor2_transform(env, seed = bound keyword, send=True)<br/><b>roles SWAPPED</b>: SUBJECT=envelope, SEED=bound kw<br/>→ 20-byte payload"]
pad --> f2
env --> f2
end
subgraph WRITE["5 - set_command writes (23-byte frames, VENDOR_SET 0x41/0x85)"]
sel["<b>SELECTOR</b> operand 10 07 7c<br/>85 00 00 || payload(20) → 23 bytes<br/>addresses the 'common' waste row"]
clr["<b>CLEAR</b> operand 0d 00 00 ← THE 5B00 WRITE<br/>85 00 00 || payload(20) → 23 bytes<br/>zeroes the absorber counter"]
gc["get_command prime 86 00 00 → EMPTY<br/>by design — NO finalize cmd, do NOT gate on it"]
sel --> clr --> gc
end
subgraph COMMIT["6 - Commit (clean power-button — NOT unplug)"]
rel["Release the libusb handle first"]
pwr["Press POWER BUTTON → printhead parks<br/>→ firmware flushes cleared EEPROM page"]
warn["⚠ Abrupt UNPLUG skips park+flush →<br/>counter not persisted → 5B00 returns"]
rel --> pwr
pwr -. fails if .-> warn
end
done([Reboots NORMAL mode → re-enumerates<br/><b>04a9:1865</b>, 5B00 GONE,<br/>printer-state = idle]):::win
start --> SM
SM --> TR
ss -. carried by .-> vset
gk -. carried by .-> vget
TR --> SESS
SESS --> CIPH
CIPH --> WRITE
sel -. carried by .-> vset
clr -. carried by .-> vset
WRITE --> COMMIT
COMMIT --> done
classDef problem fill:#fde2e2,stroke:#c0392b,stroke-width:2px,color:#7b1414;
classDef win fill:#dff5e1,stroke:#1e8449,stroke-width:2px,color:#145a32;
class start problem;
class done win;
Service-mode maintenance protocol — state machine (Mermaid)¶
stateDiagram-v2
%% Canon G6020 service-mode MAINTENANCE PROTOCOL — state machine
%% Source of truth: docs/runbook/g6020-native-reset.md §3,
%% docs/research/canon-service-mode-field-guide.md (register map 0x84/0x8a/0x8c/0x86,
%% empty-0x86 by design).
%% Render: just diagrams (mmdc -> maintenance-state-machine.svg)
%%
%% Every command is an EP0 usbprint VENDOR control transfer on iface 0:
%% writes = VENDOR_SET 0x41 OUT (IOCTL 0x220038), bRequest = frame[0]
%% reads = VENDOR_GET 0xC1 IN (IOCTL 0x22003c), 3-byte header primes the read
%% The maintenance command identity rides in the frame, NOT in a transport opcode.
direction TB
[*] --> NoSession: device in service mode (04a9:12fe)
NoSession --> Session: <b>set_session</b> 81 00 00 03<br/>(VENDOR_SET 0x41/0x81, PLAIN 4B)<br/>device length-validates → ACK
note right of NoSession
set_session MUST be PLAIN (4 bytes).
The enciphered 8-byte form STALLs.
Bulk-OUT/IN never answer — control only.
end note
state Session {
direction TB
[*] --> Keyed
Keyed --> Keyed: <b>get_keyword</b> 82 00 00 →<br/>live 3-byte keyword (fresh per session,<br/>stable within it) — seeds the write cipher
Keyed --> Keyed: get_version 8a 00 00 →<br/>stable enciphered 20B id (e7 90 c1 84…)<br/>used to recognize "Canon G6000 series"
Keyed --> Keyed: read 0x84 → constant 20B descriptor<br/>(XOR-stream codec CRACKED — NOT the counter)
Keyed --> Keyed: read 0x8c → 20B waste register<br/>(nonlinear key schedule — NOT yet cracked)
Keyed --> Selected: <b>set_command SELECTOR</b><br/>85 00 00 || payload(20) (operand 10 07 7c)<br/>VENDOR_SET 0x41/0x85 — selects 'common' waste row
Selected --> Cleared: <b>set_command CLEAR</b><br/>85 00 00 || payload(20) (operand 0d 00 00)<br/>← THE 5B00 WRITE — zeroes the counter
Cleared --> Cleared: get_command 86 00 00 → EMPTY<br/>by design: NO finalize cmd.<br/>Do NOT block / retry / gate on 0x86.
}
Session --> Committed: release USB handle →<br/>CLEAN POWER-BUTTON shutdown<br/>→ printhead parks → EEPROM flush
Committed --> [*]: reboots NORMAL (04a9:1865)<br/>5B00 cleared, state = idle
note right of Committed
Commit is the clean power-off, NOT any
command. An abrupt UNPLUG skips the
park + EEPROM flush → 5B00 returns.
end note
Methodology trifecta — trace ⟷ decompile ⟷ correlate (Mermaid)¶
flowchart LR
%% Methodology TRIFECTA — the trace ⟷ decompile ⟷ correlate loop
%% Source of truth: docs/research/canon-service-mode-field-guide.md
%% (Frida + usbmon + Ghidra, usbprint.sys decompile, ground-truth correlation),
%% memory/canon-5b00-wicreset-pivot.md (the validated narrative).
%% Render: just diagrams (mmdc -> methodology-trifecta.svg)
%%
%% Three independent evidence sources, cross-correlated by timestamp, converge on
%% the validated native reset. No single lane is sufficient; each anchors the others.
subgraph USBMON["LANE 1 — usbmon (host wire ground truth)"]
direction TB
u1["QEMU per-device pcap / dumpcap -i usbmon<N><br/>over the 04a9:12fe passthrough"]
u2["Decodes URBs in Wireshark:<br/>VENDOR_SET 0x220038 / VENDOR_GET 0x22003c<br/>bRequest 0x81/0x82/0x85/0x86 + in/out payloads"]
u3["Proves: set_session 81 00 00 03 (plain),<br/>live keyword e4 7c 5a,<br/>set_command 85 00 00 || payload(20)"]
u1 --> u2 --> u3
end
subgraph FRIDA["LANE 2 — Frida (Win11 VM, IOCTL + DRM instrumentation)"]
direction TB
f1["Hook kernel32!DeviceIoControl on printerpotty.exe<br/>→ plaintext in/out buffers + IOCTL code + ts"]
f2["Patch the 3 cloud-DRM gates JZ→JMP so WICReset<br/>emits its OWN genuine reset (gate-only cloud)"]
f3["Recovers the app command frame BEFORE the wire<br/>+ the live 3-byte keyword off get_keyword(0x82)"]
f1 --> f2 --> f3
end
subgraph GHIDRA["LANE 3 — Ghidra (offline decompile)"]
direction TB
g1["usbprint.sys 10.0.26100.8328 →<br/>IOCTL→URB field map (VENDOR_SET = 0x41 OUT,<br/>whole frame as data stage; wIndex = iface)"]
g2["printerpotty.exe → clearCounters subtree is<br/>NET-FREE (BFS); QUERY_KEYS reply → 1 bool;<br/>cloud feeds NO payload/keyword/completion byte"]
g3["devices.xml (3DES-EDE3 zero-key, decrypted) →<br/>CANON-SR5 method=3 functor tables → write cipher"]
g1 --> g2 --> g3
end
CORR{{"CORRELATE<br/>by wall-clock timestamp<br/>+ the deterministic 20-byte envelope anchor"}}:::corr
u3 --> CORR
f3 --> CORR
g3 --> CORR
CORR --> validated["<b>VALIDATED NATIVE RESET</b><br/>write cipher reproduces WICReset's genuine<br/>frame 23/23 byte-exact · cloud-INDEPENDENT<br/>· cleared 5B00 on hardware (commit d2f3c81)"]:::win
%% the cross-anchoring that makes it a loop, not a pipeline
g3 -. "cipher predicts the wire bytes" .-> u3
u3 -. "wire confirms / corrects the cipher" .-> g3
f3 -. "live keyword + plaintext frame" .-> g3
g2 -. "tells Frida which gates to patch" .-> f2
classDef corr fill:#fff4d6,stroke:#b9770e,stroke-width:2px,color:#7d5109;
classDef win fill:#dff5e1,stroke:#1e8449,stroke-width:2px,color:#145a32;
Exploit / data-flow (Graphviz)¶
Cloud-DRM bypass — control flow (Graphviz)¶
Rendering¶
The recipe resolves the renderers at run time so nothing extra is vendored:
- Mermaid (
.mmd) via the Mermaid CLImmdc. If not onPATH, the recipe falls back tonpx --yes @mermaid-js/mermaid-cli. - Graphviz (
.dot) viadot. Install withnix profile install nixpkgs#graphviz(or your platform package manager) if it is not already present.
Manual one-offs (what the recipe runs under the hood):
mmdc -i docs/diagrams/lifecycle.mmd -o docs/diagrams/lifecycle.svg
dot -Tsvg docs/diagrams/exploit-dataflow.dot -o docs/diagrams/exploit-dataflow.svg
Accuracy notes (so the diagrams stay honest)¶
- Transport is usbprint VENDOR control on EP0, never bulk:
VENDOR_SET(IOCTL0x220038) =bmRequestType 0x41OUT,bRequest = frame[0], data = the whole frame verbatim;VENDOR_GET(0x22003c) =0xC1IN. Decompiled fromusbprint.sys10.0.26100.8328 (docs/research/canon-service-mode-field-guide.md). - set_session is PLAIN
81 00 00 03(4 bytes); the enciphered 8-byte form stalls. get_keyword returns a live 3-byte keyword — the only runtime input. - set_command is one 23-byte frame
85 00 00 || payload(20), NOT aprefix || 4-byte-keywordform and NOT a select+clear concatenation. The write cipher is functor-2 with roles swapped: SUBJECT = the 20-byte functor-3 envelope, SEED = the 4-byte bound keyword. Reproduces WICReset's genuine frame 23/23 byte-exact (docs/runbook/g6020-native-reset.md§8). - get_command (0x86) is EMPTY by design — there is no finalize command. Do not gate on it.
- The commit is a clean power-BUTTON shutdown (printhead park + EEPROM flush), NOT an unplug.
- Cloud is licensing-only. The reset is cloud-INDEPENDENT:
clearCountersand its whole subtree are net-free;QUERY_KEYScollapses to one bool; no cloud byte feeds the payload, keyword, or completion (docs/research/canon-service-mode-field-guide.md). - The 3 DRM gates patched in
drm-bypass-controlflow.dotare exact VAs/bytes forprinterpotty.exesha256a199447db…564b3e8only:0x44012d(RESET_GUID),0x44054a(QUERY_KEYS transport),0x440563(valid-bit) — each74 → EB.