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.
Contents:
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:
- 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.
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:
- Making an app run in the background
- 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:
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
.
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:
() 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:
(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:
(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:
(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:
// 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:
(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:
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.
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:
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:
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.
(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:
// 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:
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:
CFTypeRef _LSSetApplicationInformation(uint, void*, CFDictionaryRef);
Combining all the information we found earlier, we receive the following code:
// 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%.
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:
(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:
(lldb) b _xpc_pipe_interface_routine
Now, we call the command with the arguments we need:
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:
(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.
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.
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:
- Reversing undocumented APIs to smoothly integrate useful functionality in your solution, mitigating compatibility issues and preventing security risks.
- Enabling anti-debugging techniques to shield your software and infrastructure against potential attacks.
- Reversing proprietary file formats to ensure your solutionโs compatibility with closed file formats.
- Monitoring system calls to help you analyze system behavior, debug your application, improve performance, enhance security, and ensure compliance.
- Conducting dynamic analysis to better identify vulnerabilities and discover potential security issues in mobile and desktop applications.
- Reverse engineering firmware to research the way particular devices are built and ensure your software works smoothly on particular hardware.
- Creating custom plugins to provide your team with instructions for frameworks that arenโt yet supported by available reverse engineering tools.
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!