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

27. SB Examples, Malloc Logging
Written by Walter Tyree

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

For the final chapter in this section, you’ll go through the same steps I myself took to understand how the MallocStackLogging environment variable is used to get the stack trace when an object is created.

From there, you’ll create a custom LLDB command which gives you the stack trace of when an object was allocated or deallocated in memory — even after the stack trace is long gone from the debugger.

Knowing the stack trace of where an object was created in your program is not only useful for reverse engineering, but also has great use cases in your typical day-to-day debugging. When a process crashes, it’s incredibly helpful to know the history of that memory and any allocation or deallocation events that occurred before your process went off the deep end.

This is another example of a script using stack-related logic, but this chapter will focus on the complete cycle of how to explore, learn, then implement a rather powerful custom command.

Setting Up the Scripts

You have a couple of scripts to use (and implement!) for this chapter. Let’s go through each one of them and how you’ll use them:

  • msl.py: This is the command (which is an abbreviation for MallocStackLogging) script you’ll be working on in this chapter. This has a basic skeleton of the logic.
  • lookup.py: Wait — you already made this command, right? Yes, but I’ll give you my own version of the lookup command that adds a couple of additional options at the price of uglier code. You’ll use one of the options to filter your searches to specific modules within a process.
  • sbt.py: This command takes a backtrace with unsymbolicated symbols, and symbolicate it. You made this in the previous chapter, and you’ll need it at the very end of this chapter. And in case you didn’t work through the previous chapter, it’s included in this chapter’s resources for you to install.

Note: These scripts are also in Appendix C “Helpful Python Scripts” Check it out for some other novel ideas for LLDB scripts. It’s important to note that a lot of scripts in Appendix C have dependencies on other files, so if you try to use only one script then it might not compile until the full set of files are included.

Now for the usual setup. Take all the Python files found in the starter directory for this chapter and copy them into your ~/lldb directory. I am assuming you have the lldbinit.py file already set up, found in Chapter 25, “SB Examples, Improved Lookup.”

Launch an LLDB session in Terminal and go through all the help commands to make sure each script has loaded successfully:

(lldb) help msl
(lldb) help lookup
(lldb) help sbt

MallocStackLogging Explained

In case you’re unfamiliar with the MallocStackLogging environment variable, when the MallocStackLogging environment variable is set to true, it’ll monitor and record allocations and deallocations of memory on the heap. Pretty neat!

ShadesOfRay(12911,0x104e663c0) malloc: stack logs being written into /tmp/stack-logs.12911.10d42a000.ShadesOfRay.gjehFY.index

ShadesOfRay(12911,0x104e663c0) malloc: recording malloc and VM allocation stacks to disk using standard recorder

ShadesOfRay(12911,0x104e663c0) malloc: process 12673 no longer exists, stack logs deleted from /tmp/stack-logs.12673.11b51d000.ShadesOfRay.GVo3li.index

Plan of Attack

You’ve seen it’s possible to grab a stack trace for an instantiated object, but you’re going to do one better than Apple.

Hunting for a Starting Point

As you just saw, Xcode provides a special backtrace for any object that gets allocated when MallocStackLogging is enabled. Go ahead and build and run the app and then tap on Generate a Ray! a few times to create some instances of RayView. Now use the Debug Memory Graph a few times to stop the app. Now inspect some of the RayView instances to look for patterns.

_malloc_zone_calloc_instrumented_or_legacy
(lldb) lookup _malloc_zone_calloc_instrumented_or_legacy
****************************************************
1 hits in: libsystem_malloc.dylib
****************************************************
_malloc_zone_calloc_instrumented_or_legacy
(lldb) lookup . -m libsystem_malloc.dylib
(lldb) lookup [lL]og -m libsystem_malloc.dylib
__mach_stack_logging_get_frames

__mach_stack_logging_get_frames_for_stackid

turn_off_stack_logging

turn_on_stack_logging

_malloc_register_stack_logger

Googling JIT Function Candidates

Google for any code pertaining to turn_on_stack_logging. Take a look at this search query:

typedef enum {
  stack_logging_mode_none = 0,
  stack_logging_mode_all,
  stack_logging_mode_malloc,
  stack_logging_mode_vm,
  stack_logging_mode_lite
} stack_logging_mode_type;

extern boolean_t turn_on_stack_logging(stack_logging_mode_type mode);

Exploring __mach_stack_logging_get_frames

Fortunately, for your exploration efforts, __mach_stack_logging_get_frames can also be found in the same header file. This function signature looks like the following:

extern kern_return_t __mach_stack_logging_get_frames(
                                        task_t task,   
                          mach_vm_address_t address,
             mach_vm_address_t *stack_frames_buffer,
                          uint32_t max_stack_frames,
                                   uint32_t *count);
    /* Gets the last allocation record (malloc, realloc, or free) about address */
task_t task = mach_task_self();
/* Omitted code.... */
    stack_entry->address = addr;
    stack_entry->type_flags = stack_logging_type_alloc;
    stack_entry->argument = 0;
    stack_entry->num_frames = 0;
    stack_entry->frames[0] = 0;

    err = __mach_stack_logging_get_frames(task,
                       (mach_vm_address_t)addr,
                           stack_entry->frames,
                                    MAX_FRAMES,
                      &stack_entry->num_frames);

    if (err == 0 && stack_entry->num_frames > 0) {
      // Terminate the frames with zero if there is room
      if (stack_entry->num_frames < MAX_FRAMES)
        stack_entry->frames[stack_entry->num_frames] = 0;
    } else {
      g_malloc_stack_history.clear();
    }
  }
}

Testing the Functions

To prevent you from getting bored to tears, I’ve already implemented the logic for the __mach_stack_logging_get_frames inside the app.

void trace_address(mach_vm_address_t addr) {

  typedef struct LLDBStackAddress {
    mach_vm_address_t *addresses;
    uint32_t count = 0;
  } LLDBStackAddress;   // 1

  LLDBStackAddress stackaddress; // 2
  __unused mach_vm_address_t address = (mach_vm_address_t)addr;
  __unused task_t task = mach_task_self_;  // 3

  stackaddress.addresses = (mach_vm_address_t *)calloc(100,
                                sizeof(mach_vm_address_t)); // 4

  __mach_stack_logging_get_frames(task,
                               address,
                stackaddress.addresses,
                                   100,
                  &stackaddress.count); // 5

  // 6
  for (int i = 0; i < stackaddress.count; i++) {

    printf("[%d] %llu\n", i, stackaddress.addresses[i]);
  }

  free(stackaddress.addresses); // 7
}

LLDB Testing

Make sure the app is running, then tap the Generate a Ray! button a few times. Click the Debug Memory Graph button again to pause the app and bring up the graph.

(lldb) po trace_address(0x152d047c0)
[0] 4362273848
[1] 4346078816
[2] 4340224704
[3] 4711642824
[4] 4701717412
[5] 4701577380
[6] 4701577128
[7] 4711642824
[8] 4705047740
[9] 4705048576
...
(lldb) image lookup -a 4362273848
Address: libsystem_malloc.dylib[0x000000000000f485] (libsystem_malloc.dylib.__TEXT.__text + 56217)
Summary: libsystem_malloc.dylib`calloc + 30
(lldb) script print lldb.SBAddress(4454012240, lldb.target)
libsystem_malloc.dylib`_malloc_zone_calloc_instrumented_or_legacy + 220

Navigating a C Array With lldb.value

You’ll again use the lldb.value class to parse the return value of this C struct which was generated inline while executing this function.

(lldb) e -lobjc++ -O -i0 -- trace_address(0x00007fa838414330)
(lldb) script print (lldb.frame.FindVariable('stackaddress'))
(LLDBStackAddress) stackaddress = {
  addresses = 0x00007fa838515cd0
  count = 30
}
(lldb) script a = lldb.value(lldb.frame.FindVariable('stackaddress'))
(lldb) script print (a)
(lldb) script print (a.count)
(uint32_t) count = 30
(lldb) script print (a.addresses[0])
(lldb) script print (a.addresses[2])
(mach_vm_address_t) [2] = 4454012240

Turning Numbers Into Stack Frames

Included within the starter directory for this chapter is the msl.py script for malloc script logging. You’ve already installed this msl.py script earlier in the “Setting up the scripts” section.

command_args = shlex.split(command)
parser = generateOptionParser()
try:
    (options, args) = parser.parse_args(command_args)
except:
    result.SetError(parser.usage)
    return

cleanCommand = args[0]
process = debugger.GetSelectedTarget().GetProcess()
frame = process.GetSelectedThread().GetSelectedFrame()
target = debugger.GetSelectedTarget()
# 1
script = generateScript(cleanCommand, options)

# 2
sbval = frame.EvaluateExpression(script, generateOptions())

# 3
if sbval.error.fail:
    result.AppendMessage(str(sbval.error))
    return

val = lldb.value(sbval)
addresses = []

# 4
for i in range(val.count.sbvalue.unsigned):
    address = val.addresses[i].sbvalue.unsigned
    sbaddr = target.ResolveLoadAddress(address)
    loadAddr = sbaddr.GetLoadAddress(target)
    addresses.append(loadAddr)

# 5
retString = processStackTraceStringFromAddresses(
                                        addresses,
                                           target)

# 6
freeExpr = 'free('+str(val.addresses.sbvalue.unsigned)+')'
frame.EvaluateExpression(freeExpr, generateOptions())
result.AppendMessage(retString)
(lldb) reload_script
(lldb) msl 0x00007fa838414330
frame #0 : 0x11197d485 libsystem_malloc.dylib`calloc + 30
frame #1 : 0x10d3cbba1 libobjc.A.dylib`class_createInstance + 85
frame #2 : 0x10d3d5de4 libobjc.A.dylib`_objc_rootAlloc + 42
frame #3 : 0x10cde7550 ShadesOfRay`-[ViewController generateRayViewTapped:] + 64
frame #4 : 0x10e512d22 UIKit`-[UIApplication sendAction:to:from:forEvent:] + 83

Stack Trace From a Swift Object

OK — I know you want me to talk about Swift code. You’ll cover a Swift example as well.

public final class SomeSwiftCode {
  private init() {}
  static let shared = SomeSwiftCode()
}
(lldb) e -lswift -O -- import SomeSwiftModule
(lldb) e -lswift -O -- SomeSwiftCode.shared
<SomeSwiftCode: 0x600000033640>
(lldb) msl 0x600000033640

DRY Python Code

Stop the app! In the schemes, select the Stripped 50 Shades of Ray Xcode scheme.

(lldb) msl 0x1268051d0
(lldb) po turn_on_stack_logging(1)

(lldb) msl 0x00007f8250f0a170

import sbt
retString = sbt.processStackTraceStringFromAddresses(
                                            addresses,
                                               target)
if options.resymbolicate:
    retString = sbt.processStackTraceStringFromAddresses(
                                                addresses,
                                                   target)
else:
    retString = processStackTraceStringFromAddresses(
                                        addresses,
                                           target)
debugger.HandleCommand('command alias enable_logging expression -lobjc -O -- extern void turn_on_stack_logging(int); turn_on_stack_logging(1);')
(lldb) reload_script
(lldb) msl 0x00007f8250f0a170 -r

Key Points

  • Some Xcode functionalities are just wrappers around LLDB so enhancing them is a good way to practice creating scripts.
  • Malloc Stack Logging will log when objects are allocated and deallocated.
  • Malloc Stack Logging has an All Allocation and Free History and an All Allocation option. The Free History option records more data.
  • Use image lookup -rn or our custom lookup to search for any interesting symbol names you find as a first step in exploring.
  • Search https://opensource.apple.com to find header files that often have useful notes and documentation you won’t find anywhere else.
  • Write and test code in Xcode for any functions you later want to bring into a Python script as JIT-ed code. The tools for debugging JIT-ed code within your scripts are effectively nonexistent.
  • Python scripts can import Python scripts. As you write code, always look for ways to refactor so that you can reuse your best work in multiple places.

Where to Go From Here?

Hopefully, this full circle of idea, research & implementation has proven useful and even inspired you to create your own scripts. There’s a lot of power hidden quietly away in the many frameworks that already exist on your [i|mac|tv|watch]OS device.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now