Build the mental model before touching anything.
Good investigations start with questions, not commands. Before running anything, write down what you already know.
- The app has a text field that accepts user input.
- It compares that input against an expected value.
- The expected value is never shown in the UI.
- For the comparison to work, the value must exist in memory when it runs.
- A string. The app compares text, so the expected value is text.
- It probably relates to the achievement name ("Process Observer").
- It lives in heap memory after the challenge screen loads.
- It only exists while the app is running.
That's a map. It's small, but it's enough to know where to look and what to look for.
Step 2
Attach to the running process.
Connect your device via USB. Open a terminal and start LLDB.
List connected devices and confirm yours appears.
D93D6CA8-5B2E-4A1C-9F3D-... iPhone connected
Select your device, then tell LLDB to wait for the app to launch and attach the moment it does.
(lldb) device process attach -n InstrumentationJourney --waitfor
# LLDB is now waiting. Open the app on your device. Process 1234 stopped * thread #1, stop reason = signal SIGSTOP
Open the app on your device. LLDB catches it at launch and pauses the process immediately. Let it resume.
InstrumentationJourney to appear and attach the instant it starts. You catch the app from the very beginning of its execution.
Step 3
Explore what the binary exposes.
image lookup searches the symbol table of the loaded binary. The -r flag enables regex matching, and -n searches by symbol name. Together, -rn lets you filter the entire symbol table using a pattern.
Start broad. Ask for everything inside the app module and look for names that sound relevant to the challenge.
The output is long. Scan it for words that relate to what the challenge is asking: things like secret, verify, unlock. Those are the kinds of names a developer would give to code that handles a hidden value and compares it against user input.
Narrow the search progressively. Start with what looks most unusual.
InstrumentationJourney`SecretAssembler.init() InstrumentationJourney`SecretAssembler.verify(_:) InstrumentationJourney`SecretAssembler.unlockCode.getter ...
A class called SecretAssembler. It has a verify method and something called unlockCode. That's a clear signal. verify is the comparison. unlockCode is what's being compared against.
Confirm it from another angle. Search for verify to see where the comparison happens.
InstrumentationJourney`SecretAssembler.verify(_:)
One result. The comparison lives in SecretAssembler. Now look specifically at unlock to find where the expected value is produced.
InstrumentationJourney`static InstrumentationJourney.SecretAssembler.unlockCode.getter : Swift.String
A getter named unlockCode that returns a String. That is what produces the value verify compares against. The investigation now has a clear target.
Step 4
Find the runtime address and intercept the function.
To set a breakpoint on a specific function, you need its address in memory at runtime. Adding -v to the lookup gives verbose output, which includes a Function: range field with exactly that.
Summary: InstrumentationJourney`static InstrumentationJourney.SecretAssembler.unlockCode.getter : Swift.String at SecretAssembler.swift:33 Function: id = {0x...}, name = "static InstrumentationJourney.SecretAssembler.unlockCode.getter : Swift.String" range = [0x000000010269adc8-0x000000010269b210)
The Function: range field shows the start and end addresses of the function in memory. The start address, 0x000000010269adc8, is where execution enters the getter. That is the address to use.
The address inside InstrumentationJourney[0x...] earlier in the output is the file offset, a fixed position within the binary on disk. It changes with ASLR at every launch and is not usable as a breakpoint address. The value in Function: range is the actual runtime address for the current session.
Set a breakpoint at that address using -a, which tells LLDB to place the breakpoint at a specific memory address.
(lldb) continue
Navigate to the challenge screen on your device. The getter runs the moment that screen initializes, and the breakpoint fires.
The process is paused at line 33, the entry point of unlockCode. LLDB stopped execution before the function ran. You're now inside a Swift frame, which means the expression evaluator has full access to Swift types and can call methods on the live process.
po (print object) evaluates a Swift expression against the running process and prints the result. Use it to call the getter directly.
That's the unlock code. Resume the process, type the code into the app, and complete the challenge.
po uses the language of the current frame. Stopped inside SecretAssembler.unlockCode.getter, LLDB is in a Swift context and can resolve Swift types. It evaluates the expression in the live process, assembles the string, and returns the result. You didn't modify anything. You just asked.
Reflection
What made this work?
Take a moment to look back at the steps.
We didn't modify the application. We didn't change any code. We didn't patch anything. We attached a debugger to a running process and used it to observe data that existed in memory but was never shown in the UI.
The reason this works is straightforward. For the application to verify your input, it must have the expected value available at the moment of comparison. That value has to live somewhere. A debugger gives you access to that memory.
The technique used here, finding a function through symbol search and intercepting it at runtime, is one of the most fundamental forms of runtime investigation. It doesn't require source code. It doesn't require modifying the binary. It only requires understanding that if a process produces a value, a debugger can observe it.
Today the hidden value was an unlock code. In real work, it might be a session token, a feature flag, an API key, or a piece of business logic that only activates under specific conditions. The underlying approach is the same. Attach, observe, understand.
That's what runtime instrumentation is built on.