I wanted an app to manage my camera’s settings. The official app is iOS-only and doesn’t provide an easy way to set settings except on the camera itself. No API documentation exists. The Bluetooth protocol isn’t documented. The backup file format is also undocumented.
The sensible approach: reverse engineer it myself; a feat that would take a good amount of dedicated time.
What if I could do it without actually doing it; make claude do all the heavy lifting and I just prompt my way to success.
The Constraint
Going in, one thing was clear in my mind: I would not write code. Not a single function, not a quick fix, nothing.
If code needed to exist, claude would write it.
My job was everything else:
- Provide RE methodology and problem framing
- Run tests on physical hardware
- Collect logs and share them back
- Tell
claudewhen it was stuck (more on that later)
claude’s job:
- Dump and investigate symbols from the iOS app
- Write Frida hooks
- Analyze captured data
- Implement the tools, scripts, and actual program
I already knew how to reverse engineer things. What I wanted to test was whether an AI coding agent could execute the plan.
What’s a Characteristic?
Note
Starting this project I knew very little about Bluetooth Low Energy. I didn’t know what a “characteristic” was. Didn’t know services from descriptors. The whole protocol was a mystery. This project was actually done before Idle Signal Farmer and lead to some of the inspiration for exploring BLE more.
When starting this project the only thing I knew: Bluetooth communication is the key. That there are send and receive functions. Those functions have data payloads. If I can capture the payloads, surely claude can figure out the rest.
With a hope and a dream, that was the game plan and how it all started.
First Hooks, First Failures
The first prompt was straightforward:
“First analyze the .app, identify the symbols to hook for the send/recive of bluetooth data. Then write Frida hooks for this .app. I’ll run them to collect data.”
claude got to work. It pulled symbols from the binary, identified likely Bluetooth-related methods, and started writing JavaScript hooks. I handled the annoying parts: setting up the donor app, configuring the Frida dylibs, packaging everything so it would run on an iOS device.
The early hooks were rough. claude would write JavaScript, I’d run it, Frida would error out. claude would confidently propose a fix; the fix would also error out. Rinse, repeat. Left to its own devices, it would iterate on the same broken approach indefinitely. Knowing when to say “stop, try something else entirely” became part of my job.
But eventually, logs started flowing. The first output was a wall of hex that looked a bit like this:
[WRITE] 85B9163E-62D1-49FF-A6F5-054B4630D4A1 Data: 69 50 68 6f 6e 65 2d 34 31 33 35 00[WRITE] C52EDBCE-1FE2-4ECC-9483-907E6592BE9E Data: 0a 00[WRITE] F557D96B-8284-4667-8793-B971C1DECA2A Data: bb 64 00 00 32 34 35 36 37 38[NOTIFY] 0F36EC14-29E5-411A-A1B6-64EE8383F090 Data: cc 71 a3 00 ...That’s a lot of data and I had no desire to look through it myself.
“Find patterns in this log. What repeats? <This section> is roughly where the pairing happens. <This section is when I see a pairing code>. What changes between runs? What’s the sequence we need to decipher and understand to repro our own pairing handshake?”
claude grepped through the captures and found the structure. 0a 00 appeared before every pairing attempt. bb 64 showed up right after the device ID write. Some UUIDs were critical; most were noise.
The hooks evolved to label what we’d learned:
var CBPeripheral = ObjC.classes.CBPeripheral;Interceptor.attach(CBPeripheral["- writeValue:forCharacteristic:type:"].implementation, { onEnter: function(args) { var data = ObjC.Object(args[2]); var characteristic = ObjC.Object(args[3]); var uuid = characteristic.UUID().UUIDString();
console.log("[WRITE] " + uuid); console.log(" Data: " + formatHex(data));
// Patterns discovered from log analysis if (formatted.hex === "0a 00") { console.log(" → CONFIG HANDSHAKE"); } else if (formatted.hex.startsWith("bb 64")) { console.log(" → PAIRING COMMAND"); } }});As the hooks matured, claude built a list of “critical” characteristic UUIDs:
var criticalUUIDs = [ "85B9163E-62D1-49FF-A6F5-054B4630D4A1", // Phone name write "F557D96B-8284-4667-8793-B971C1DECA2A", // Device ID operations "C52EDBCE-1FE2-4ECC-9483-907E6592BE9E", // DateTime/crypto challenge "0F36EC14-29E5-411A-A1B6-64EE8383F090" // Crypto response];When a write hit one of these UUIDs, the hook would add extra analysis: byte-by-byte structure breakdown, expected vs actual values, hypothesis logging (“WATCH FOR NOTIFICATIONS AFTER THIS WRITE”). The hooks evolved from “log everything” to “understand the protocol.”
The Protocol Emerges
With data in hand, claude started piecing together the protocol. First discovery: what an unpaired camera advertises:
[BTManager] Discovered: RSSI: -45 { kCBAdvDataLocalName = "X100VI-214B"; kCBAdvDataManufacturerData = {length = 9, bytes = 0xd80401313231344201}; kCBAdvDataServiceUUIDs = ("804DAA8E-FFEB-4AB3-8E75-6EDD7303208D");}That 9-byte manufacturer data and service UUID became our “this is the camera” fingerprint. The pairing sequence turned out to be a multi-step dance:
- Read a 4-byte device ID from the camera
- Transform it (set bit 29:
id | 0x20000000) - Write the transformed ID back
- Send a phone name (12 bytes, null-terminated)
- Handle system pairing (6-digit code verification)
- Send datetime sync
- Send a crypto challenge (23 bytes, including a session counter)
- Re-discover services (new ones appear after authentication)
claude documented all of this with characteristic enthusiasm. One of its markdown files declared the protocol “1000% COMPLETE” with “PERFECT UNDERSTANDING ACHIEVED.” A bit much; the documentation was accurate though.
The phone name write was particularly satisfying to capture:
[CBPeripheral] Writing to: 85B9163E-62D1-49FF-A6F5-054B4630D4A1 0 1 2 3 4 5 6 7 8 9 A B 69 50 68 6f 6e 65 2d 34 31 33 35 00 iPhone-4135.Twelve bytes, null-terminated. This write triggers the iOS pairing dialog. The camera sees the phone name and initiates the 6-digit code verification.
The device ID transformation was even more interesting. claude wrote a unit test to verify the pattern:
func testDeviceIDTransformation() { let originalID: UInt32 = 0x12345678 let transformedID = X100VIProtocol.transformDeviceID(originalID)
// Bit 29 mask: 0x20000000 XCTAssertEqual(transformedID, 0x32345678) XCTAssertTrue((transformedID & 0x20000000) != 0)}Read 0x12345678, set bit 29, write back 0x32345678. That’s the handshake that tells the camera “I acknowledge your device ID.”
The session counter in the crypto challenge was a satisfying catch. Earlier analysis had assumed those bytes were padding zeros. They weren’t: an incrementing counter tracking connection attempts. Get that wrong and authentication fails silently. The kind of bug that would have taken hours to track down.
Something Actually Worked
In no time we went from nothing to the beginning of a working implementation. claude had written enough hooks to capture the full pairing flow, analyzed the captured data correctly, and produced protocol documentation that mostly matched what the device expected.
I hadn’t written any code. claude had done the symbol analysis, the hook development, the log parsing, and the documentation. My contribution was running tests, providing context, and occasionally pointing it in a different direction.
The foundation was solid. Time to see how far down this rabbit hole really goes.
Next: Part 2: Finding North — NSData struggle and GPS breakthrough