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

18. Mach-O Fun
Written by Walter Tyree

Hopefully you’re not too burnt out from dumping raw bytes and comparing them to structs in the previous chapter, because here comes the real fun!

To hammer in the different Mach-O section types, you’ll build a series of examples across this chapter. You’ll start with a “scanner” that looks for any insecure http: hardcoded strings loaded into the process. After that, you’ll learn how to cheat those silly, gambling or freemium games where you never win the loot.

Commence funtime meow.

Mach-O Refresher

Just so you know what’s expected of you, you’ll start with a brief refresher from the previous chapter.

Segments are groupings on disk and in memory that have the same memory protections. Segments can have zero or more sections found inside a grouping.

Sections are sub-components found in a segment. They serve a specific purpose to the program. For example, there’s a specific section for compiled code and a different section for hard-coded strings.

Sections and segments are dictated by the load commands, which are at the beginning of the executable, immediately following the Mach-O Header

You saw a couple of the important segments in the previous chapter, notably the __TEXT, __DATA, and __LINKEDIT segments.

The Mach-O Sections

Included in this chapter is an iOS project called MachOFun. Open it up and take a look around.

It’s a simple UITabBarController application which breaks up the different examples you’ll implement in this chapter.

One tab showcases a UITableViewController with some placeholder data. You’ll first build a data source that finds all hardcoded insecure HTTP URLs in memory and then you’ll display them in the UITableView as a “public shaming”.

The other tab shows a rather ugly implementation of a slot machine for gambling purposes, for which you’ll use your Mach-O knowledge to cheat the system and always win.

Build and run. At any point, suspend the program via LLDB and run the following command.

(lldb) image dump sections MachOFun

As you learned in the previous chapter, this will dump all the segments and corresponding sections found in the MachOFun module.

Search for the MachOFun.__TEXT.__text, section which stores executable code in the MachOFun application.

Note: I am not the biggest fan of the image dump sections [modulename] LLDB command, since it produces an overload of output and is hard on the eyes. Also, if you forget to provide a module, LLDB will default to every module loaded into the process, which is a huge amount of output. But that command is the default and requires no extra setup. If you have trouble visually parsing the sections, use the LLDB console filter on the lower right of Xcode to make your life easier. Just remember to turn it off when you’re done.

In the console output, you’ll see something similar to the following, though your memory addresses may be different.

  0x00000001 code             [0x00000001006e0240-0x00000001006e8b34)  r-x  0x00004240 0x000088f4 0x80000400 MachOFun.__TEXT.__text

Breaking down this output:

  • The 0x00000001 is LLDB’s way to identify the section.
  • LLDB has identified the content as code.
  • The addresses in brackets is where this section is located in memory.
  • The 0x00004240 is the offset on disk, while the 0x000088f4 value is the size of the section on disk.
  • Finally, the flags have the value 0x80000400 which are S_ATTR_SOME_INSTRUCTIONS and S_ATTR_PURE_INSTRUCTIONS OR’d together. Once again, I’ll leave that to you to research in mach-o/loader.h.

Note: The size of a section or segment on disk could be different when compared to the size when loaded into memory. This will be determined by the Mach-O load command. For example, the __PAGEZERO segment takes up 0 bytes on disk, but when loaded into memory, it takes up the first 2^32 bits in a 64-bit process. You can verify this on any executable (I use the ls as an example) by inspecting the Load Commands: otool -l $(which ls) | grep "Load command 0" -A11, The filesize variable is 0, while the vmsize variable is 0x0000000100000000, or 2^32.

Now that you know how to parse the LLDB output, it’s time to turn your attention back to __TEXT.__text section.

Using LLDB, take any method or function you can think of and find the section that it’s located in. I’ll use my go-to default, -[UIViewController viewDidLoad], but you should pick something different.

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

You’ll see output similar to the following:

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

The method -[UIViewController viewDidLoad] is located in UIKitCore. As mentioned previously, if you’re running a version of iOS earlier than 12, then this method will be located in UIKit. The output above gives the full path to the UIKitCore module. The offset on disk is shown, via UIKitCore[0x0000000000abb3f8]; it’s contained in UIKitCore.__TEXT.__text section at offset 11243624. If you wanted the load address to be displayed in the output, then you could supply the --verbose option (or just -v) to LLDB.

Clear the screen and run the image dump sections MachOFun LLDB command again. This time, search for the MachOFun.__TEXT.__cstring Section.

In my output, I got the following:

0x00000006 data-cstr        [0x0000000109e6a320-0x0000000109e6b658)  r-x  0x0000c320 0x00001338 0x00000002 MachOFun.__TEXT.__cstring

This is where the UTF-8 hardcoded strings are stored for your print statements, key-value coding/observing, or anything else that’s between quotation marks in your source code. Using LLDB, dump the memory in this section by referencing the size and the start load address.

(lldb) memory read -r -fC -c 0x00001338 0x0000000109e6a320

This prints the memory starting at address 0x0000000109e6a320, format the output as printable characters (-fC), repeat the process 0x00001338 times, and force (-r) to print the entire count, since LLDB defaults to an upper limit of 1024 bytes. You’ll see a load of familiar strings in here, such as "Unexpectedly found nil while unwrapping an Optional value". Take a look through the strings and see what you can find.

Alternatively, you can use Xcode’s graphical memory viewer from the Debug ▸ Debug Workflow ▸ View Memory menu. Then paste or type 0x0109e6a320 into the Address text box. Then select a value in the Number of Bytes dropdown that is greater than 0x1338. You can use the LLDB command p/d 0x1338 to convert the value to decimal. Then just choose a number greater than that.

Numerous other sections contain UTF-8 strings for different purposes. For example, the __TEXT.__objc_methname section contains Objective-C method names that are referenced directly by your application. The __TEXT.__swift4_reflstr section contains references to Swift’s reflected items. A candidate for Swift runtime reflection would be references to IBOutlet or IBInspectable variables.

I highly recommend exploring these sections further on your own.

Finding HTTP Strings

Now that you know hardcoded UTF-8 strings are stored in the __TEXT.__cstring module, you’ll use that knowledge to search every module in the process to see if any string begins with the characters "http:"

Open up InsecureNetworkRequestsTableViewController.swift and add the following below import UIKit:

import MachO

Next, navigate to the setupDataSource function.

The logic is all setup to display any hits from the dataSource variable, but it only contains placeholder data for now. The dataSource variable is a typealiased array of (module: String, strings: [String])’s. That means for every element in the array, there will be a module name, plus an array of strings for that module that contain any insecure "http:" strings.

Remove the code in setupDataSource and replace it with the following:

for i in 0..<_dyld_image_count() {
    let imagePath = _dyld_get_image_name(i)!
    let name = String(validatingUTF8: imagePath)!
    let basenameString = (name as NSString).lastPathComponent

    var module : InsecureHTTPRequestsData = (basenameString, [])
    var rawDataSize: UInt = 0
    guard let rawData =
      getsectdatafromFramework(basenameString,
                               "__TEXT",
                               "__cstring",
                               &rawDataSize) else {
      continue
    }

    print(
      "__TEXT.__cstring data: \(rawData), \(basenameString)")
}

The main point of interest in this code is the getsectdatafromFramework API. This function takes the name of the module, the name of the containing segment as well as the section and gives the pointer to the location of the section in memory! In addition, there’s an inout variable called rawDataSize which gives the size of the section in memory.

Build and run. You’ll see every module that contains a __TEXT.__cstring section as well as the appropriate load address of where in memory it can be found.

From the console output, you’ll can see the lots of output. I got 360 hits including:

__TEXT.cstring data: 0x0000000102bae5e0, libBacktraceRecording.dylib
__TEXT.cstring data: 0x0000000102b82688, libRPAC.dylib
__TEXT.cstring data: 0x0000000102bedf88, libViewDebuggerSupport.dylib
__TEXT.cstring data: 0x0000000102a6a0a0, MachOFun
...

Pause the application. Then take any address you find and use LLDB to query information about. I’ll take the __TEXT.__cstring load address for libViewDebuggerSupport.dylib in my process. As always, your output for load addresses will likely be different.

Grabbing the __TEXT.__cstring load address of libViewDebuggerSupport.dylib, query info about it using LLDB:

(lldb) image lookup -a 0x0000000102bedf88

I get the following output:

Address: libViewDebuggerSupport.dylib[0x000000000002df88] (libViewDebuggerSupport.dylib.__TEXT.__cstring + 0)
Summary: "numberOfSections"

The libViewDebuggerSupport.dylib[0x000000000002df88] shows the offset on disk to where the __TEXT.__cstring location is stored. Remember, that might not be the finalized offset on disk if the executable is a fat executable with multiple architecture slices. The Summary part of the output might seem a little misleading with "numberOfSections", but remember, this is the location of where hardcoded UTF-8 strings are stored. Using LLDB, print out the first string at libViewDebuggerSupport.dylib.__TEXT.__cstring, like so:

(lldb) x/s 0x0000000102bedf88

You’ll get:

0x102bedf88: "numberOfSections"

The first hardcoded string compiled into the libViewDebuggerSupport.dylib module is the string “numberOfSections”. This is the output of the compiled version of libViewDebuggerSupport.dylib on my machine, and the first string could be different in other versions of libViewDebuggerSupport.dylib.

Now that you’ve found the start address of the __TEXT.__cstring sections, it’s time to parse that whole buffer of memory to search for any strings that begin with "http:".

Remember, this buffer of memory is a bunch of UTF-8 C strings. That means you need to parse a string for as long as you can until you hit a NULL byte.

Open InsecureNetworkRequestsTableViewController.swift and in setupDataSource(), remove the print statement you made earlier and replace with the following:

var index = 0
while index < rawDataSize {
  let cur = rawData.advanced(by: index)
  let length = strlen(cur)
  index = index + length + 1

  guard let str = String(utf8String: cur),
    length > 0 else {
      continue
  }

  if str.hasPrefix("http:") {
    module.strings.append(str)
  }
}

if module.strings.count > 0 {
  dataSource.append(module)
}

This code will grab the rawData pointing to a __TEXT.__cstring section in memory. The while loop performs several checks, making sure a valid UTF-8 string of length greater than 0 exists. If so, then the beginning of the string is checked to see if it contains the characters "http:". If so, then the string is added to the strings array. Finally, if a module has any strings that contain "http:", then that is added to the dataSource variable.

Finally, make sure you have a controlled test in the MachOFun module to make sure this is correctly working.

In viewDidLoad, add the following code right after `super.viewDidLoad():

let _ = "https://www.google.com"
let _ = "http://www.altavista.com"

If everything works as expected, the https://www.google.com string will not be displayed (since it begins with “https”), while the http://www.altavista.com string will (hopefully?) be displayed.

Build and run.

As you can see, there are a number of insecure hardcoded URLs not only in the MachOFun module, but modules like libxml2.2dylib, GeoServices, CFNetwork, etc.

Sections in the __DATA Segment

Now that you’ve got your public insecure URL shaming out of the way, it’s time to shift the attention to the writeable __DATA segment and explore some interesting sections.

Suspend the MachOFun app and use LLDB to query the data sections. Execute the good ol’ following LLDB command:

(lldb) image dump sections MachOFun

Search for the __DATA_CONST.__objc_classlist section in the output. In my process, I got the following…

0x00000015 data-ptrs        [0x000000010213c7a8-0x000000010213c7c8)  rw-  0x000107a8 0x00000020 0x10000000 MachOFun.__DATA_CONST__objc_classlist

This section stores Class pointers to Objective-C or Swift classes. This section is an array of Class pointers that point to the actual Classes stored into __DATA.__objc_data. Think of the __DATA.__objc_data section as a buffer of Objective-C data packed together, just as how the hardcoded UTF-8 strings are stored in the __TEXT.__cstring section.

Jumping back to the __DATA_CONST.__objc_classlist section, you can quickly determine that there are four classes implemented by the MachOFun module. How can you determine this? The segment size is 0x00000020 divided by the size of a pointer in a 64-bit process (8 bytes), which leaves you with four Objective-C/Swift classes.

Use LLDB to dump the raw pointers from the __DATA_CONST.__objc_classlist section to prove this is correct.

(lldb) x/4gx 0x0000010dcae8e0
0x10dcae8e0: 0x000000010dcb0580 0x000000010dcb0690
0x10dcae8f0: 0x000000010dcb0758 0x000000010dcb0800

Then for each pointer:

(lldb) exp -l objc -O -- 0x000000010dcb0580
MachOFun.CasinoContainerView

(lldb) exp -l objc -O -- 0x000000010dcb0690
MachOFun.CasinoViewController

(lldb) exp -l objc -O -- 0x000000010dcb0758
MachOFun.InsecureNetworkRequestsTableViewController

(lldb) exp -l objc -O -- 0x000000010dcb0800
MachOFun.AppDelegate

Inside MachOFun, there are four Swift classes, due to the fact the module and period precedes the class name.

Tools like class-dump use this information along with numerous other Mach-O sections to display Swift/Objective-C classes.

The __bss, __common and __const Sections

Sometimes a module needs to keep references to data that lives past a function call. As you’ve learned earlier, if you were to declare a constant such as let v = UIView() inside of a function, the pointer v is stored on the stack which points to allocated memory on the heap. But as soon as the instruction pointer leaves the function, the reference to the v variable is long gone. That’s why there are several sections in the __DATA segment designed to store variables across the lifetime of a process.

When you declare a global variable, which is a variable outside the scope of any method or function, it will typically be placed into the __DATA.__common section. This section expects to share information across the module and even across other modules.

What if a developer wanted to have a variable survive across function calls, but not have it accessible to any other modules, or even from other source files within the same module? This is typically achieved by storing variables in the __DATA.__bss section. The C/Objective-C family does this via a static declaration to a variable. In Swift, this can be achieved with a private declaration on a Swift variable.

Finally, there are global variables that you want declared as unchanging for the life of the program. You can mark these as const in C/Objective-C to store variables in the __DATA.__const section. From a developers standpoint, Swift mostly doesn’t need you to touch the __DATA.__const section due to the let keyword and checking for changes to a variable at compile time.

Cheating Freemium Games

The __DATA segment not only stores references to data in the module, but it also provides references to external variables, classes, methods, and functions that are not defined within the module.

Think about why this is the case for a second. If, in theory, a module can be loaded at any address, a reference point must be used to indicate where to start looking when calling out to that code. Since this location is not known until runtime, this starting reference point must be writable from the calling module.

This applies to external C functions, Swift/Objective-C classes, global variables, etc.

The __DATA_CONST.__got is a rather interesting section as it stores references to external functions that are lazily resolved at runtime when called. For this complex dance to work, the __DATA_CONST.__got section stores a series of function pointers that point to offsets in the __TEXT.__stub_helper section in the calling module. This sets off a flurry of activity as dyld resolves the location of this external function. I’ll stay out of the gory details of this, but just know that external functions by default are referenced through the __DATA_CONST.__got section and are “resolved” if the function pointer doesn’t point to an address in the __TEXT.__stub_helper section. This whole process is called a fixup.

Resume execution of the MachOFun program and navigate the app to the Casino tab. Once at the slot machine, give it a couple of spins.

For you intermediate to advanced readers out there, see if you can recall the API or APIs to generate a random number. Remember your guess and see if it’s true below. Suspend the program and type the following in LLDB:

(lldb) exp -l objc -O -- [[NSBundle mainBundle] executablePath]

This will give you the full path to the running application. Copy the full path to the clipboard and type the following in LLDB, replacing `${APP_PATH} with the path you copied:

(lldb) platform shell dyld_info -fixups ${APP_PATH}

You’re running the Terminal command dyld_info, searching for all symbols inside the MachOFun executable that dyld is going to have to bind.

Now use the filter or find or just scroll through the output to find your best guess for the random number generator function.

Did you guess the function correctly? I found:

__DATA_CONST __got            0x1000102D0              bind  libSystem.B.dylib/_arc4random_uniform

This means that arc4random_uniform is being called somewhere in the MachOFun code. This function will generate a random number with a range given by the first parameter.

This 0x1000102D0 value is the calculated offset in memory without the ASLR slide. This 0x1000102D0 value includes the __PAGEZERO offset (given by the 0x100000000) with the actual real offset on disk with the value 0x102D0.

How can you translate this 0x1000102D0 value into memory? You can use the _dyld_get_image_vmaddr_slide API to get the address slide! Remember, earlier in the chapter when you printed out all of the _dyld_get_image_name(i)! when looking for __cstring sections?

In LLDB, type the following, replacing the 3 with whatever index for you matches up with the MachOFun image:

(lldb) po (char *)_dyld_get_image_name(3)

This is to make sure you are referencing the correct index into the modules. Make sure the output references the MachOFun executable.

/Users/virtualadmin/Library/Developer/CoreSimulator/Devices/53BD59A2-6863-444C-8B4A-6C2E8159D81F/data/Containers/Bundle/Application/D62A2699-1881-4BC5-BD11-ACAD2479D057/MachOFun.app/MachOFun

After that, use LLDB with the same index number with the _dyld_get_image_vmaddr_slide API and add it to the value you retrieved from objdump command:

(lldb) p/x (intptr_t)_dyld_get_image_vmaddr_slide(3) + 0x1000102D0

For me, I got the value 0x00000001005ec2d0. This value is the resolved load address to the location to the external stub reference of arc4random_uniform in memory. Dereference the value of this address and examine it with LLDB:

(lldb) x/gx 0x00000001005ec2d0

This will produce something similar to the following:

0x1005ec2d0: 0x00000001800d8bc4

Query this new address and see what it resolved to:

(lldb) image lookup -a 0x00000001800d8bc4

And lo and behold you’ll get the in-memory address to arc4random_uniform:

Address: libsystem_c.dylib[0x0000000000023bc4] (libsystem_c.dylib.__TEXT.__text + 141016)
Summary: libsystem_c.dylib`arc4random_uniform

This means that the arc4random_uniform function has already been resolved, since the function pointer in __DATA_CONST.__got is pointing to arc4random_uniform instead of an offset in the __TEXT.__stub_helper section.

Hell, you’re not even going to set a breakpoint on arc4random_uniform since you’re so confident that this slot machine is calling arc4random_uniform to generate random numbers. You’ll change around the pointer in memory just to see what can happen!

In LLDB, create a global function that always returns the value 5.

(lldb) exp -l objc -p -- int lolzfunc()  { return 5; }

The out-of-the-ordinary -p option says to execute this code outside of any stack frame. This is necessary since you can’t declare functions inside other C code. This means there’s a global function named lolzfunc floating around somewhere in memory.

Grab the address of the lolzfunc via LLDB:

(lldb) p/x lolzfunc
(int (*)()) $0 = 0x00000001018209b0

The plan of attack should be clear now. You will change around the external stub’s pointer of arc4random_uniform to the address of the newly created function, lolzfunc.

In LLDB, type the following:

(lldb) memory write -s8 0x00000001005ec2d0 0x00000001018209b0

The first pointer is the original address of arc4random_uniform that you found earlier. The second pointer is the new lolzfunc address.

This tells LLDB to write 8 bytes (-s8) at location 0x00000001005ec2d0, with value 0x00000001018209b0.

You just followed a very complex set of instructions. To recap the steps:

  1. Use ex -l objc -O -- [[NSBundle mainBundle] executablePath] to get the path of the running executable.
  2. Use platform shell dyld_info -fixups <the_path_from_step_1> to dump out all of the symbols that need fixups.
  3. Find the entry for _arc4random_uniform and copy the memory address.
  4. Use (char *)_dyld_get_image_name(<index>) to figure out the index of the main executable.
  5. Use p/x (intptr_t)_dyld_get_image_vmaddr_slide(<the_index_from_step_4>) + <the_memory_address_from_step_3> to get the resolved address.
  6. Create the cheating function with exp -l objc -p -- int lolzfunc() { return 5; }.
  7. Get the address of the cheating function with p/x lolzfunc.
  8. Insert the cheating function memory write -s8 <resolved_address_from_step_5> <memory_address_from_step_7>.

Resume the application, then give the game another spin and see what happens.

You’re winning… every time… what are the odds of that? Crazy, eh?

Objective-C Swizzling vs Function Interposing

Unlike Objective-C method swizzling, lazy pointer loading occurs on a per-module basis. That means that the trick you just performed will only work when the MachOFun module calls out to arc4random_uniform. It wouldn’t work if, say, CFNetwork called out to arc4random_uniform.

Going back to the MachOFun app, do you see that "Print a random number to console" button?

That code resolves to an IBAction method which calls SomeClassInAFramework.printARandomNumber(). That code is implemented in a different framework creatively called AFramework. Inside the static printARandomNumber() function, arc4random_uniform is being called.

Press the button a couple of times and observe how arc4random_uniform works normally. This means that if you wanted to swap all arc4random_uniform stubs, you’d have to iterate through each module, find the arc4random_uniform location stub in memory and replace it with the address of the new function.

On your own time, you may want to explore a macro from dyld called DYLD_INTERPOSE, or else Facebook’s Fishhook. It will allow you to add a new DATA__interpose section to a Mach-O file to swizzle a dyld symbol and do for your entire executable what you just did for the one instance of arc4random.

Key Points

  • Use image dump sections to find all of the segments and sections for a module.
  • TEXT.__cstring holds hard coded utf8 strings in an executable. Where do you think the other strings are?
  • Explore the __DATA sections to find links to other modules as well as symbol names and metadata.
  • The terminal apps nm, otool and dyld_info can help you discover symbols in applications.

Where to Go From Here?

Oh my! There is so much more for you to learn about Mach-O, but the road ends here for now.

  • Check out Jonathan Levin’s work on describing Mach-O.

  • I also haven’t even started on the complexity and power of the __LINKEDIT segment. A surprisingly good reference is Facebook’s Fishhook, a runtime library for modifying external stubs, found here. You will have a brief glimpse into the __LINKEDIT’s symbol table in later chapters, but there will be a lot of information that can be learned elsewhere.

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.
© 2026 Kodeco Inc.