Chapters

Hide chapters

Advanced Apple Debugging & Reverse Engineering

Fourth Edition · iOS 16, macOS 13.3 · Swift 5.8, Python 3 · Xcode 14

Section I: Beginning LLDB Commands

Section 1: 10 chapters
Show chapters Hide chapters

Section IV: Custom LLDB Commands

Section 4: 8 chapters
Show chapters Hide chapters

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

If you’ve read this far, you now have a solid foundation in debugging. You can find and attach to a process of interest, efficiently create regular expression breakpoints to cover a wide range of culprits, navigate the stack frame and tweak variables using the expression command.

It’s time to explore one of the best tools for finding code of interest through the powers of lldb. You’ve already seen it a few times, but in this chapter, you’ll take a deep dive into the image command.

The image command is an alias for the target modules subcommand. The image command specializes in querying information about code that lives within a module. “Module” is a generic term for executable code, like an executable or a shared library. Examples of shared libraries include frameworks like UIKit for iOS or dynamic libraries like libSystem.B.dylib. A module can apply to a shared library on disk or code that’s loaded into a process.

Listing Modules

You’ll continue using the Signals project. Fire up the project in Xcode and then build & run on a simulator.

Note: You might be wondering why you keep running on the simulator and not a device. It’s just easier and faster. You don’t have to worry about certificate permissions and the connection to the device and things like that. Also, the processor on your computer is, hopefully, faster than the one on your phone, so stopping and starting the app won’t take as long. If you want to run on device, most everything with the Signals app will work the same though. Go ahead.

At any point, suspend the program and type the following in lldb:

(lldb) image list

This command lists all the modules currently loaded. You’ll see a lot! For such a simple program to run in memory, all those modules had to be loaded into the process!

The start of the list should look something like the following:

[  0] 88B10840-E223-3B24-B89F-922AFF796077 0x00000001027ac000 /Users/lolz/Library/Developer/Xcode/DerivedData/Signals-bugxmcyqgcvqzfdfkgoztexuneae/Build/Products/Debug-iphonesimulator/Signals.app/Signals
[  1] 86A8BA48-8BB4-3B30-9CDA-051F73C74F44 0x0000000102a80000 /usr/lib/dyld
[  2] CC38080D-7C09-357A-B552-DB1B456781CE 0x00000001029dc000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
[  3] 4597B93E-E778-3BB5-BB66-E4344A5F2FDB 0x00000001c7be2000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libBacktraceRecording.dylib
[  4] 14F79286-A67E-30FE-B786-E5A978D6DB0D 0x00000001c7bf5000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libMainThreadChecker.dylib
...

Like some other commands, there’s a -b switch to make the output “brief”. Enter the following command:

(lldb) image list -b

Now you just get the names of each module.

The first module is the main executable, Signals. The second module is the dynamic link editor or, more simply, dyld. dyld is responsible for loading any code into memory and executes code well before any of your code has a chance to start running.

You can filter out modules by specifying their name. Type the following into lldb:

(lldb) image list Foundation

The output will look similar to the following:

[  0] E14C8373-8298-3E74-AFC5-61DDDC413228 0x00000001806ea000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation

This is a useful way to find information about just the modules you want. The output has a few interesting pieces to note:

  1. The module’s UUID prints first: E14C8373-8298-3E74-AFC5-61DDDC413228. The UUID is important for hunting down symbolic information and uniquely identifies the version of the Foundation framework.
  2. Following the UUID is the load address: 0x00000001806ea000. This identifies where the module loads into the executable’s process space.
  3. Finally, you have the full path to the module’s location on the disk.

Note: Some modules won’t be at the physical location they claim to be on the disk. This is likely because they’re part of the dyld shared cache, or dsc. dsc packs hundreds — sometimes thousands — of shared libraries together. Starting in macOS Monterey, Apple no longer includes the separated dynamic libraries on disk, leaving only dsc to explore if you’re not spelunking in memory. You’ll learn more about dsc at the end of this chapter.

Take a deeper dive into another common module, UIKit. Type the following into lldb:

(lldb) image dump symtab -s address UIKit
Symtab, file = /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit, num_symbols = 3 (sorted by address):
               Debug symbol
               |Synthetic symbol
               ||Externally Visible
               |||
Index   UserID DSX Type            File Address/Value Load Address       Size               Flags      Name
------- ------ --- --------------- ------------------ ------------------ ------------------ ---------- ----------------------------------
[    0]      0     Data            0x0000000000000fc8 0x00000001ac4ebfc8 0x0000000000000030 0x001e0000 UIKitVersionString
[    1]      1     Data            0x0000000000000ff8 0x00000001ac4ebff8 0x0000000000000008 0x001e0000 UIKitVersionNumber

This dumps all the symbol table information available for UIKit. Remember, from a code standpoint, UIKit is a wrapper for the private UIKitCore module. As you can see, the UIKit module doesn’t include many symbols due to the sharing of code for Mac Catalyst. Repeat the same action for UIKitCore:

(lldb) image dump symtab UIKitCore -s address

It’s more output than you can shake a stick at! This command sorts the output by the addresses of each function thanks to the -s address argument. The lldb command above is comparable to dumping the symbol table information via nm, but instead, it happens in memory. Although not applicable to iOS Simulator shared libraries, this is convenient because you won’t be able to run the nm command on libraries packed into the dsc since nm expects an actual file on disk.

The image dump output has a lot of useful information, but your eyes will likely hurt scrolling through UIKitCore’s symbol table in its entirety. You need a way to effectively query the UIKitCore module.

The image lookup command is perfect for narrowing your search. Type the following into lldb:

(lldb) image lookup -n "-[UIViewController viewDidLoad]"

You’ll get output similar to the following:

1 match found in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
        Address: UIKitCore[0x00000000004b9278] (UIKitCore.__TEXT.__text + 4943316)
        Summary: UIKitCore`-[UIViewController viewDidLoad]

This dumps out information relating solely to UIViewController‘s viewDidLoad instance method. The -n option searches for functions or symbols. On this computer, the viewDidLoad method is located at offset 0x00000000004b9278 of the UIKitCore file on the disk and is found in the UIKitCore.__TEXT.__text section of UIKitCore. The __TEXT.__text section is an R-X mapped section of memory where executable code typically lives. You’ll learn about the Mach-O components in the “Low Level” section of this book.

Typing the full -[UIViewController viewDidLoad] method can be a little tedious, and this can only dump out methods where you already know the name of the symbol.

This is where regular expressions come into play again. The -r option lets you perform a regular expression query. Type the following into lldb:

(lldb) image lookup -rn UIViewController

Not only does this dump out all methods containing the phrase “UIViewController”, but it also spits out results like UIViewControllerBuiltinTransitionViewAnimator since it contains “UIViewController”. You can be smart with the regular expression query, just like when you were setting breakpoints, to only spit out UIViewController methods. Type the following into lldb:

(lldb) image lookup -rn '\[UIViewController\ '

This is good, but what about categories? They come in the form of (+|-)[UIViewController(CategoryName) methodName]. Search for all UIViewController category methods:

(lldb) image lookup -rn '\[UIViewController\('

Searching for the presence of the parenthesis immediately following the Objective-C class name returns category methods for that particular class. Not only does this print out both public and undocumented APIs, but it also gives you hints to the methods the UIViewController class overrides from its parent classes.

Note: image lookup‘s output can be a little jarring to read when there are many search results. In the upcoming chapters, you’ll look at ways to manipulate lldb to improve readability via command aliases and using lldb’s Script Bridging interface. You can get a taste of what “pretty” output looks like with the following zinger:

script print("\n".join([i.GetSymbol().GetName() + "\n" for i in lldb.target.FindGlobalFunctions("\[UIViewController\(", 0, lldb.eMatchTypeRegex)]))

You can also limit your search queries to a specific module by appending the module name as the final argument to your search query. If you wanted to see all the implementations of viewDidLoad implemented in UIKitCore, you’d type the following:

(lldb) image lookup -rn viewDidLoad\]$ UIKitCore

Using the regular expression syntax, the command above looks for any viewDidLoad methods. The \]$ syntax dictates the final character in the symbol’s name is a closing bracket. This is a nice addition because Objective-C blocks could be implemented in a viewDidLoad method. When that happens, the symbol name for the block’s function would be the name of the symbol for the function where the block is created with the phrase __block_invoke appended, along with an increasing number if multiple blocks are implemented in the function.

Note: Regarding lldb‘s command interface, there’s a subtle difference between searching for code in a module — image lookup — versus breaking in code for a module — breakpoint set. If you wanted to search for all blocks in the Commons framework that contain the phrase _block_invoke in their symbols, you’d use image lookup -rn _block_invoke Commons. If you wanted to make a breakpoint for every block in the Commons framework, you’d use rb appendSignal.*block_invoke -s Commons. Take note of the -s argument versus the space.

Note: It’s worth mentioning that a private symbol’s name can be stripped out of code in a shared library. A private symbol is a symbol that can’t be linked to from another module — i.e., dictated from the private keyword in Swift or the static symbol declaration in C. The presence of a private symbol’s name in the symbol table hints that Apple didn’t care to remove it. If lldb can infer a symbol at an address but can’t find a name for it, lldb names the symbol ___lldb_unnamed_symbol<Counter>$$<Module> where Counter increases for every unknown symbol name in the module. See how many stripped, private symbols there are with an image lookup -rn ___lldb.

Swift Symbol Naming

Swift, like C++, generates mangled symbol names depending on the context/attributes of the originating source code. Compiling a simple C file and then comparing it to Swift best illustrates this. In Terminal, compile and view a function in C code:

% echo "void somefunction(void) {}" > /tmp/mangling_test.c
% clang -o /tmp/mangling_test /tmp/mangling_test.c -shared
% nm /tmp/mangling_test
0000000000003fb4 T _somefunction
% echo "func somefunction() {}" > /tmp/mangling_test.swift
% swiftc -o /tmp/mangling_test /tmp/mangling_test.swift -emit-library
% nm /tmp/mangling_test
0000000000003fb0 t _$s13mangling_test12somefunctionyyF
0000000000003fb4 s ___swift_reflection_version
% nm /tmp/mangling_test | xcrun swift-demangle
0000000000003fb0 t mangling_test.somefunction() -> ()
0000000000003fb4 s ___swift_reflection_version
(lldb) language swift demangle _$s13mangling_test12somefunctionyyF
_$s13mangling_test12somefunctionyyF ---> mangling_test.somefunction() -> ()
(lldb) image lookup -n somefunction
(lldb) image lookup -n mangling_test.somefunction
(lldb) image lookup -n $s13mangling_test12somefunctionyyF

More Swift Naming Conventions

Understanding how Swift mangles symbols can help you identify clever breakpoints in your source code.

(lldb) image lookup -rn ^\$s Commons
(lldb) image lookup -rn ^\$s.*viewDidLoad`
class SomeClass {
    var heresAVariable: Int = 0
}
_$s4Bleh9SomeClassC14heresAVariableSivg
(lldb) rb \$s.*vg$

Undocumented Debugging Methods

The image lookup command also does a great job searching for undocumented methods. Apple has included undocumented APIs for internal debugging that can also be used by those who are willing to dig through the symbol tables to find these helpful APIs.

(lldb) exp -lobjc -O -- [[UIApplication sharedApplication] _ivarDescription]
(lldb) image lookup -rn UIGestureEnvironment

Dyld Shared Cache

When working with all these shared libraries, you may have noticed something a little odd. Open Terminal and type these commands:

% cd /tmp
% touch some_program.swift
% swiftc some_program.swift -framework CoreAudio
% otool -L some_program
some_program:
  /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
  /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0)
  /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio (compatibility version 1.0.0, current version 1.0.0)
% ls /usr/lib/libobjc.A.dylib
ls: /usr/lib/libobjc.A.dylib: No such file or directory
% ls /usr/lib/libSystem.B.dylib
ls: /usr/lib/libSystem.B.dylib: No such file or directory
% ls /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio
ls: /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio: No such file or directory

Extracting Shared Libraries Without a Third-Party Tool

Apple has shipped a special bundle located at /usr/lib/dsc_extractor.bundle since about Big Sur or so. You can link to this bundle and use it to extract dsc libraries using the publicly exported symbol dyld_shared_cache_extract_dylibs_progress.

extern int
dyld_shared_cache_extract_dylibs_progress(const char* shared_cache_file_path,
                                          const char* extraction_root_path,
                                          void (^progress)(unsigned current, unsigned total));
import Foundation

typealias extract_dylibs = @convention(c) (
  UnsafePointer<CChar>?,
  UnsafePointer<CChar>?, ((UInt32, UInt32) -> Void)?) -> Int32

if CommandLine.argc != 3 {
  print("\(String(utf8String: getprogname()) ?? "") <cache_path> <output_path>")
  exit(1)
}

guard let handle = dlopen("/usr/lib/dsc_extractor.bundle", RTLD_NOW) else {
  print("Couldn't find handle")
  exit(1)
}

guard let sym = dlsym(
  handle, "dyld_shared_cache_extract_dylibs_progress") else {

  print("Can't find dyld_shared_cache_extract_dylibs_progress")
  exit(1)
}

let extract_dylibs_func = unsafeBitCast(
  sym, to: extract_dylibs.self)
let err = extract_dylibs_func(CommandLine.arguments[1],
                              CommandLine.arguments[2])
 { cur, total in
    print("\(cur)/\(total)")
}

if err != 0 {
  print("Something went wrong")
  exit(1)
} else {
  print("success! files written at \"\(CommandLine.arguments[2])\"")
}
% swiftc dsc_extractor.swift
% dsc_extractor /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e /tmp/dsc_payload
% dsc_extractor /System/Library/dyld/dyld_shared_cache_arm64e /tmp/dsc_payload

Key Points

  • image is an alias for the target modules subcommand and lets you inspect the loaded libraries for an app.
  • Use image lookup -n to search for modules by name. Add the -r flag to also use regex syntax to narrow your searches.
  • Swift symbols get mangled by the compiler, but following a pattern, so you can still use regex to match them.
  • Use language swift demangle in lldb to demangle symbols or use xcrun swift-demangle in terminal.
  • Using image lookup to search Apple’s code for undocumented methods that may assist you in your debugging.
  • Apple combines libraries into a shared cache, but you can extract and introspect individual libraries.

Where to Go From Here?

A number of challenges in this chapter can help you get interested in symbol exploration:

Need Another Challenge?

Using the private UIKitCore NSObject category method _shortMethodDescription as well as your image lookup -rn command, attach to the SpringBoard process and search for the class responsible for displaying time in the upper-left corner of the status bar. Keep in mind that you’ll need SIP disabled. Change the class’s value to something more amusing. Drill into subviews and see if you can find it using the tools given so far.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now