16.
Hooking & Executing Code with dlopen & dlsym
Written by Derek Selander
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 the next chapter. 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 the dlopen and dlsym combo really shines. 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.
This project is very simple. All it does is display a watermarked image in a UIImageView.
However, there’s something special about this watermarked image. The actual image displayed is hidden away in an array of bytes compiled into the program. That is, the image is not bundled as a separate file inside the application. Rather, the image is actually located within the executable itself. Clearly the author didn’t want to hand out the original image, anticipating people would reverse engineer the Assets.car file, which typically is a common place to hold images within an application. Instead, the data of the image is stored in the __TEXT section of the executable, which is encrypted by Apple when distributed through the App Store. If that __TEXT section sounded alien, you’ll learn about it in Chapter 18: “Hello, Mach-O”.
First, you’ll explore hooking into a common C function. Once you’ve mastered the concepts, you’ll execute a private Swift method that’s unavailable to you at development time thanks to the Swift compiler. Using dlopen and dlsym, you’ll be able to call and execute this private method inside a framework with zero modifications to the framework’s code.
Now that you’ve got more theory than you’ve ever wanted in an introduction, it’s finally time to get started.
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.
This function is actually called quite a bit when your executable starts up.
Open and launch the Watermark project in Xcode. Create a new symbolic breakpoint, putting getenv in the Symbol section. Next, add a custom action with the following:
po (char *)$rdi
Now, make sure the execution automatically continues after the breakpoint hits.
Finally, build and run the application on the iPhone XS Simulator, then watch the console. You’ll get a slew of output indicating this method is called quite frequently.
It’ll look similar to the following:
"DYLD_INSERT_LIBRARIES"
"NSZombiesEnabled"
"OBJC_DEBUG_POOL_ALLOCATION"
"MallocStackLogging"
"MallocStackLoggingNoCompact"
"OBJC_DEBUG_MISSING_POOLS"
"LIBDISPATCH_DEBUG_QUEUE_INVERSIONS"
"LIBDISPATCH_CONTINUATION_ALLOCATOR"
... etc ...
Note: A far more elegant way to dump all environment variables available to your application is to use the
DYLD_PRINT_ENV. To set this up, go to Product\Manage Scheme, and then add this in theEnvironmentvariables section. You can simply add the name, DYLD_PRINT_ENV, with no value, to dump out all environment variables at runtime.
However, an important point to note is all these calls to getenv are happening before your executable has even started. You can verify this by putting a breakpoint on getenv and looking at the stack trace. Notice main is nowhere in sight. This means you’ll not be able to alter these function calls until your code can get executed.
Since C doesn’t use dynamic dispatch, hooking a function requires you to intercept the function before it’s loaded. On the plus side, C functions are relatively easy to grab. All you need is the name of the C function without any parameters along with the name of the dynamic framework in which the C function is implemented.
However, since C is all-powerful and used pretty much everywhere, there are different tactics of varying complexity you can explore to hook a C function. If you want to hook a C function inside your own executable, that’s not a lot of work. However, if you want to hook a function called before your code (main executable or frameworks) is loaded in by dyld, the complexity definitely goes up a notch.
As soon as your executable executes main, it’s already imported all the dynamic frameworks specified in the load commands, as you learned in the previous chapter. The dynamic linker will recursively load frameworks in a depth-first manner. If you were to call an external framework, it can be lazily loaded or immediately loaded upon module load by dyld. Typically, most external functions are lazily loaded unless you specify special linker flags.
With lazily loaded functions, the first time the function is called, a flurry of activity occurs as dyld finds the module and location responsible for the function. This value is then put into a specific section in memory (__DATA.__la_symbol_ptr, but we’ll talk about that later). Once the external function is resolved, all future calls to that function will not need to be resolved by dyld.
This means if you want to have the function hooked before your application starts up, you’ll need to create a dynamic framework to put the hooking logic in so it’ll be available before the main function is called. You’ll explore this easy case of hooking a C function inside your own executable first.
Back to the Watermarks project!
Open AppDelegate.swift, and replace application(_:didFinishLaunchingWithOptions:) with the following:
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
}
This creates a call to getenv to get the HOME environment variable.
Next, remove the symbolic getenv breakpoint you previously created and build and run the application.
The console output will look similar to the following:
HOME env: /Users/derekselander/Library/Developer/CoreSimulator/Devices/2B9F4587-F75E-4184-861E-C2CAE8F6A1D9/data/Containers/Data/Application/D7289D91-D73F-47CE-9FAC-E9EED14219E2
This is the HOME environment variable set for the Simulator you’re running on.
Say you wanted to hook the getenv function to act completely normally, but return something different to the output above if and only if HOME is the parameter.
As mentioned earlier, you’ll need to create a framework that’s relied upon by the Watermark executable to grab that address of getenv and change it before it’s resolved in the main executable.
In Xcode, navigate to File ▸ New ▸ Target and select Cocoa Touch Framework. Choose HookingC as the product name, and set the language to Objective-C.
Once this new framework is created, create a new C file. In Xcode, select File\New\File, then select C file. Name this file getenvhook. Uncheck the checkbox for Also create a header file. Save the file with the rest of the project.
Make sure this file belongs to the HookingC framework that you’ve just created, and not Watermark.
OK… you’re finally about to write some code… I swear.
Open getenvhook.c and replace its contents with the following:
#import <dlfcn.h>
#import <assert.h>
#import <stdio.h>
#import <dispatch/dispatch.h>
#import <string.h>
-
dlfcn.hwill be responsible for two very interesting functions:dlopenanddlsym. -
assert.hwill test the library containing the realgetenvis correctly loaded. -
stdio.hwill be used temporarily for a Cprintfcall. -
dispatch.hwill be used to to properly set up the logic for GCD’sdispatch_oncefunction. -
string.hwill be used for thestrcmpfunction, which compares two C strings.
Next, redeclare the getenv function with the hard-coded stub shown below:
char * getenv(const char *name) {
return "YAY!";
}
Finally, build and run your application to see what happens. You’ll get the following output:
HOME env: YAY!
Awesome! You were able to successfully replace this method with your own function. However, this isn’t quite what you want. You want to call the original getenv function and augment the return value if "HOME" is supplied as input.
What would happen if you tried to call the original getenv function inside your getenv function? Try it out and see what happens. Add some temporary code so the getenv looks like the following:
char * getenv(const char *name) {
return getenv(name);
return "YAY!";
}
Your program will… sort of… run and then eventually crash. This is because you’ve just created a stack overflow. All references to the previously linked getenv have disappeared now that you’ve created your own getenv fuction.
Undo that previous line of code. That idea won’t work. You’re going to need a different tactic to grab the original getenv function.
First things first though, you need to figure out which library holds the getenv function. Make sure that problematic line of code is removed, and build and run the application again. Pause execution and bring up the LLDB console.
Once the console pops up, enter the following:
(lldb) image lookup -s getenv
You’ll get output looks similar to the following:
1 symbols match 'getenv' in /Users/derekselander/Library/Developer/Xcode/DerivedData/Watermark-frqludlofnmrzcbjnkmuhgeuogmp/Build/Products/Debug-iphonesimulator/Watermark.app/Frameworks/HookingC.framework/HookingC:
Address: HookingC[0x0000000000000f60] (HookingC.__TEXT.__text + 0)
Summary: HookingC`getenv at getenvhook.c:16
1 symbols match 'getenv' in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//usr/lib/system/libsystem_c.dylib:
Address: libsystem_c.dylib[0x000000000005f1c4] (libsystem_c.dylib.__TEXT.__text + 385956)
Summary: libsystem_c.dylib`getenv
You’ll get two hits. One of them will be the getenv function you created yourself. More importantly, you’ll get the location of the getenv function you actually care about. It looks like this function is located in libsystem_c.dylib, and its full path is at /usr/lib/system/libsystem_c.dylib. Remember, the simulator prepends that big long path to these directories, but the dynamic linker is smart enough to search in the correct areas. Everything after iPhoneSimulator.sdk is where this framework is actually stored on a real iOS device.
Now you know exactly where this function is loaded, it’s time to whip out the first of the amazing “dl” duo, dlopen. Its function signature looks like the following:
extern void * dlopen(const char * __path, int __mode);
dlopen expects a fullpath in the form of a char * and a second parameter, which is a mode expressed as an integer that determines how dlopen should load the module. If successful, dlopen returns an opaque handle (a void *) ,or NULL if it fails.
After dlopen (hopefully) returns a reference to the module, you’ll use dlsym to get a reference to the getenv function. dlsym has the following function signature:
extern void * dlsym(void * __handle, const char * __symbol);
dlsym expects to take the reference generated by dlopen as the first parameter and the name of the function as the second parameter. If everything goes well, dlsym will return the function address for the symbol specified in the second parameter or NULL if it failed.
Replace your getenv function with the following:
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!";
}
You used the RTLD_NOW mode of dlopen to say, “Hey, don’t wait or do any cute lazy loading stuff. Open this module right now.” After making sure the handle is not NULL through a C assert, you call dlsym to get a handle on the “real” getenv.
Build and run the application. You’ll get output similar to the following:
Real getenv: 0x10d2451c4
Fake getenv: 0x10a8f7de0
2016-12-19 16:51:30.650 Watermark[1035:19708] HOME env: YAY!
Your function pointers will be different than my output, but take note of the difference in address between the real and fake getenv.
You’re starting to see how you’ll go about this. However, you’ll need to make a few touch-ups to the above code first. For example, you can cast function pointers to the exact type of function you expect to use. Right now, the real_getenv function pointer is void *, meaning it could be anything. You already know the function signature of getenv, so you can simply cast it to that.
Replace your getenv function one last time with the following:
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
}
You might not be used to this amount of C code, so let’s break it down:
-
This creates a static variable named
handle. It’s static so this variable will survive the scope of the function. That is, this variable will not be erased when the function exits, but you’ll only be able to access it inside thegetenvfunction. -
You’re doing the same thing here as you declare the
real_getenvvariable as static, but you’ve made other changes to thereal_getenvfunction pointer. You’ve cast this function pointer to correctly match the signature ofgetenv. This will allow you to call the realgetenvfunction through thereal_getenvvariable. Cool, right? -
You’re using GCD’s
dispatch_oncebecause you really only need to call the setup once. This nicely complements thestaticvariables you declared a couple lines above. You don’t want to be doing the lookup logic every time your augmentedgetenvruns! -
You’re using C’s
strcmpto see if you’re querying the"HOME"environment variable. If it’s true, you’re simply returning"/WOOT"to show yourself that you can change around this value. Essentially, you’re overriding what thegetenvfunction returns. -
If
"HOME"is not supplied as an input parameter, then just fall back on the defaultgetenv.
Open AppDelegate.swift, and replace application(_:didFinishLaunchingWithOptions:) with the following:
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
}
Build and run the application. Provided everything went well, you’ll get output similar to the following:
HOME env: /WOOT
PATH env: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/local/bin
As you can see, your hooked getenv augmented the HOME environment variable, but defaulted to the normal getenv for PATH.
Although annoying, it’s worth driving this point home yet again. If you called a UIKit method, and UIKit calls getenv, your augmented getenv function will not get called during this time because the getenv’s address had already been resolved when UIKit’s code loaded.
In order to change around UIKit‘s call to getenv, you would need knowledge of the indirect symbol table and to modify the getenv address stored in the __DATA.__la_symbol_ptr section of the UIKit module. This is something you’ll learn about in a later chapter.
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.
First off, Swift often uses classes or structs in typical development. This is a unique challenge because dlsym will only give you a C function. You’ll need to augment this function so the Swift method can reference self if you’re grabbing an instance method, or reference the class if you’re calling a class method. When accessing a method that belongs to a class, the assembly will often reference offsets of self or the class when performing the method. Since dlysm will grab you a C-type function, you’ll need to creatively utilize your knowledge of assembly, parameters and registers to turn that C function into a Swift method.
The second issue you need to worry about is that Swift mangles the names of its methods. The happy, pretty name you see in your code is actually a scary long name in the module’s symbol table. You’ll need to find this method’s correct mangled name in order to reference the Swift method through dlysm.
As you know, this project produces and displays a watermarked image. Here’s the challenge for you: using only code, display the original image in the UIImageView. You’re not allowed to use LLDB to execute the command yourself, nor are you allowed to modify any contents in memory once the program is running.
Are you up for this challenge? Don’t worry, I’ll show you how it’s done!
First, open AppDelegate.swift and remove all the printing logic found inside application(_:didFinishLaunchingWithOptions:). Next, open CopyrightImageGenerator.swift.
Inside this class is a private computed property containing the originalImage. In addition, there’s a public computed property containing the watermarkedImage. It’s this method that calls the originalImage and superimposes the watermark. It’s up to you to figure out a way to call this originalImage method, without changing the HookingSwift dynamic library at all.
Open ViewController.swift and add the following code to the end of viewDidLoad():
if let handle = dlopen("", RTLD_NOW) {}
You’re using Swift this time, but you’ll use the same dlopen & dlsym trick you saw earlier. You now need to get the correct location of the HookingSwift framework. The nice thing about dlopen is you can supply relative paths instead of absolute paths.
Time to find where that framework is relative to the Watermark executable.
In Xcode, make sure the Project Navigator is visible (through Cmd + 1). Next, open the Products directory and right-click the Watermark.app. Next, select Show in Finder.
Once the Finder window pops up, right click the Watermark bundle and select Show Package Contents.
It’s in this directory the actual Watermark executable is located, so you simply need to find the location of the HookingSwift framework’s executable relative to this Watermark executable.
Next, select the Frameworks directory. Finally select the HookingSwift.framework. Within this directory, you’ll come across the HookingSwift binary.
This means you’ve found the relative path you can supply to dlopen. Modify the dlopen function call you just added so it looks like the following:
if let handle = dlopen("./Frameworks/HookingSwift.framework/HookingSwift", RTLD_NOW) {
}
Now to the hard part. You want to grab the name of the method responsible for the originalImage property inside the CopyrightImageGenerator class. By now, you know you can use the image lookup LLDB function to search for method name compiled into an executable.
Since you know originalImage is implemented in Swift, use a “Swift style” type of search with the image lookup command. Make sure the app is running, then type the following into LLDB:
(lldb) image lookup -rn HookingSwift.*originalImage
You’ll get output similar to the following:
1 match found in /Users/derekselander/Library/Developer/Xcode/DerivedData/Watermark-gbmzjibibkpgfjefjidpgkfzlakw/Build/Products/Debug-iphonesimulator/Watermark.app/Frameworks/HookingSwift.framework/HookingSwift:
Address: HookingSwift[0x0000000000001550] (HookingSwift.__TEXT.__text + 368)
Summary: HookingSwift`HookingSwift.CopyrightImageGenerator.(originalImage in _71AD57F3ABD678B113CF3AD05D01FF41).getter : Swift.Optional<__C.UIImage> at CopyrightImageGenerator.swift:36
In the output, search for the line containing Address: HookingSwift[0x0000000000001550]. This is where this method is implemented inside the HookingSwift framework. This will likely be a different address for you.
For this particular example, the function is implemented at offset 0x0000000000001550 inside the HookingSwift framework. Copy this address and enter the following command into LLDB:
(lldb) image dump symtab -m HookingSwift
This dumps the symbol table of the HookingSwift framework. In addition to dumping the symbol table, you’ve told LLDB to show the mangled names of the Swift functions. There will be quite a few symbols that pop up in the display. Paste that address you copied into the LLDB search bar so the scary amount of output becomes managable.
You’ll get an address that matches the address you copied:
Here’s the line that interests you.
[ 4] 9 D X Code 0x0000000000001550 0x000000010baa4550 0x00000000000000f0 0x000f0000 $S12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg
Yep, that huge angry alphanumeric chunk at the end is the Swift mangled function name. It’s this monstrosity you’ll stick into dlsym to grab the address of the originalImage getter method.
Open ViewController.swift and add the following code inside the if let you just added:
let sym = dlsym(handle, "$S12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg")!
print("\(sym)")
Note: Until Swift stops playing spin the bottle with its ABI naming, these symbol names could (and have!) change from version to version, meaning the mangled function could be different for you.
You’ve opted for an implicitly unwrapped optional since you want the application to crash if you got the wrong symbol name. Build and run the application. If everything worked out, you’ll get a memory address at the tail end of the console output (yours will likely be different):
0x0000000103105770
This address is the location to CopyrightImageGeneratorg’s originalImage method that dlsym provided. You can verify this by creating a breakpoint on this address in LLDB:
(lldb) b 0x0000000103105770
LLDB creates a breakpoint on the following function:
Breakpoint 1: where = HookingSwift`HookingSwift.CopyrightImageGenerator.(originalImage in _71AD57F3ABD678B113CF3AD05D01FF41).getter : Swift.Optional<__ObjC.UIImage> at CopyrightImageGenerator.swift:35, address = 0x0000000103105770
Great! You can bring up the address of this function at runtime, but how do you go about calling it? Thankfully, you can use the typealias Swift keyword to cast functions signatures.
Open ViewController.swift, and add the following directly under the print call you just added:
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
Here’s what this does:
-
This declares the type of function that is syntactically equivalent to the Swift function for the
originalImageproperty getter. There’s something very important to notice here.privateMethodAliasis designed so it takes one parameter type ofAny, but the actual Swift function expects no parameters. Why is this?It’s due to the fact that by looking at the assembly to this method, the reference to
selfis expected in the RDI register. This means you need to supply the instance of the class as the first parameter into the function to trick this C function into thinking it’s a Swift method. If you don’t do this, there’s a chance the application will crash! -
Now you’ve made this new alias, you’re casting the
symaddress to this new type and calling itoriginalImageFunction. -
You’re executing the method and supplying the instance of the class as the first and only parameter to the function. This will cause the
RDIregister to be properly set to the instance of the class. It’ll return the original image without the watermark. -
You’re assigning the
UIImageView’s image to the original image without the watermark.
With these new changes in, build and run the application. As expected, the original, watermark-free image will now be displayed in the application.
Congratulations — you’ve discovered two new amazing functions and how to use them properly. Grabbing the location of code at runtime is a powerful feature that lets you access hidden code the compiler normally blocks from you. In addition, it lets you hook into code so you can perform your own modifications at runtime.
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.
This is one of the most exciting features of reverse engineering — so get prepared, and caffeinated, for your foray into the next chapter!