Foreword

TLDR;

A lot has changed since I last wrote on this blog. Recently, a part of my team at Postman was laid off, and our main project was halted. It was a devastating experience for me, as it felt like abandoning a project I had dedicated myself to for a year and a half.

This isn’t the first time I’ve faced such a situation. Reflecting on how I overcame similar challenges in the past, I sought ways to transform this negative energy into something positive.

Working on a cross-platform framework was immensely enjoyable and made me realize my passion for systems programming and macOS internals. However, I also recognized that opportunities to work on runtimes or frameworks are quite rare, and jobs focusing on macOS application are almost non-existent. I took a step back to evaluate my career and asked myself: “How can I find a more fulfilling direction for my research and work?”

So, I took a piece of paper and wrote down all the components that would make up my ideal job:

  • Being in the systems programming side (no more web)
  • Exploring macOS internals
  • Research should be in the DNA of the job

This introspective exercise led me to the idea of exploring security for macOS application. as it perfectly intersects all my interests.

This long preamble is to explain that, moving forward, the focus of this blog will shift towards reverse engineering and macOS internals; essential pillars of my new journey.

Intro

As a starting point for this new journey, I decided to go through Advanced Apple Debugging & Reverse Engineering, originally written by Derek Selander. This post serves as a way to revise what I’ve read and as a reference for the future.

In this post, we will do an introduction to the usage of LLDB for reverse engineering. For the sake of this post, I will share with you a dummy application and our mission will be to crack the license.

By the end of this post you should be able to:

  • Attach a macOS application to LLDB
  • Inspect and lookup symbols (for debug build)
  • Create breakpoints
  • Modifying a register

Note: This post is not about attacking a real world application, but rather a pragmatic way to learn reverse engineering with LLDB.

I hope that you’ll enjoy this journey!

Pre-requisite

Before moving on next step, you should double check that you have on your marchine:

To perform certain debugging and reverse engineering tasks on macOS, you might need to disable System Integrity Protection (SIP). SIP is a security feature that restricts the root user’s abilities to modify certain protected areas of macOS. For educational and testing purposes, you can disable SIP on a virtual machine (VM) to avoid affecting your primary system’s security.

Step 1: Attach your application with LLDB

Open your favorite terminal application and start LLDB.

lldb

Now your are in lldb. you should see something like:

❯ lldb
(lldb)

Now we need to create what we call a target:

(lldb) target create /path/to/CrackThelicenseDebug.app

In the LLDB debugger, a “target” refers to the executable file that you want to debug. Defining a target in LLDB involves specifying the path to the executable you intend to debug.

Now we can launch the process by running:

(lldb) process launch

Note: Alternatively, you can attach your application by using lldb <path/of/your/application> or lldb -p <pid> and then run run` command in LLDB.

The application should appear:

launched demo application

If you try to pass a random password you’ll see the “Access denied” message. Let see how we could workarround this. (Note: by default the error message appear)

Step 2: Inspecting symbols

A breakpoint is a fundamental feature of a debugger that allows developers to stop the execution of a program at a specific point. By setting breakpoints, developers can inspect the code, the stack, and the registers at that moment. Breakpoints can be set at specific lines of code, on certain conditions, or when specific symbols are encountered.

Let’s interrupt the execution of the application by pressing Ctrl+C. You should see something like this:

Process 22605 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x0000000181ae61f4 libsystem_kernel.dylib` mach_msg2_trap  + 8
libsystem_kernel.dylib`mach_msg2_trap:
->  0x181ae61f4 <+8>: ret
libsystem_kernel.dylib'macx_swapon:    0x181ae61f8 <+0>: mov    x16, #-0x30
    0x181ae61fc <+4>: svc    #0x80
    0x181ae6200 <+8>: ret
libsystem_kernel.dylib'macx_swapoff:    0x181ae6204 <+0>: mov    x16, #-0x31
    0x181ae6208 <+4>: svc    #0x80
    0x181ae620c <+8>: ret
libsystem_kernel.dylib'thread_get_special_reply_port:    0x181ae6210 <+0>: mov    x16, #-0x32
Target 0: (CrackThelicense) stopped.
(lldb)

Behind the scenes, it sends a SIGSTOP signal to the process. The execution has stopped in a system call mach_msg2_trap, which is part of the libsystem_kernel.dylib shared library. We did this, to be able to use LLDB commands: we cannot add breakpoints during the execution of the program.

Now we’d rather stop the process when the application checks the license key. The first tool we could use is image lookup, which “looks up information within executable and dependent shared library images.

Did you know that you can access LLDB documentation by runinng the help command. For instance, try to run help image lookup and see what it does!

By impersonating an attacker, you might try to look up symbols that contain words like “check” or “license”… Once we will have the symbol we would be able to set the breakpoint.

Let’s try with “check” word:

(lldb) image lookup -rn check

I would totally understand if you’d rather close this tab and run away forever! But let’s try to explain what’s happening here. When you run the image lookup command with the -rn flag, it searches the symbol table for functions containing the word “check” across all libraries linked to your binary.

Obviously, it is a nightmare to process this output. We know that we don’t care about all the shared libraries; what we want is to look up symbols for the application itself.

Since you don’t have access to the source code, we want to know all targets loaded. There’s a perfect command for this: image list:

List current executable and dependent shared library images.

On the top of the list you should be able to see:

[  0] 1132E96C-BD61-35EF-BA0E-0D6C64B6C5A4 0x0000000100000000 /macos-lldb-reverse-engineering/CrackThelicenseDebug.app/Contents/MacOS/CrackThelicense

You could have deduced this CrackThelicense by looking at the folder structure, but image list is another cool command to know :)

Now, we can use this library name to filter entries:

🎉 TADA! There is a match!

(lldb) image lookup -rn check CrackThelicense
1 match found in /Users/tonygo/Desktop/macos-lldb-reverse-engineering/CrackThelicenseDebug.app/Contents/MacOS/CrackThelicense:
        Address: CrackThelicense[0x0000000100005120] (CrackThelicense.__TEXT.__text + 5184)
        Summary: CrackThelicense`CrackThelicense.ContentView.checkLicense(key: Swift.String) -> Swift.Bool at ContentView.swift:41

Way more better. There is a checkLicense that seems to be what we were looking for.

Step 3: Setting Breakpoints and Modifying Execution

Now that we have the symbol, we can set a breakpoint.

(lldb) b CrackThelicense.ContentView.checkLicense(key: Swift.String) -> Swift.Bool
# Output:

Breakpoint 1: where = CrackThelicense`CrackThelicense.ContentView.checkLicense(key: Swift.String) -> Swift.Bool 
    at ContentView.swift:41, address = 0x0000000100005120

A breakpoint number “1” has been created. Keep that in your mind for later.

Now we can, proceed, and runing the continue or c command:

(lldb) c

You can go to the application, type a randon license key and click on the “submit” button:

cracked application

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100005120 CrackThelicense` ContentView.checkLicense(key=Swift.String @ 0x000000016fdfde60, self=CrackThelicense.ContentView @ 0x00000001ccafac64)  at ContentView.swift:41
   38  	        .padding()
   39  	    }
   40
-> 41  	    func checkLicense(key: String) -> Bool {
   42  	        return key == "ABC123"
   43  	    }
   44  	}
Target 0: (CrackThelicense) stopped.

You can see that the breakpoint number 1 is hitten (the one we created before).

Then, use the next (or n) command to step through the instructions:

You should see this:

(lldb) n
Process 8604 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x0000000100005160 CrackThelicense` ContentView.checkLicense(key="adsasddada", self=CrackThelicense.ContentView @ 0x0000600000f8ebd0)  + 64 at ContentView.swift:42
   39  	    }
   40
   41  	    func checkLicense(key: String) -> Bool {
-> 42  	        return key == "ABC123"
   43  	    }
   44  	}
Target 0: (CrackThelicense) stopped.

Do it again!

(lldb) n
Process 8604 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x0000000100005084 CrackThelicense` closure #1 in closure #1 in ContentView.body.getter(self=CrackThelicense.ContentView @ 0x0000600000f8ebd0)  + 164 at ContentView.swift:22
   19  	                .textFieldStyle(RoundedBorderTextFieldStyle())
   20  	                .padding()
   21  	            Button(action: {
-> 22  	                if checkLicense(key: licenseKey) {
   23  	                    accessGranted = true
   24  	                }
   25  	            }) {
Target 0: (CrackThelicense) stopped.

We reach the point where the function determines the return value. This the time to modify the return value. Yes, we can do that with LLDB!

Step 4: Modify registers

The first thing to now, it that you can inspect registers with register command:

(lldb) register read # the command
General Purpose Registers:
        x0 = 0x0000000000000000
        x1 = 0xea00000000006164
        x2 = 0x0000333231434241
        x3 = 0xe600000000000000
        x4 = 0x0000000800000003
...

When you set a breakpoint and the execution halts in LLDB, the registers show the state of the CPU at that precise moment.

LLDB offers you the ability to modify these registers. For instance, if you want to modify the third register to write the value 12 you can do:

(lldb) register write x3 12

There is a convention for ARM chips that dictates how functions receive parameter and how they return values. For instance for arm64, when it comes to registers x0 to x7”:

“Argument values passed to and results returned from a subroutine.” (link)

In our context the first reigister x0 is used to store the return value.

x0 = 0x0000000000000000

As we typed a wrong license key, this is totaly find to have false aka 0 aka 0x00000000. But as I said earlier with LLDB you can modify these registers:

(lldb) register write x0 1

Here you asked LLDB to change the value 0x to 1 aka true. Let’s check that it worked:

(lldb) register read x0
      x0 = 0x0000000000000001

Now, if you press c/continue, the execution should resume and TADA 🎉:

cracked application

If you’ve made it this far, congratulations! You’ve successfully navigated through the world of LLDB, setting up breakpoints, inspecting registers, and even cracking a basic application’s license.

As a next step, we could do the same but with a release build no?