Advanced lldb tricks for Swift - Injecting and changing code on the fly

Advanced lldb tricks for Swift - Injecting and changing code on the fly

While Xcode provides several visual abstractions for lldb commands like adding breakpoints by clicking the lines of code and running by clicking the play button, lldb provides several useful commands that are not present in Xcode's UI. This can range from creating methods on the fly to even changing the CPU's registers to force a specific flow on the app without having to recompile it, and knowing them can greatly improve your debugging experience.

Not all Swift things are developed in Xcode -- things like the Swift Compiler or Apple's SourceKit-LSP work better through other means, and these means usually end up having you use lldb manually. Without Xcode to assist you, some of these tricks can prevent you from having to compile the app again to test certain changes.

Injecting properties and methods

You might already know po (short for "print object") -- the friendly command that is commonly used to print the contents of a property:

func foo() {
    var myProperty = 0
} // a breakpoint
po myProperty
0

However, po is much more powerful than that -- despite the name implying that it prints things, po is an alias for expression --object-description --, an argumented version of the more raw expression (or just e) command that makes the output more swifty:

e myProperty
(Int) $R4 = 0 // not very pretty!

Because it's an alias, po can do everything that e can. e is meant for evaluating expressions, which can range from printing properties to changing their values and even defining new classes. As a simple use, we can change the value of a property in the code to force a new flow without recompiling the code:

po myProperty
0
po myProperty = 1
po myProperty
1

Besides that, if you write po alone, you'll be able to write a multiline expression like this. We can use this to create completely new methods and classes inside our debugging session:

po
Enter expressions, then terminate with an empty line to evaluate:
1 class $BreakpointUtils {
2     static var $counter = 0
3 }
4 func $increaseCounter() {
5     $BreakpointUtils.$counter += 1
6     print("Times I've hit this breakpoint: \($BreakpointUtils.$counter)")
7 }
8

(Dollar signs are used here to indicate that these properties and methods belong to lldb, and not the actual code.)

The previous example allows me to call $increaseCounter() directly from lldb, which will add 1 to my "I can't handle this bug anymore" counter.

po $increaseCounter()
Times I've hit this breakpoint: 1
po $increaseCounter()
Times I've hit this breakpoint: 2

The ability to do this can be combined with lldb's ability to import plugins, which can considerably enhance your debugging experience. A good example of this is Chisel, a tool made by Facebook that contains lots of lldb plugins -- like the border command, which adds a bright border to an UIView so you can quickly locate it on the screen, and they all work through clever usages of e/po.

You can then use lldb's breakpoint actions to automatically trigger these methods whenever the breakpoint is hit. Combined with po's property changing abilities, you can create special breakpoints that will alter the app's flow for the test you're trying to do.

In general, all advanced breakpoint commands are extremely painful to write manually in lldb (which is why I'll avoid them in this article), but thankfully you can easily set breakpoint actions inside of Xcode:

v - Avoiding po's dynamic behavior

If you've used po for some time, you might've seen a cryptic error message like this in the past:

error: Couldn't lookup symbols:
$myProperty #1 : Swift.Int in __lldb_expr_26.$__lldb_expr(Swift.UnsafeMutablePointer<Any>) -> ()

This is because po evaluates your code by compiling it, and unfortunately there are still cases where this can go wrong even though the code you're trying to access is correct.

If you're dealing with something that doesn't need to be evaluated (like a static property instead of a method or closure), you can use the v command (short for frame variable) as an alternative to printing with po to instantly get the contents of an object.

v myProperty
(Int) myProperty = 1

disassemble - Breakpointing into memory addresses to change their contents

Note: The following commands are useful only in extreme cases. You won't learn a new Swift trick here, but you might learn something interesting about software engineering!

I got into reverse engineering by spelunking popular apps with a jailbroken iPad, and when you do that, you don't have the option to recompile code -- you need to change it on the fly. For example, if I can't recompile the code, how can I force the following method to go inside the isSubscribed condition even though I'm not subscribed?

var isSubscribed = false

func run() {
    if isSubscribed {
        print("Subscribed!")
    } else {
        print("Not subscribed.")
    }
}

We can this solve by playing with the app's memory -- inside any stack frame, you can call the disassemble command to see the full set of instructions for that stack:

myapp`run():
->  0x100000d60 <+0>:   push   rbp
    0x100000d61 <+1>:   mov    rbp, rsp
    0x100000d64 <+4>:   sub    rsp, 0x70
    0x100000d68 <+8>:   lea    rax, [rip + 0x319]
    0x100000d6f <+15>:  mov    ecx, 0x20
    ...
    0x100000d9c <+60>:  test   r8, 0x1
    0x100000da0 <+64>:  jne    0x100000da7
    0x100000da2 <+66>:  jmp    0x100000e3c
    0x100000da7 <+71>:  mov    eax, 0x1
    0x100000dac <+76>:  mov    edi, eax
    ...
    0x100000ec7 <+359>: call   0x100000f36
    0x100000ecc <+364>: add    rsp, 0x70
    0x100000ed0 <+368>: pop    rbp
    0x100000ed1 <+369>: ret

The neat thing here is not the command itself, but what you can do with this information. We're used to setting breakpoints to lines of code and specific selectors in Xcode, but inside lldb's console you can also breakpoint specific memory addresses.

We need to know a bit of assembly to solve this problem: If my code contains an if, then the resulting assembly of that code is certain to have a jump instruction. in this case, the jump instruction is 0x100000da0 <+64>: jne 0x100000da7, which will jump to the memory address 0x100000da7 if the r8 register (that is set in the previous 0x100000d9c <+60>: test r8, 0x1 instruction) is different than zero (so, true). As I'm not subscribed, r8 will certainly be zero, which will prevent that instruction from being triggered.

To see this happening and to fix it, let's first breakpoint and position the app at the jne instruction:

b 0x100000da0
continue
//Breakpoint hits the specific memory address

If I run disassemble again, the little arrow will show that we're at the correct memory address to begin the action.

-> 0x100000da0 <+64>:  jne    0x100000da7

There are two ways to solve this problem:

Approach 1: Changing the content of CPU registers

The register read and register write commands are provided by lldb to allow you to inspect and change the contents of CPU registers, and the first way to solve this problem is to simply change the contents of r8.

By being positioned at the jne instruction, register read will return the following:

General Purpose Registers:
       rax = 0x000000010295ddb0
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff508
       rdx = 0x0000000000000000
       rdi = 0x00007ffeefbff508
       rsi = 0x0000000010000000
       rbp = 0x00007ffeefbff520
       rsp = 0x00007ffeefbff4b0
        r8 = 0x0000000000000000

Because r8 is zero the jne instruction will not trigger, making the code output "Not subscribed.". However, this is an easy fix -- we can set r8to something that's not zero by running register write and resume the app:

register write r8 0x1
continue
"Subscribed!"

In regular day to day iOS development, register write can be used to replace entire objects in the code. If a method is going to return something you don't want, you can create a new object in lldb, get its memory address with e and inject it into the desired register.

Approach 2: Changing the instructions themselves

The second and possibly most insane way of solving this is by actually rewriting the app itself on the fly.

Just like with registers, lldb provides memory read and memory write to allow you to change the contents of any memory addresses being used by your app. This can be used as an alternative way to change the contents of an property on the fly, but in this case, we can use it to change the instructions themselves.

Two things can be done here: If we want to reverse the logic of that if instruction, we can either change test r8, 0x1 to test r8, 0x0 (so it checks for a false condition instead), or jne 0x100000da7 (jump not empty) to je 0x100000da7 (jump empty, or if !condition). I find the latter easier, so that's what I'm going to follow. If we read the contents of that instruction, we'll see something like this:

memory read 0x100000da0
0x100000da0: 75 05 e9 95 00 00 00 b8 01 00 00 00 89 c7 e8 71

This looks crazy, but we don't need to understand all of it -- we just need to know that the OPCODE of the instruction corresponds to the two bits in the beginning (75). By following this chart, we can see that the OPCODE for je is 74, so if we want to make jne become je, we need to swap the first two bits with 74.

To do this, we can use memory write to write the exact same contents to that address, but with the first two bits changed to 74.

memory write 0x100000da0 74 05 e9 95 00 00 00 b8 01 00 00 00 89 c7 e8 71
dis
...
0x100000da0 <+64>:  je     0x100000da7
...

Now, running the app will result in "Subscribed!" being printed.

Conclusion

While disassembling and writing to memory might be too extreme for daily development, you can use some of the more advanced lldb tricks to increase your productivity. Changing properties, defining helper methods and mixing them with breakpoint actions will allow you to navigate and test your code faster without having you recompile it.

Follow me on my Twitter (@rockbruno_), and let me know of any suggestions and corrections you want to share.

References and Good reads

Beyond po (WWDC 19)
Damn Vulnerable iOS App