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

16. Hooking & Executing Code With dlopen & dlsym
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

Using LLDB, you’ve seen how easy it is to create breakpoints and inspect things of interest. You’ve also seen how to create classes you wouldn’t normally have access to. Unfortunately, you’ve been unable to wield this power at development time because you can’t get a public API if the framework, or any of its classes or methods, are marked as private. However, all that is about to change.

It’s time to learn about the complementary skills of developing with these frameworks. In this chapter, you’re going to learn about methods and strategies to “hook” into Swift and C code as well as execute methods you wouldn’t normally have access to while developing.

This is a critical skill to have when you’re working with something such as a private framework and want to execute or augment existing code within your own application. To do this, you’re going to call on the help of two awesome functions: dlopen and dlsym.

The Objective-C Runtime vs. Swift & C

Objective-C, thanks to its powerful runtime, is a truly dynamic language. Even when compiled and running, not even the program knows what will happen when the next objc_msgSend comes up.

There are different strategies for hooking into and executing Objective-C code; you’ll explore these in later chapters, but this chapter focuses on how to hook into and use these frameworks under Swift.

Swift acts a lot like C or C++. If it doesn’t need the dynamic dispatch of Objective-C, the compiler doesn’t have to use it. This means when you’re looking at the assembly for a Swift method that doesn’t need dynamic dispatch, the assembly can simply call the address containing the method. This “direct” function calling is where dlopen and dlsym really shine. This is what you’re going to learn about in this chapter.

Setting Up Your Project

For this chapter, you’re going to use a starter project named Watermark, located in the starter folder.

Easy Mode: Hooking C Functions

When learning how to use the dlopen and dlsym functions, you’ll be going after the getenv C function. This simple C function takes a char * (null terminated string) for input and returns the environment variable for the parameter you supply.

po (char *)$arg1

"DYLD_INSERT_LIBRARIES"
"NSZombiesEnabled"
"OBJC_DEBUG_POOL_ALLOCATION"
"MallocStackLogging"
"MallocStackLoggingNoCompact"
"OBJC_DEBUG_MISSING_POOLS"
"LIBDISPATCH_DEBUG_QUEUE_INVERSIONS"
"LIBDISPATCH_CONTINUATION_ALLOCATOR"
... etc ...
func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions:
  [UIApplication.LaunchOptionsKey : Any]? = nil)
  -> Bool {
  if let cString = getenv("HOME") {
    let homeEnv = String(cString: cString)
    print("HOME env: \(homeEnv)")
  }
  return true
}
HOME env: /Users/wtyree/Library/Developer/CoreSimulator/Devices/53BD59A2-6863-444C-8B4A-6C2E8159D81F/data/Containers/Data/Application/839B711F-0FB2-42B0-BC93-018868852A31

#include <dlfcn.h>
#include <assert.h>
#include <stdio.h>
#include <dispatch/dispatch.h>
#include <string.h>
char * getenv(const char *name) {
  return "YAY!";
}
HOME env: YAY!
char * getenv(const char *name) {
  return getenv(name);
  return "YAY!";
}

(lldb) image lookup -s getenv
1 symbols match 'getenv' in /Users/wtyree/Library/Developer/Xcode/DerivedData/Watermark-dlayapbfrqyqcyeehrxxaiewhkma/Build/Products/Debug-iphonesimulator/Watermark.app/Frameworks/HookingC.framework/HookingC:
        Address: HookingC[0x0000000000003f60] (HookingC.__TEXT.__text + 0)
        Summary: HookingC`getenv at getenvhook.c:15
1 symbols match 'getenv' in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/libsystem_c.dylib:
        Address: libsystem_c.dylib[0x0000000000056378] (libsystem_c.dylib.__TEXT.__text + 347788)
        Summary: libsystem_c.dylib`getenv
1 symbols match 'getenv' in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/AppleAccount.framework/AppleAccount:
extern void * dlopen(const char * __path, int __mode);
extern void * dlsym(void * __handle, const char * __symbol);
char * getenv(const char *name) {
  void *handle = dlopen("/usr/lib/system/libsystem_c.dylib",
                        RTLD_NOW);
  assert(handle);
  void *real_getenv = dlsym(handle, "getenv");
  printf("Real getenv: %p\nFake getenv: %p\n",
          real_getenv,
          getenv);
  return "YAY!";
}
Real getenv: 0x1018fe378
Fake getenv: 0x100d57e40
HOME env: YAY!
char * getenv(const char *name) {
  static void *handle;      // 1
  static char * (*real_getenv)(const char *); // 2

  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{  // 3
    handle = dlopen("/usr/lib/system/libsystem_c.dylib",
                    RTLD_NOW);
    assert(handle);
    real_getenv = dlsym(handle, "getenv");
  });

  if (strcmp(name, "HOME") == 0) { // 4
    return "/WOOT";
  }

  return real_getenv(name); // 5
}
func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions:
  [UIApplication.LaunchOptionsKey : Any]? = nil)
  -> Bool {
  if let cString = getenv("HOME") {
    let homeEnv = String(cString: cString)
    print("HOME env: \(homeEnv)")
  }

  if let cString = getenv("PATH") {
    let homeEnv = String(cString: cString)
    print("PATH env: \(homeEnv)")
  }
  return true
}
HOME env: /W00T
PATH env: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/local/bin

Hard Mode: Hooking Swift Methods

Going after Swift code that isn’t dynamic is a lot like going after C functions. However, there are a couple of complications with this approach that make it a bit harder to hook into Swift methods.

if let handle = dlopen("", RTLD_NOW) {}

if let handle =
  dlopen("./Frameworks/HookingSwift.framework/HookingSwift",
         RTLD_NOW) {
}
(lldb) image lookup -rn HookingSwift.*originalImage
1 match found in /Users/wtyree/Library/Developer/Xcode/DerivedData/Watermark-dlayapbfrqyqcyeehrxxaiewhkma/Build/Products/Debug-iphonesimulator/Watermark.app/Frameworks/HookingSwift.framework/HookingSwift:
  Address: HookingSwift[0x0000000000003264] (HookingSwift.__TEXT.__text + 328)
  Summary: HookingSwift`HookingSwift.CopyrightImageGenerator.originalImage.getter : Swift.Optional<__C.UIImage> at CopyrightImageGenerator.swift:45
(lldb) image dump symtab -m HookingSwift

[    8]     54 D X Code            0x0000000000003264 0x0000000100e73264 0x00000000000000d4 0x000f0000 $s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg
let sym = dlsym(handle, "$S12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg")!
print("\(sym)")
0x00000001005df264
(lldb) b 0x0000000103105770
Breakpoint 2: where = HookingSwift`HookingSwift.CopyrightImageGenerator.originalImage.getter : Swift.Optional<__C.UIImage> at CopyrightImageGenerator.swift:45, address = 0x00000001005df264
typealias privateMethodAlias = @convention(c) (Any) -> UIImage? // 1
let originalImageFunction = unsafeBitCast(sym, to: privateMethodAlias.self) // 2
let originalImage = originalImageFunction(imageGenerator) // 3
self.imageView.image = originalImage // 4

Key Points

  • The getenv function is called well before your main function is. getenv is therefore a good place to set breakpoints when you want to hook into the beginning of the app.
  • Create frameworks when you want to hook into libraries, as once your main loads all symbol addresses will be bound and your application won’t perform symbol lookup or loads again.
  • Use dlopen to explicitly load a module, then dlsym to get a handle to a function in the module.
  • Working with Swift methods requires the mangled name but you can find those using image dump symtab.
  • In Swift, you can use a typealias to cast function signatures.

Where to Go From Here?

You’re learning how to play around with dynamic frameworks. The previous chapter showed you how to dynamically load them in LLDB. This chapter showed you how to modify or execute Swift or C code you normally wouldn’t be able to. In the next chapter, you’re going to play with the Objective-C runtime to dynamically load a framework and use Objective-C’s dynamic dispatch to execute classes you don’t have the APIs for.

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