Skip to content

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]&lt;&lt;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&lt;N&gt;<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)

Canon G6020 5B00 exploit / data-flow

Cloud-DRM bypass — control flow (Graphviz)

WICReset cloud-DRM bypass control flow

Rendering

just diagrams        # render every .mmd + .dot in this dir to SVG
just diagrams png    # also emit PNG

The recipe resolves the renderers at run time so nothing extra is vendored:

  • Mermaid (.mmd) via the Mermaid CLI mmdc. If not on PATH, the recipe falls back to npx --yes @mermaid-js/mermaid-cli.
  • Graphviz (.dot) via dot. Install with nix 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 (IOCTL 0x220038) = bmRequestType 0x41 OUT, bRequest = frame[0], data = the whole frame verbatim; VENDOR_GET (0x22003c) = 0xC1 IN. Decompiled from usbprint.sys 10.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 a prefix || 4-byte-keyword form 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: clearCounters and its whole subtree are net-free; QUERY_KEYS collapses 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.dot are exact VAs/bytes for printerpotty.exe sha256 a199447db…564b3e8 only: 0x44012d (RESET_GUID), 0x44054a (QUERY_KEYS transport), 0x440563 (valid-bit) — each 74 → EB.