Logo
blank Skip to main content

How to Use LLDB to Reverse Engineer Undocumented macOS Functions

API

Your team is likely to face a few operating system–specific challenges when aiming to create efficient and competitive macOS software. For example, adding certain logic via regular means might seem impossible, as there are no documented APIs that can aid with the implementation.

Apple provides first-party system tools containing functionality that can be relevant for developers trying to implement similar functionality. But since such tools aren’t open-source, finding out exactly how a certain first-party tool works is not an easy task. This is where reverse engineering techniques come into play.

In this article, we explore how to reverse engineer undocumented macOS functions using LLDB and show two practical examples. This article will be helpful for product managers and development leaders working on macOS projects that involve low-level solutions and integration of logic that isn’t officially documented or otherwise well-known among the development community.

Reversing undocumented macOS functions: reasons and tools

Reverse engineering can help your team look inside the closed functionality of macOS and find unique and unobvious solutions for development challenges they encounter – both legally and ethically.

Reversing undocumented macOS functions might be necessary in the following cases:

Reasons to reverse engineer undocumented macOS functions
  • Implement non-standard logic. Some publicly accessible macOS APIs can be extremely limited compared to their private counterparts. Careful reversing can help your team better understand system behavior, provide unique capabilities that are otherwise inaccessible, and improve or optimize existing logic.
  • Improve software compatibility. Your developers might need to integrate third-party applications that use undocumented functions. Better understanding these functions via reverse engineering can facilitate improved compatibility and feature integration.
  • Understand system behavior. Reversing undocumented functions helps you comprehend how or why things happen within the system, and this allows your developers to adapt your software accordingly. Understanding the behavior of underlying code is also crucial for efficiently debugging and fixing various issues. When it comes to older codebases that may use undocumented functions, understanding the system helps developers maintain legacy solutions without having access to their original source code.
  • Optimize work of APIs. Using undocumented APIs directly can lead to better performance compared to relying on system utilities or higher-level abstractions. For instance, calling a system API directly can minimize overhead and improve responsiveness in applications.

To start reversing undocumented macOS features, your team can use different approaches and tools. There’s not even a need to look for specific macOS reverse engineering tools, as you might go for a regular disassembler like IDA, Hopper, or Ghidra, or choose dynamic tools like Frida or LLDB.

Common tools for reversing undocumented macOS functions

For the purpose of this article, we’ll use LLDB, so let’s take a closer look at this tool.

What is LLDB?

LLDB is an immensely powerful free and open-source debugger. It has even become the default debugger for macOS and iOS since Xcode 5.0.

We use LLDB because it can attach to anything. Thus, with some effort, we can find out exactly how any application’s functionality works. Of course, fully decompiling an application with only LLDB would take a considerable amount of time. Therefore, for complex tasks, we recommend using a combination of the tools mentioned above to save time and increase efficiency.

Below, we show two examples of using LLDB to reverse undocumented macOS functions that are responsible for:

  1. Making an app run in the background
  2. Finding xpc messages from first-party system tools

Note: All examples we show are for research purposes only.

Looking to deliver a competitive macOS app?

Entrust Apriorit’s experts with any task, from software development and testing to reverse engineering and security audits!

Using LLDB to discover how to make an app run in the background

Say we are developing a macOS application and want it to be able to hide and run in the background. To do that, we can use Apple’s lsappinfo command tool by:

  • Running it with the setinfo command
  • Giving it the PID, ASN, or name of the process and the parameters we want to set for the process

You can use the lsappinfo tool for multiple purposes, as it can set and read lots of different parameters. For example, to make an app only run in the background, we would execute something like this:

ShellScript
sudo lsappinfo setinfo "Xcode" "ApplicationType"="BackgroundOnly"

But now we need to know how lsappinfo works under the hood.

Note: Undocumented functionality is usually undocumented for a reason. In this case, that reason is to avoid potential volatility of your system or APIs, as even a minor system update can strongly influence any of the function’s logic – or break it completely.

To begin the reverse engineering process using LLDB, we need a starting point. For this example, we’ll use nm /usr/bin/lsappinfo with the path to our tool. If you don’t know where your tool is located, run whereis <tool_name> to get the full path.

At this point, we might get thousands of lines of lsappinfo’s symbol table to go through, finding ourselves in need of some other disassembly tool (Hopper, IDA, or Ghidra) to find the function we require. But for simplicity’s sake, let’s say we just happened to find the function we needed when examining the thousands of lines given to us by the nm command: the _LSSetApplicationInformation function. From here, we can start the dynamic reverse engineering process.

Note: The _LSSetApplicationInformation function requires root privileges. The same applies for lsappinfo, where setinfo only works under sudo privileges. To attach LLDB to any process, we need to turn SIP off; otherwise, the system won’t let us attach LLDB.

First, let’s make sure the function we found was indeed what we were looking for. Below, we show how to use LLDB to set a breakpoint on this function and run the lsappinfo tool using parameters of the setinfo command. Also, make sure that your target application is launched before calling lsappinfo using setinfo.

ShellScript
lldb
(lldb) file /usr/bin/lsappinfo
Current executable set to '/usr/bin/lsappinfo' (arm64e).
(lldb) b _LSSetApplicationInformation
Breakpoint 1: where = LaunchServices`_LSSetApplicationInformation, address =
0x00000001809893c0
(lldb) process launch setinfo "Xcode" "ApplicationType"="Foreground"
Process 2004 launched: '/usr/bin/lsappinfo' (arm64e)
Process 2004 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x000000019d5f13c0 LaunchServices`_LSSetApplicationInformation LaunchServices`:
-> 0x19d5f13c0 <+0>: pacibsp
0x19d5f13c4 <+4>: sub sp, sp, #0xa0
0x19d5f13c8 <+8>: stp x26, x25, [sp, #0x50]
0x19d5f13cc <+12>: stp x24, x23, [sp, #0x60]
Target 0: (lsappinfo) stopped.

As we can see from the code above, a breakpoint is hit on our candidate function. Now, we need to find out which arguments the lsappinfo tool passes to this function so we can pass the same arguments in our code. To do that, let’s call the register read lldb command:

ShellScript
() register read
General Purpose Registers:
x0 = 0x00000000fffffffe
x1 = 0x0000000000000000
x2 = 0x0000600001d75d20
x3 = 0x000000016fdfe628
x4 = 0x0000000000000004
x5 = 0x0000000000000480
x6 = 0x0000000000000000
x7 = 0x000000019d307cfd
CoreFoundation`_OBJC_$_INSTANCE_METHODS_NSArray + 765
x8 = 0x0000000000000000
x9 = 0x4335ae2177540027
x10 = 0x007ffffffffffff8

Most of the time, function parameters should be stored somewhere between registers x0 and x7. Just to be safe, we’ve provided the output for the first ten registers. Thus, if we are lucky, we can figure out what data is stored within them. Fortunately, LLDB can help us with this:

ShellScript
(lldb) print (void *) $x0
(void *) 0x00000000fffffffe
(lldb) print (void *) $x1
(__NSCFType *) 0x0000600002c351a0
(lldb) print (void *) $x2
(__NSDictionaryM *) 0x0000600002c35040 1 key/value pair

From the output above, we can say that at least one of the arguments is a dictionary; the other is unknown to us for now. Let’s try getting the values stored at those addresses:

ShellScript
(lldb) po $x0
4294967294

The output doesn’t seem extremely helpful, as it looks like this parameter is a simple numeric value that doesn’t change under any circumstances. Let’s try other registers:

ShellScript
(lldb) po $x2
{
ApplicationType = Foreground;
}

It looks like the arguments we’ve passed to the setinfo command are then passed to the _LSSetApplicationInformation function via a dictionary. And this dictionary is the third argument passed to the function, because it’s stored in the x2 register. So, we can start preparing the logic to call the function of interest. In this example, we’re using Swift with bridging headers to call C functions, but you can use other languages for this task as well:

Swift
// Create a Swift dictionary with key-value pairs
let dictionary: [AnyHashable: Any] = [
"ApplicationType" as NSString: "BackgroundOnly" as NSString,
]
// Cast NSDictionary to CFDictionaryRef
let cfDictionary = nsDictionary as CFDictionary

Now, let’s go back to the x1 register. Here, we see the  __NSCFType object, which refers to an internal Core Foundation (CF) or Foundation framework object. Let’s retrieve its description:

ShellScript
(lldb) po $x1
LSASN:{hi=0x0;lo=0x54054}

As we can see, the  __NSCFType is an LSASN-type object. Such objects provide another way for macOS to identify processes, apart from the regular PID approach. hi=0x0;lo=0x54054 is a unique autonomous system number (ASN), which the LaunchServices framework gives to each running app. And if we get information about Xcode via the lsappinfo tool, we can see that this information is the exact same ASN as the one given to our function of interest, only presented in a different way by deconstructing it to hi and lo parts.

Below, we provide a limited output from the info command. You can find even more information about any given process using the lsappinfo tool:

ShellScript
lsappinfo info "Xcode"
"Xcode" ASN:0x0-54054:
bundleID="com.apple.dt.Xcode"
bundle path="/Applications/Xcode.app"
executable path="/Applications/Xcode.app/Contents/MacOS/Xcode"

Read also

How to Reverse Engineer an iOS App and macOS Software

Take a closer look at the basic reversing process that can help your team significantly improve your software integration capabilities and easily maintain legacy code.

Learn more
reverse engineering

Now, we need to somehow pass this LSASN structure within our Swift code to our function of interest. While going through the output of the nm command and searching for ASN, we managed to find two potential functions of interest: __LSASNCreate and __LSASNCreateWithUInt64.

Setting the breakpoint at the __LSASNCreate function didn’t result in anything at all. Doing so with the __LSASNCreateWithUInt64 function showed that it’s called when setting the information for our app. Keep in mind that the symbol names within the binary have a leading underscore before the actual name, so we need to remove the second name when setting the breakpoint. Let’s go through a similar process of decompiling this function by reading the registers:

ShellScript
x0 = 0x0000000000000000
x1 = 0x0000000000054054
x2 = 0x9fd7025e8371b127
x3 = 0x0000000000000000
x4 = 0x0000600002ea8060
x5 = 0x0000000000000001
x6 = 0x0000000000000000
x7 = 0x0000000000000000
x8 = 0x000000019d317738 CoreFoundation`kCFAllocatorDefault
x9 = 0x000fffffffffffff
x10 = 0x0fffffffffffffff

Usually, we can only guess a function’s parameters during macOS reverse engineering. And we should start guessing from the beginning of registers. Obviously, we have the 0 value in the x0 register and some other currently unknown uint values. Let’s start testing with the following signature:

C
void* _LSASNCreateWithUInt64(UInt64, UInt64)

Here, we’re jumping a little bit ahead, calling this function with the same arguments we saw when working within the registers for our function of interest. However, it’s not obvious where the x1 value comes from. Looking through a backtrace of _LSASNCreateWithUInt64 function calls, we can see a call to the _LSCopyMatchingApplications function, and looking at the call trace again, it seems like this function should be just enough for us to get the application’s LSASN.

ShellScript
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x000000019d60b71c LaunchServices`_LSASNCreateWithUInt64
frame #1: 0x000000019d60b658 LaunchServices`invocation function for block in
copyCFArrayOfLSASNsWithRawASNsFromXPCObject(void*) + 64
frame #2: 0x000000019cdbbedc libxpc.dylib`xpc_array_apply + 96
frame #3: 0x000000019d60b5a4 LaunchServices`copyCFArrayOfLSASNsWithRawASNsFromXPCObject(void*) + 236
frame #4: 0x000000019d60b444 LaunchServices`_LSCopyMatchingApplications + 416

By following the same steps, we figured out that the _LSCopyMatchingApplications function takes the same numerical value as _LSSetApplicationInformation and a dictionary with the application name we gave to lsappinfo. So, we can try calling the _LSCopyMatchingApplications function using the previously discovered signature:

Swift
// Create the search criteria dictionary
let criteria: [String: Any] = [
"LSDisplayName": "Xcode"
]
// Create a variable with the "magic" value
let lsMagicValue: UInt64 = 4294967294
// Get the dictionary of matching applications
let dict = _LSCopyMatchingApplications(lsMagicValue, criteria as CFDictionary)

Now, since we’ve decided to use Swift for this example, we need to pass the LSASN object (that was returned by the _LSCopyMatchingApplications function) to our function of interest.

At this point, we’re done with the first three parameters passed to the LSSetApplicationInformation function. Other than these parameters, none of the registers seem to hold any data that could be of interest. However, if we still want to find more information about the arguments passed to this function, calling the disassemble command could be helpful, allowing us to find out exactly how this function works with the given registers:

ShellScript
disassemble --name _LSSetApplicationInformation

After running this command, we’ll see a huge amount of disassembled data. But to keep this article straight to the point, let’s say that we didn’t find other registers being used in any meaningful way. And we know the values within three of the registers already:

  • The first parameter is a number: 4294967294
  • The second parameter is an LSASN object
  • The third parameter is a dictionary: CFDictionary

Thus, we can determine that the signature of the function looks like this:

C
CFTypeRef _LSSetApplicationInformation(uint, void*, CFDictionaryRef);

Combining all the information we found earlier, we receive the following code:

Swift
// Create a Swift dictionary with your key-value pairs
let dictionary: [AnyHashable: Any] = [
"ApplicationType" as NSString: "BackgroundOnly" as NSString,
]

// Cast NSDictionary to CFDictionaryRef
let cfDictionary = nsDictionary as CFDictionary

// Create the search criteria dictionary
let criteria: [String: Any] = [
"LSDisplayName": "Xcode"
]

// Create a variable with the "magic" value
let lsMagicValue: UInt64 = 4294967294

// Get a dictionary of matching applications
let dict = _LSCopyMatchingApplications(lsMagicValue, criteria as CFDictionary)

// Cast pointers to get the returned value for swift usable objects
let cfArrayRef = dict?.takeRetainedValue()
let nsArray = cfArrayRef! as NSArray

// Get the pointer to the LSASN object we got earlier
let lsasn = nsArray[0] as! NSObject
let asnPtr = Unmanaged.passUnretained(lsasn).toOpaque()

// Call our function of interest with the asn and dictionary we created earlier
_LSSetApplicationInformation(lsMagicValue, asnPtr, cfDictionary)

Calling this code makes our application run in the background, removing its icon from the Dock completely.

Related project

Developing a Custom Secrets Management Desktop Application for Secure Password Sharing and Storage

Explore a real-life story of creating efficient and secure software that helped our client protect their valuable assets and improve the company’s overall security score by 30%.

Project details
Developing a Custom Secrets Management Desktop Application for Secure Password Sharing and Storage

Using LLDB to find xpc messages from first-party system tools

Sometimes, the logic of first-party tools is a bit more complex than simple function calls. It can involve communication with other system processes: for example, xpc messages or Apple Events. In this case, let’s look at how we can intercept xpc messages sent by a first-party system tool.

One way to do so is by using LLDB to reverse engineer the launchctl command-line utility tool that sends xpc messages. Say we want to find the message sent when calling the launchctl list com.apple.Spotlight command. For this, we need to attach to the launchctl process via LLDB:

ShellScript
(lldb) file /bin/launchctl

Then, we need to search for the function that sends the message of interest and put a breakpoint on it. In this case, it’s the _xpc_pipe_interface_routine function:

ShellScript
(lldb) b _xpc_pipe_interface_routine

Now, we call the command with the arguments we need:

ShellScript
run launchctl list com.apple.Spotlight

Then, instead of simply printing out the description, let’s call the (lldb) p printf("%s",(char*) command and pass the output of the xpc_copy_description Objective-C function to the printf("%s",(char*) command. That is going to give us a formatted string out of the xpc dictionary stored in the $x2 register:

ShellScript
(lldb) p printf("%s",(char*) xpc_copy_description($x2))
}<dictionary: 0x600001ac4000> { count = 5, transaction: 0, voucher = 0x0, contents =
"handle" => <uint64: 0xb927bf09f6092327>: 0
"name" => <string: 0x6000030c4090> { length = 19, contents = "com.apple.Spotlight" }
"type" => <uint64: 0xb927bf09f609231f>: 7
"legacy" => <bool: 0x209a99c70>: true
"domain-port" => <mach send right: 0x600003ec4000> { name = 2055, right = send, urefs = 5 }

In such cases, since a whole lot of xpc messages are sent by the launchctl process, we may not find the ones we need right away. Therefore, we might need to jump from breakpoint to breakpoint.

To sum up, when reverse engineering functions with LLDB, your team can call system functions with existing data to modify, print, or otherwise work with that data in order to improve the logic of the function you are trying to reverse engineer.

Read also

How to Reverse Engineer an Undocumented macOS API to Use It in a Swift Project

Find out how API reversing can help your team enhance product security, improve performance, and expand existing functionality with new services and features.

Learn more
Reverse Engineering an Undocumented macOS API

How Apriorit can help you with reverse engineering

At Ariorit, we care about keeping the balance between improving our clients’ software and making sure reverse engineering activities are ethical and legal.

Our experts apply reversing techniques for various tasks, including conducting research, improving cybersecurity, extending software capabilities, and enhancing integration opportunities. Additionally, we offer hardware reverse engineering services to make sure your software operates smoothly on chosen devices.

What Apriorit reverse engineers can do for you

With strong expertise in cybersecurity and reversing, our experts can help you with various tasks not only for macOS but for Windows and Linux as well. For example, we’ll gladly assist you with:

For each project and task, we carefully choose the most relevant tools and technologies, aiming to provide maximum value and respect your budget and deadlines.

Conclusion

Dynamic reverse engineering tools like LLDB debugger have a variety of useful applications in skilled hands. In this article, we showed you only one possible use case: reversing and analyzing undocumented functions within a macOS application.

Using LLDB for reverse engineering can help you with a variety of other challenging tasks, especially when combined with other reverse engineering tools.

However, such tasks can be tricky, requiring help from experienced and skilled engineers. At Apriorit, we have dedicated teams with deep expertise in reverse engineering and macOS development. Our specialists are ready to assist you with projects of any complexity.

Looking for niche experts in reverse engineering?

Enhance your project team with experienced reversing and cybersecurity developers from Apriorit to complete all of your tasks efficiently!

Have a question?

Ask our expert!

Michael-Teslia
Michael Teslia

Program Manager

Tell us about your project

Send us a request for proposal! We’ll get back to you with details and estimations.

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us