Rust, Reverse Engineering

Reading time: minutes

What if I stored data in my mouse

It started with a dumb idea. 

I have a Logitech MX Vertical, which travels between my home machine, work laptop and other devices constantly. At some point I looked at it and thought: this thing has a flash memory. It has to, otherwise how does it remember the DPI setting between plugs. So what if I stored something in it?

Yea, I was bored.

The plan was to treat the mouse like a tiny USB drive. Since it physically travels between computers, it could technically carry data with it.

Logitech mice communicate over something called HID++, a protocol they built on top of standard USB HID. It's partially documented by Logitech, partially reverse engineered by the open source community. I wrote a Rust tool to enumerate every feature the mouse exposes. There are 33.

HID++ 2.0 works like this: every device has a feature table. Each entry maps a stable feature ID to a device-specific index. The ID is consistent across all Logitech devices. The index varies per model. So you first ask the device "where is feature 0x2201?" and it tells you "index 0x12 on this mouse". Then you use that index for all subsequent calls. Every call is a short packet: report ID, device index, feature index, function ID, and up to 3 bytes of params. The response comes back in the same shape.

Most are undocumented. There are names like EnableHiddenFeatures and TemplateBytesNVS. That last one sounds exactly like what I wanted: non-volatile storage, index 0x1eb0. I couldn't find any info on it in the docs, but I'm guessing it's something to do with configuration blobs for macros or button templates. Unfortunately, the mouse wouldn't respond to anything, so I couldn't find out.

I spent a while on 0x1c00 (the PersistentRemappableAction) first. Six slots, read/write flags, and so on. Turns out macOS's IOHIDManager silently blocks the longer HID++ report format you need to actually write to it. The OS just drops the packets. There was no error, no explanation, nothing. I found this out after writing a pile of probe code and staring at empty responses for longer than I'd like to admit. Yes, there are ways around IOHIDManager, like talking directly to the USB device via IOKit, but that's a different story.

Then I looked at the device name register. It looked promising and the write calls were accepted. But the mouse kept saying "MX Vertical" regardless of what I sent. It was just ghosting me.

What did worked was the DPI register (0x2201).

You can read two values from it:

  • the current (active) DPI
  • a fallback/default DPI

Setting DPI works via function 0x03, and reading via 0x02.

Here's the important (and updated) part:

  • Writing DPI only updates the active DPI
  • The fallback/default DPI remains unchanged
  • On power cycles, the device restores the fallback value

So while the DPI register accepts arbitrary u16 values, they are not persisted across power cycles.

However, there is one interesting property:

As long as the mouse stays powered, the value survives switching computers. You can write a value on one device, move the receiver to another, and read it back.

So the "storage" is only:

2 bytes of cross-computer, session-scoped storage (while the mouse remains powered)

So the original idea: "using DPI as persistent storage" doesn't actually hold up. The only solution would be to "cache" and rewrite it on the mouse on power cycles, but that would defeat the idea.

I also tested a range of other features and write paths (including 0x1e, 0x1f, and several 0x18xx config features). None of them affected the fallback DPI or exposed writeable persistent storage.

At this point, it looks like the default DPI is either:

  • stored in a part of firmware not exposed via HID++, or
  • only writeable through vendor tooling / signed commands

Even though the original goal didn't work out, the process was still worth it.

I learned how HID++ works. I learned how macOS manages HID devices at kernel level and where it draws lines. I learned that "feature table" and "feature index" are different things and that the IFeatureSet reverse lookup is apparently broken on this device. I learned that the device name register will politely accept your writes and then completely ignore them, which is a deeply relatable response.

None of the knowledge came from reading docs. The Logitech hidpp20 docs exist, but half the features are missing, so I had to try it out. It all was trial and error until it worked. But maybe don't do that with your BIOS.

The result is objectively useless. No hidden storage, no secret USB drive in a mouse. But that wasn't really the point. The point was seeing how far I could get.

The 2 bytes are still there. You just have to keep the mouse alive…

I will dig deeper though, because the default DPI has to be somewhere in my mouse. 

Code is here if you want to explore it yourself: https://github.com/timwehrle/mouse-fs.