A Mach-O object file is a file format used for executables, libraries, object code, and core dumps. These are binary files. There’s a Mach-O header and then load commands and segments of up to 255 sections with references to symbols encoded into objects and symbol names. Many of those symbols are APIs that Apple makes available that the code uses. We can see those APIs by extracting a list of symbols, but not really the logic underlying it. Tools like Hopper Disassembler can be used to look at these files and extract symbols, or a command like nm.
Per the man page of nm, “nm displays the name list (symbol table of nlist structures) of each object file in the argument list.,” The basics of compiler programming aside, let’s take a basic task: show all the symbols used in a binary compiled for a Mac (or a mach-O object file). We can use the nm command to extract a list of symbols used as follows:
nm /Applications/Little\ Snitch.app/Contents/MacOS/Little\ Snitch
This can be useful to find what behaviors might be supported by a given binary, those that leverage deprecated APIs, etc. However, nm fails if the file isn’t a mach-O object file. So prior to checking we can use the file command to make sure a file is a Mach-O binary.
file /Applications/Little\ Snitch.app/Contents/MacOS/Little\ Snitch
The output is as follows:
/Applications/Little Snitch.app/Contents/MacOS/Little Snitch: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]
/Applications/Little Snitch.app/Contents/MacOS/Little Snitch (for architecture x86_64): Mach-O 64-bit executable x86_64
/Applications/Little Snitch.app/Contents/MacOS/Little Snitch (for architecture arm64): Mach-O 64-bit executable arm64
Notice that after the path to a file name passed and then a :, that Mach-O is the first string that appears.
A reason we’re just checking that it’s Mach-O after the path to the file is that different Mach-O types have different outputs, but they can all work with nm. So nm /somedocument
throws an error whereas nm /usr/bin/zprint
(as an example) outputs a list of symbols.
Once we have a list of symbols, we could extend our use a bit further to find all files that use certain symbols by recursively checking all directories from a given working directory to list all files with a given string in the symbol list. For example, the first symbol for zprint is _CFDictionaryApplyFunction so we’d run a script to search for _CFDictionaryApplyFunction – it would take a long time to run but would eventually output a list of binaries that call the _CFDictionaryApplyFunction symbol. While symbols and Mach-O may seem very specific it’s just basic string matching. So add some regular expression logic to a script to parse out those without the string passed in. If there’s any string that matches in the binary being checked (loaded then into an IFS array) then it’s a match and should be output, if it’s not a match, no output and move to the next file.
Let’s say we’re researching APIs or functions that are used in an endpoint security system extension. One function any endpoint security system extension will need is es_new_client. That means the compiled extension will show the _es_new_client (https://developer.apple.com/documentation/endpointsecurity/3259700-es_new_client) symbols when disassembled or inspected via nm. Therefore, it’s possible to recursively inspect all objects in /Applications or /Applications/Utilities to see if they call es_new_client. Given that nm looks for symbols in a given binary, we could then reverse the logic and look for binaries that use a given symbol. Let’s call our script mn.sh as this is a fairly straight-forward task with bash. I did a similar script a few years ago that looked for binary dependency that used the otool binary to crawl a directory tree and called it looto (otool spelled backwards). That’s at https://github.com/krypted/looto. So let’s take a mn reversal of nm and just use the same repo – so the script is now posted at https://github.com/krypted/looto/blob/master/mn.sh
Let’s run it to look for that _es_new_client symbol (although this example will take a very long time ’cause it’s looking at every file the existing shell can access :
sudo ~/mn.sh -r / _es_new_client
So general usage:
mn.sh [options: -r] [path] [search_param] [grep_flags]
The grep_flags include the flags of the grep utility – so for search case insensitively, run:
mn.sh $(pwd) str -I